SingleInstanceHelper.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. using System;
  2. using System.IO;
  3. using System.IO.Pipes;
  4. using System.Threading;
  5. using System.Threading.Tasks;
  6. using Avalonia;
  7. using Avalonia.Controls;
  8. using Avalonia.Controls.ApplicationLifetimes;
  9. using Avalonia.Threading;
  10. using Serilog;
  11. namespace YZWater.Avalonia;
  12. /// <summary>
  13. /// 单实例管理:确保程序只运行一个实例,新启动时激活已有窗口
  14. /// </summary>
  15. internal static class SingleInstanceHelper
  16. {
  17. private const string MutexName = "Global\\YZWater3_SingleInstance";
  18. private const string PipeName = "YZWater3_SingleInstance_Pipe";
  19. private static Mutex? _mutex;
  20. private static CancellationTokenSource? _cts;
  21. /// <summary>
  22. /// 尝试获取单实例权限。true 表示这是第一个实例(或去重机制本身没建起来,照常启动)。
  23. /// </summary>
  24. public static bool TryAcquire()
  25. {
  26. try
  27. {
  28. _mutex = new Mutex(true, MutexName, out bool createdNew);
  29. return createdNew;
  30. }
  31. catch (Exception ex)
  32. {
  33. // 单实例只是体验优化,不能当启动闸门。此刻 Serilog 还没初始化(在 App 构造函数里),
  34. // 异常抛出去就是"双击图标没反应、一行日志都找不到"。
  35. // Global\ 命名空间还受账户权限影响(受限账户/服务账户未必建得起来),
  36. // 失败时放弃去重、继续启动,最多是开出两个窗口,总比开不了机强。
  37. _mutex = null;
  38. Console.Error.WriteLine($"单实例互斥体创建失败,跳过去重继续启动: {ex.Message}");
  39. return true;
  40. }
  41. }
  42. /// <summary>
  43. /// 通知已有实例激活窗口
  44. /// </summary>
  45. public static void ActivateExistingInstance()
  46. {
  47. try
  48. {
  49. using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
  50. client.Connect(1000); // 1秒超时
  51. using var writer = new StreamWriter(client);
  52. writer.WriteLine("ACTIVATE");
  53. writer.Flush();
  54. }
  55. catch (Exception ex)
  56. {
  57. Log.Warning(ex, "通知已有实例失败");
  58. }
  59. }
  60. /// <summary>
  61. /// 启动监听,等待新实例的激活请求
  62. /// </summary>
  63. public static void StartListening()
  64. {
  65. _cts = new CancellationTokenSource();
  66. var token = _cts.Token;
  67. Task.Run(() =>
  68. {
  69. while (!token.IsCancellationRequested)
  70. {
  71. try
  72. {
  73. using var server = new NamedPipeServerStream(
  74. PipeName, PipeDirection.In, 1,
  75. PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
  76. // 等待连接(异步,可取消)
  77. var ar = server.BeginWaitForConnection(null, null);
  78. // 等待句柄完成,同时监听取消
  79. var handles = new[] { ar.AsyncWaitHandle, token.WaitHandle };
  80. var idx = WaitHandle.WaitAny(handles);
  81. if (idx == 1) // token 被取消
  82. {
  83. break;
  84. }
  85. server.EndWaitForConnection(ar);
  86. if (!server.IsConnected) continue;
  87. // 读取命令
  88. using var reader = new StreamReader(server);
  89. var command = reader.ReadLine();
  90. if (command == "ACTIVATE")
  91. {
  92. // 在 UI 线程激活主窗口
  93. Dispatcher.UIThread.Post(() =>
  94. {
  95. ActivateMainWindow();
  96. });
  97. }
  98. // 断开后继续等待下一次
  99. server.Disconnect();
  100. }
  101. catch (Exception ex)
  102. {
  103. if (token.IsCancellationRequested) break;
  104. Log.Warning(ex, "单实例监听异常");
  105. Thread.Sleep(500); // 避免异常风暴
  106. }
  107. }
  108. }, token);
  109. }
  110. /// <summary>
  111. /// 激活主窗口(带到前台并聚焦)
  112. /// </summary>
  113. private static void ActivateMainWindow()
  114. {
  115. try
  116. {
  117. if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  118. {
  119. var mainWindow = desktop.MainWindow;
  120. if (mainWindow == null) return;
  121. // 如果窗口最小化,恢复正常状态
  122. if (mainWindow.WindowState == WindowState.Minimized)
  123. {
  124. mainWindow.WindowState = WindowState.Normal;
  125. }
  126. // 激活窗口(带到前台)
  127. mainWindow.Activate();
  128. mainWindow.Focus();
  129. // 尝试置顶再取消(某些系统需要这个技巧才能真正到前台)
  130. var oldTopmost = mainWindow.Topmost;
  131. mainWindow.Topmost = true;
  132. Task.Delay(100).ContinueWith(_ =>
  133. {
  134. Dispatcher.UIThread.Post(() =>
  135. {
  136. mainWindow.Topmost = oldTopmost;
  137. });
  138. });
  139. Log.Information("已激活主窗口");
  140. }
  141. }
  142. catch (Exception ex)
  143. {
  144. Log.Warning(ex, "激活主窗口失败");
  145. }
  146. }
  147. /// <summary>
  148. /// 释放单实例资源
  149. /// </summary>
  150. public static void Release()
  151. {
  152. Serilog.Log.Information("SingleInstanceHelper 正在释放...");
  153. _cts?.Cancel();
  154. // 等待监听任务完成
  155. // 注意:不能 Wait 太久,因为可能在 UI 线程调用
  156. _cts?.Dispose();
  157. _cts = null;
  158. try
  159. {
  160. _mutex?.ReleaseMutex();
  161. _mutex?.Dispose();
  162. }
  163. catch (Exception ex)
  164. {
  165. Serilog.Log.Warning(ex, "SingleInstanceHelper Mutex 释放失败");
  166. }
  167. _mutex = null;
  168. Serilog.Log.Information("SingleInstanceHelper 已释放");
  169. }
  170. }