Selaa lähdekoodia

fix: 修复程序关闭后后台进程仍在运行的问题

- 调整服务停止顺序:先停止 PlcPollingService 再停止 QuartzSchedulerService
- Quartz 改为 waitForJobsToComplete: false,避免 PlcPollingJob 连接超时卡住关闭流程
- 为各服务 Stop/Dispose 方法添加详细日志,便于追踪停止过程
- 添加 NotificationService 的 Dispose 调用
- 移除 ViewE 页面的 TECH STACK 显示
DESKTOP-74CLTRG\Leol 1 kuukausi sitten
vanhempi
commit
1d206bf114

+ 150 - 9
src/YZWater.Avalonia/App.axaml.cs

@@ -401,51 +401,192 @@ public partial class App : Application
 
     private void StopServices()
     {
+        Serilog.Log.Information("=== StopServices 开始 ===");
+        var sw = System.Diagnostics.Stopwatch.StartNew();
+
         // 取消事件订阅(防止已释放的 hub 收到回调)
         PlcPollingService.Instance.DataUpdated -= OnDataUpdatedForBroadcast;
         AlarmService.Instance.AlarmRaised -= OnAlarmRaisedForBroadcast;
+        Serilog.Log.Information("[{Elapsed}ms] 事件订阅已取消", sw.ElapsedMilliseconds);
+
+        // 1. 先停止看门狗(防止它尝试重启其他正在停止的服务)
+        try
+        {
+            WatchdogService.Instance.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] WatchdogService 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] WatchdogService 停止失败", sw.ElapsedMilliseconds);
+        }
 
-        _hubClient?.Dispose();
+        // 2. 先停止 PLC 轮询(停止 PLC 连接尝试,避免 Quartz Job 卡住)
+        try
+        {
+            PlcPollingService.Instance.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] PlcPollingService 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] PlcPollingService 停止失败", sw.ElapsedMilliseconds);
+        }
+
+        // 3. 停止报警服务
+        try
+        {
+            AlarmService.Instance.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] AlarmService 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] AlarmService 停止失败", sw.ElapsedMilliseconds);
+        }
+
+        // 4. 停止 WebSocket 通信
+        try
+        {
+            _hubClient?.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] HubClient 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] HubClient 停止失败", sw.ElapsedMilliseconds);
+        }
         _hubClient = null;
-        _hubServer?.Dispose();
+
+        try
+        {
+            _hubServer?.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] HubServer 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] HubServer 停止失败", sw.ElapsedMilliseconds);
+        }
         _hubServer = null;
-        _dataLogger?.Dispose();
+
+        // 5. 停止数据日志
+        try
+        {
+            _dataLogger?.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] DataLoggingService 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] DataLoggingService 停止失败", sw.ElapsedMilliseconds);
+        }
         _dataLogger = null;
 
-        // 停止新集成的服务
+        // 6. 停止 Quartz 调度器(此时 PlcPollingJob 不会再有 PLC 连接操作)
         try
         {
+            Serilog.Log.Information("[{Elapsed}ms] 正在停止 QuartzSchedulerService...", sw.ElapsedMilliseconds);
             QuartzSchedulerService.Instance.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] QuartzSchedulerService 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] QuartzSchedulerService 停止失败", sw.ElapsedMilliseconds);
+        }
+
+        // 7. 停止其他服务
+        try
+        {
             WorkflowService.Instance.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] WorkflowService 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] WorkflowService 停止失败", sw.ElapsedMilliseconds);
+        }
+
+        try
+        {
             DeviceStateMachineManager.Instance.Clear();
+            Serilog.Log.Information("[{Elapsed}ms] DeviceStateMachineManager 已清理", sw.ElapsedMilliseconds);
         }
         catch (Exception ex)
         {
-            Serilog.Log.Warning(ex, "停止新服务时出现异常");
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] DeviceStateMachineManager 清理失败", sw.ElapsedMilliseconds);
         }
 
+        try
+        {
+            NotificationService.Instance.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] NotificationService 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] NotificationService 停止失败", sw.ElapsedMilliseconds);
+        }
+
+        // 8. 重置单例(为登出/重登录准备)
         QuartzSchedulerService.ResetInstance();
         WorkflowService.ResetInstance();
-
+        WatchdogService.ResetInstance();
         AlarmService.ResetInstance();
         PlcPollingService.ResetInstance();
-        PlcService.Dispose();
+
+        // 9. 停止 PLC 底层连接
+        try
+        {
+            PlcService.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] PlcService 已停止", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] PlcService 停止失败", sw.ElapsedMilliseconds);
+        }
+
+        sw.Stop();
+        Serilog.Log.Information("=== StopServices 完成 (耗时 {Elapsed}ms) ===", sw.ElapsedMilliseconds);
     }
 
     private void Cleanup()
     {
+        Serilog.Log.Information("========== Cleanup 开始 ==========");
+        var sw = System.Diagnostics.Stopwatch.StartNew();
+
         if (Interlocked.Exchange(ref _cleanupStarted, 1) != 0)
+        {
+            Serilog.Log.Information("Cleanup 已在进行中,跳过重复调用");
             return;
+        }
 
         StopServices();
+        Serilog.Log.Information("[{Elapsed}ms] StopServices 已完成", sw.ElapsedMilliseconds);
+
         if (_userLoggedOutHandler != null)
         {
             AuthService.UserLoggedOut -= _userLoggedOutHandler;
             _userLoggedOutHandler = null;
         }
         ThemeService.Instance.ThemeChanged -= ApplyTheme;
-        SingleInstanceHelper.Release();
-        DatabaseService.Dispose();
+
+        try
+        {
+            SingleInstanceHelper.Release();
+            Serilog.Log.Information("[{Elapsed}ms] SingleInstanceHelper 已释放", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] SingleInstanceHelper 释放失败", sw.ElapsedMilliseconds);
+        }
+
+        try
+        {
+            DatabaseService.Dispose();
+            Serilog.Log.Information("[{Elapsed}ms] DatabaseService 已释放", sw.ElapsedMilliseconds);
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "[{Elapsed}ms] DatabaseService 释放失败", sw.ElapsedMilliseconds);
+        }
+
+        sw.Stop();
+        Serilog.Log.Information("========== Cleanup 完成 (总耗时 {Elapsed}ms) ==========", sw.ElapsedMilliseconds);
+
+        // 最后关闭日志(确保前面的日志都写入)
         LogService.Shutdown();
     }
 

+ 38 - 5
src/YZWater.Avalonia/Services/SingleInstanceHelper.cs

@@ -20,6 +20,7 @@ internal static class SingleInstanceHelper
     private const string PipeName = "YZWater3_SingleInstance_Pipe";
 
     private static Mutex? _mutex;
+    private static CancellationTokenSource? _cts;
 
     /// <summary>
     /// 尝试获取单实例权限
@@ -55,9 +56,12 @@ internal static class SingleInstanceHelper
     /// </summary>
     public static void StartListening()
     {
+        _cts = new CancellationTokenSource();
+        var token = _cts.Token;
+
         Task.Run(() =>
         {
-            while (true)
+            while (!token.IsCancellationRequested)
             {
                 try
                 {
@@ -65,8 +69,18 @@ internal static class SingleInstanceHelper
                         PipeName, PipeDirection.In, 1,
                         PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
 
-                    // 等待连接(阻塞)
+                    // 等待连接(异步,可取消)
                     var ar = server.BeginWaitForConnection(null, null);
+
+                    // 等待句柄完成,同时监听取消
+                    var handles = new[] { ar.AsyncWaitHandle, token.WaitHandle };
+                    var idx = WaitHandle.WaitAny(handles);
+
+                    if (idx == 1) // token 被取消
+                    {
+                        break;
+                    }
+
                     server.EndWaitForConnection(ar);
 
                     if (!server.IsConnected) continue;
@@ -89,11 +103,12 @@ internal static class SingleInstanceHelper
                 }
                 catch (Exception ex)
                 {
+                    if (token.IsCancellationRequested) break;
                     Log.Warning(ex, "单实例监听异常");
                     Thread.Sleep(500); // 避免异常风暴
                 }
             }
-        });
+        }, token);
     }
 
     /// <summary>
@@ -143,8 +158,26 @@ internal static class SingleInstanceHelper
     /// </summary>
     public static void Release()
     {
-        _mutex?.ReleaseMutex();
-        _mutex?.Dispose();
+        Serilog.Log.Information("SingleInstanceHelper 正在释放...");
+
+        _cts?.Cancel();
+
+        // 等待监听任务完成
+        // 注意:不能 Wait 太久,因为可能在 UI 线程调用
+        _cts?.Dispose();
+        _cts = null;
+
+        try
+        {
+            _mutex?.ReleaseMutex();
+            _mutex?.Dispose();
+        }
+        catch (Exception ex)
+        {
+            Serilog.Log.Warning(ex, "SingleInstanceHelper Mutex 释放失败");
+        }
         _mutex = null;
+
+        Serilog.Log.Information("SingleInstanceHelper 已释放");
     }
 }

+ 1 - 6
src/YZWater.Avalonia/Views/ViewEView.axaml

@@ -2,7 +2,7 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:vm="using:YZWater.Core.ViewModels"
              x:Class="YZWater.Avalonia.Views.ViewEView"
-            >
+             x:DataType="vm:ViewEViewModel">
 
 
     <Border x:Name="RootBorder" Background="{DynamicResource AppBgBrush}">
@@ -50,11 +50,6 @@
                             </StackPanel>
                             <TextBlock Text="{Binding Description}" TextWrapping="Wrap"
                                        Foreground="{DynamicResource TextSecondaryBrush}" FontSize="13"/>
-                            <Separator Margin="0,4"/>
-                            <Grid ColumnDefinitions="100,*" RowDefinitions="Auto,Auto">
-                                <TextBlock Grid.Row="0" Grid.Column="0" Text="TECH STACK" FontFamily="{DynamicResource MonoFont}" FontSize="10" Foreground="{DynamicResource TextTertiaryBrush}"/>
-                                <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding TechStack}" FontFamily="{DynamicResource MonoFont}" FontSize="10" Foreground="{DynamicResource TextSecondaryBrush}"/>
-                            </Grid>
                         </StackPanel>
                     </Border>
 

+ 25 - 8
src/YZWater.Core/Services/DataLoggingService.cs

@@ -111,18 +111,34 @@ public class DataLoggingService : IDisposable
     /// </summary>
     public void Stop()
     {
+        Log.Information("DataLoggingService 正在停止... _cts={HasCts}, _loggingTask={HasTask}",
+            _cts != null, _loggingTask != null);
+
         _cts?.Cancel();
-        try
-        {
-            _loggingTask?.Wait(TimeSpan.FromSeconds(2));
-        }
-        catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
+
+        if (_loggingTask != null)
         {
-            // 取消是正常退出路径。
+            try
+            {
+                var completed = _loggingTask.Wait(TimeSpan.FromSeconds(2));
+                if (!completed)
+                {
+                    Log.Warning("DataLoggingService 日志任务在2秒内未完成");
+                }
+                else
+                {
+                    Log.Information("DataLoggingService 日志任务已完成");
+                }
+            }
+            catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
+            {
+                // 取消是正常退出路径
+                Log.Information("DataLoggingService 日志任务已取消(正常)");
+            }
         }
 
         _loggingTask = null;
-        Log.Information("数据日志服务已停止");
+        Log.Information("DataLoggingService 已停止");
     }
 
     /// <summary>
@@ -408,9 +424,10 @@ public class DataLoggingService : IDisposable
     {
         if (_disposed) return;
         _disposed = true;
+        Log.Information("DataLoggingService 正在释放...");
         Stop();
         _cts?.Dispose();
         _cts = null;
-        Log.Information("数据日志服务已释放");
+        Log.Information("DataLoggingService 已释放");
     }
 }

+ 13 - 1
src/YZWater.Core/Services/HubClient.cs

@@ -383,6 +383,10 @@ public class HubClient : IDisposable
     {
         if (_disposed) return;
         _disposed = true;
+
+        Log.Information("HubClient 正在释放... _shouldReconnect={Reconnect}, _connectionTask={HasTask}",
+            _shouldReconnect, _connectionTask != null);
+
         _shouldReconnect = false;
         _cts?.Cancel();
 
@@ -396,15 +400,23 @@ public class HubClient : IDisposable
         }
 
         _ws?.Dispose();
+
         try
         {
-            _connectionTask?.Wait(TimeSpan.FromSeconds(2));
+            var completed = _connectionTask?.Wait(TimeSpan.FromSeconds(2));
+            if (completed == true)
+                Log.Information("HubClient 连接任务已完成");
+            else
+                Log.Warning("HubClient 连接任务在2秒内未完成");
         }
         catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
         {
             // 取消是正常退出路径。
+            Log.Information("HubClient 连接任务已取消(正常)");
         }
         _connectionTask = null;
         _cts?.Dispose();
+
+        Log.Information("HubClient 已释放");
     }
 }

+ 10 - 2
src/YZWater.Core/Services/HubServer.cs

@@ -149,6 +149,8 @@ public class HubServer : IDisposable
     {
         if (!IsRunning) return;
 
+        Log.Information("HubServer 正在停止... 客户端数: {Count}", _clients.Count);
+
         _cts?.Cancel();
         _listener?.Stop();
 
@@ -160,14 +162,20 @@ public class HubServer : IDisposable
             ws.Dispose();
         }
         _clients.Clear();
+        Log.Information("HubServer 客户端连接已关闭");
 
         try
         {
-            _acceptTask?.Wait(TimeSpan.FromSeconds(2));
+            var completed = _acceptTask?.Wait(TimeSpan.FromSeconds(2));
+            if (completed == true)
+                Log.Information("HubServer 接受任务已完成");
+            else
+                Log.Warning("HubServer 接受任务在2秒内未完成");
         }
         catch (AggregateException ex) when (ex.InnerExceptions.All(e => e is OperationCanceledException))
         {
             // 取消是正常退出路径。
+            Log.Information("HubServer 接受任务已取消(正常)");
         }
         _acceptTask = null;
         _listener?.Close();
@@ -176,7 +184,7 @@ public class HubServer : IDisposable
         _cts = null;
 
         IsRunning = false;
-        Log.Information("中控服务端已停止");
+        Log.Information("HubServer 已停止");
     }
 
     /// <summary>

+ 20 - 3
src/YZWater.Core/Services/PlcPollingService.cs

@@ -73,12 +73,28 @@ public partial class PlcPollingService : ObservableObject, IDisposable, IService
     /// </summary>
     public void Stop()
     {
+        Log.Information("PlcPollingService 正在停止... _cts={HasCts}, _pollingTask={HasTask}",
+            _cts != null, _pollingTask != null);
+
         _cts?.Cancel();
-        _pollingTask?.Wait(TimeSpan.FromSeconds(5));
+
+        if (_pollingTask != null)
+        {
+            var completed = _pollingTask.Wait(TimeSpan.FromSeconds(5));
+            if (!completed)
+            {
+                Log.Warning("PlcPollingService 轮询任务在5秒内未完成,可能仍在运行");
+            }
+            else
+            {
+                Log.Information("PlcPollingService 轮询任务已完成");
+            }
+        }
+
         _cts?.Dispose();
         _cts = null;
         _pollingTask = null;
-        Log.Information("PLC 轮询服务已停止");
+        Log.Information("PlcPollingService 已停止");
     }
 
     /// <summary>
@@ -506,8 +522,9 @@ public partial class PlcPollingService : ObservableObject, IDisposable, IService
     {
         if (_disposed) return;
         _disposed = true;
+        Log.Information("PlcPollingService 正在释放...");
         Stop();
-        Log.Information("PLC 轮询服务已释放");
+        Log.Information("PlcPollingService 已释放");
     }
 
     /// <summary>

+ 15 - 4
src/YZWater.Core/Services/QuartzSchedulerService.cs

@@ -92,8 +92,9 @@ public class QuartzSchedulerService : IDisposable
 
         try
         {
-            // 等待正在执行的任务完成
-            await _scheduler.Shutdown(waitForJobsToComplete: true);
+            Log.Information("Quartz 调度器正在停止...");
+            // 不等待 Job 完成,立即关闭(避免 PlcPollingJob 连接 PLC 超时卡住关闭流程)
+            await _scheduler.Shutdown(waitForJobsToComplete: false);
             _isStarted = false;
             Log.Information("Quartz 调度器已停止");
         }
@@ -275,8 +276,18 @@ public class QuartzSchedulerService : IDisposable
         if (_disposed) return;
         _disposed = true;
 
-        // 同步等待停止
-        StopAsync().GetAwaiter().GetResult();
+        Log.Information("Quartz 调度服务正在释放...");
+
+        // 同步等待停止(但 StopAsync 内部已有超时保护)
+        try
+        {
+            StopAsync().GetAwaiter().GetResult();
+        }
+        catch (Exception ex)
+        {
+            Log.Warning(ex, "Quartz 调度服务同步停止时出现异常,继续释放");
+        }
+
         _scheduler = null;
         Log.Information("Quartz 调度服务已释放");
     }

+ 33 - 3
src/YZWater.Core/Services/WatchdogService.cs

@@ -21,6 +21,19 @@ public class WatchdogService : IDisposable
         }
     }
 
+    /// <summary>
+    /// 重置单例(用于登出后重新登录)
+    /// </summary>
+    public static void ResetInstance()
+    {
+        lock (_instanceLock)
+        {
+            var old = _instance;
+            _instance = null;
+            old?.Dispose();
+        }
+    }
+
     private CancellationTokenSource? _cts;
     private Task? _watchdogTask;
     private bool _disposed;
@@ -91,12 +104,28 @@ public class WatchdogService : IDisposable
     /// </summary>
     public void Stop()
     {
+        Log.Information("WatchdogService 正在停止... _cts={HasCts}, _watchdogTask={HasTask}",
+            _cts != null, _watchdogTask != null);
+
         _cts?.Cancel();
-        _watchdogTask?.Wait(TimeSpan.FromSeconds(5));
+
+        if (_watchdogTask != null)
+        {
+            var completed = _watchdogTask.Wait(TimeSpan.FromSeconds(5));
+            if (!completed)
+            {
+                Log.Warning("WatchdogService 看门狗任务在5秒内未完成");
+            }
+            else
+            {
+                Log.Information("WatchdogService 看门狗任务已完成");
+            }
+        }
+
         _cts?.Dispose();
         _cts = null;
         _watchdogTask = null;
-        Log.Information("看门狗服务已停止");
+        Log.Information("WatchdogService 已停止");
     }
 
     /// <summary>
@@ -270,11 +299,12 @@ public class WatchdogService : IDisposable
     {
         if (_disposed) return;
         _disposed = true;
+        Log.Information("WatchdogService 正在释放...");
         Stop();
         _healthChecks.Clear();
         _failureCounts.Clear();
         _lastRestartTimes.Clear();
-        Log.Information("看门狗服务已释放");
+        Log.Information("WatchdogService 已释放");
     }
 }