From 0b22f18c5fea01cc3c4813db9fdfad82dc574ffb Mon Sep 17 00:00:00 2001 From: fplj-fplj Date: Fri, 21 Aug 2026 12:57:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=95=BF=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E8=BF=90=E8=A1=8C=E5=90=8E=E9=9A=8F=E6=9C=BA=E9=97=AA?= =?UTF-8?q?=E9=80=80=E7=9A=84=E5=90=8E=E5=8F=B0=E7=BA=BF=E7=A8=8B=E6=9C=AA?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 运行 20 分钟至数小时后出现闪退,通常不是启动即崩,而是后台线程 (SystemEvents / System.Threading.Timer / System.Timers.Timer)抛出 未处理异常导致进程直接退出。本次针对以下路径做防御性修复: - SystemEvents.UserPreferenceChanged / DisplaySettingsChanged 在系统 事件专用线程触发,原实现直接访问 WPF UI 对象,跨线程访问会抛 InvalidOperationException 并终止进程;现封送到主线程 Dispatcher, 并在 Window_Closed 取消订阅 UserPreferenceChanged。 - HandleFloatingBarRecovery 的 DelayAction 回调在 Timer 线程直接 执行 ViewboxFloatingBarMarginAnimation,现改为仅负责 Dispatcher.BeginInvoke 封送。 - 看门狗 watchdogTimer 回调整体增加 try/catch,避免看门狗自身异常 成为闪退源。 - WindowOverviewModel.WindowsUpdated、SystemEventMonitor 的 ForegroundWindowChanged/ProcessChanged、RulesetService 的 StatusUpdated 等事件订阅者异常加兜底,避免在 Timer 线程上冒泡。 - DelayAction / TimerTrigger / 时间与日期显示定时器回调增加异常保护。 由于无可测试环境,尚未测试。 --- Ink Canvas/App.xaml.cs | 104 +++++++++++------- .../Automation/Services/RulesetService.cs | 28 ++++- .../Automation/Services/SystemEventMonitor.cs | 19 +++- .../Automation/Triggers/TimerTrigger.cs | 14 ++- Ink Canvas/Helpers/DelayActionHelper.cs | 10 +- Ink Canvas/Helpers/WindowOverviewModel.cs | 12 +- Ink Canvas/MainWindow.xaml.cs | 66 +++++++++-- Ink Canvas/MainWindow_cs/MW_AutoTheme.cs | 48 ++++++-- Ink Canvas/MainWindow_cs/MW_Timer.cs | 96 +++++++++------- 9 files changed, 284 insertions(+), 113 deletions(-) diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index 80caedd9d..d41170e9e 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -979,7 +979,14 @@ private void App_DispatcherUnhandledException(object sender, DispatcherUnhandled } } - Ink_Canvas.MainWindow.ShowNewMessage(MainWindowStrings.Main_App_UnexpectedError); + try + { + Ink_Canvas.MainWindow.ShowNewMessage(MainWindowStrings.Main_App_UnexpectedError); + } + catch (Exception notifyEx) + { + System.Diagnostics.Debug.WriteLine(notifyEx); + } LogHelper.NewLog(e.Exception.ToString()); // 记录到崩溃日志 @@ -1861,57 +1868,74 @@ private void StartHeartbeatMonitor() watchdogTimer = new Timer(_ => { - if (isAppExiting) - return; - if (IsOobeShowing) - return; - - if (!isStartupComplete && appStartupStartTime != DateTime.MinValue) + try { - DateTime startTime = _isSplashScreenShown && splashScreenStartTime != DateTime.MinValue - ? splashScreenStartTime - : appStartupStartTime; - TimeSpan elapsedSinceStart = DateTime.Now - startTime; - if (elapsedSinceStart.TotalMinutes >= 2) + if (isAppExiting) + return; + if (IsOobeShowing) + return; + + if (!isStartupComplete && appStartupStartTime != DateTime.MinValue) { - string timeType = _isSplashScreenShown ? "启动画面已显示" : "应用启动开始"; - string restartReason = $"检测到启动假死:{timeType}{elapsedSinceStart.TotalMinutes:F2}分钟,但未收到启动完成心跳,自动重启。"; - LogHelper.WriteLogToFile(restartReason, LogHelper.LogType.Error); - WriteCrashLog(restartReason); - SyncCrashActionFromSettings(); - if (CrashAction == CrashActionType.SilentRestart) + DateTime startTime = _isSplashScreenShown && splashScreenStartTime != DateTime.MinValue + ? splashScreenStartTime + : appStartupStartTime; + TimeSpan elapsedSinceStart = DateTime.Now - startTime; + if (elapsedSinceStart.TotalMinutes >= 2) { - TryRestartWithBreaker(restartReason); + string timeType = _isSplashScreenShown ? "启动画面已显示" : "应用启动开始"; + string restartReason = $"检测到启动假死:{timeType}{elapsedSinceStart.TotalMinutes:F2}分钟,但未收到启动完成心跳,自动重启。"; + LogHelper.WriteLogToFile(restartReason, LogHelper.LogType.Error); + WriteCrashLog(restartReason); + SyncCrashActionFromSettings(); + if (CrashAction == CrashActionType.SilentRestart) + { + TryRestartWithBreaker(restartReason); + } + return; } - return; } - } - - if (isStartupComplete) - { - var now = DateTime.Now; - var sinceHeartbeat = now - lastHeartbeat; - var sinceStartupComplete = startupCompleteHeartbeat == DateTime.MinValue - ? TimeSpan.Zero - : now - startupCompleteHeartbeat; - if (sinceStartupComplete.TotalSeconds < 30) + if (isStartupComplete) { - return; - } + var now = DateTime.Now; + var sinceHeartbeat = now - lastHeartbeat; + var sinceStartupComplete = startupCompleteHeartbeat == DateTime.MinValue + ? TimeSpan.Zero + : now - startupCompleteHeartbeat; - if (sinceHeartbeat.TotalSeconds > 10) - { - string restartReason = $"检测到主线程无响应,自动重启。心跳超时 {sinceHeartbeat.TotalSeconds:F1} 秒。"; - LogHelper.NewLog(restartReason); - WriteCrashLog(restartReason); - SyncCrashActionFromSettings(); - if (CrashAction == CrashActionType.SilentRestart) + if (sinceStartupComplete.TotalSeconds < 30) { - TryRestartWithBreaker(restartReason); + return; + } + + if (sinceHeartbeat.TotalSeconds > 10) + { + string restartReason = $"检测到主线程无响应,自动重启。心跳超时 {sinceHeartbeat.TotalSeconds:F1} 秒。"; + LogHelper.NewLog(restartReason); + WriteCrashLog(restartReason); + SyncCrashActionFromSettings(); + if (CrashAction == CrashActionType.SilentRestart) + { + TryRestartWithBreaker(restartReason); + } } } } + catch (Exception ex) + { + // 看门狗回调运行在 ThreadPool 上,任何未捕获异常都会直接终止进程。 + // 这里兜底记录,避免看门狗自身成为闪退源。 + try + { + LogHelper.WriteLogToFile($"心跳看门狗回调异常: {ex.Message}", LogHelper.LogType.Error); + WriteCrashLog($"心跳看门狗回调异常: {ex}"); + } + catch + { + System.Diagnostics.Debug.WriteLine(ex); + } + } }, null, 0, 3000); } diff --git a/Ink Canvas/Automation/Services/RulesetService.cs b/Ink Canvas/Automation/Services/RulesetService.cs index aa184da02..7d92c47a2 100644 --- a/Ink Canvas/Automation/Services/RulesetService.cs +++ b/Ink Canvas/Automation/Services/RulesetService.cs @@ -1,3 +1,4 @@ +using Ink_Canvas.Helpers; using Ink_Canvas.WorkflowAutomation.Abstractions; using Ink_Canvas.WorkflowAutomation.Enums; using Ink_Canvas.WorkflowAutomation.Models; @@ -52,12 +53,26 @@ public RulesetService() private void OnStatusMayHaveChanged(object sender, EventArgs e) { - NotifyStatusChanged(); + try + { + NotifyStatusChanged(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"规则状态变化事件处理失败: {ex.Message}", LogHelper.LogType.Warning); + } } private void OnFallbackTimerElapsed(object sender, ElapsedEventArgs e) { - NotifyStatusChanged(); + try + { + NotifyStatusChanged(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"规则状态兜底轮询处理失败: {ex.Message}", LogHelper.LogType.Warning); + } } /// @@ -217,7 +232,14 @@ public void UnregisterRuleHandler(string id, RuleRegistryInfo.HandleDelegate han /// public void NotifyStatusChanged() { - StatusUpdated?.Invoke(this, EventArgs.Empty); + try + { + StatusUpdated?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"规则状态更新通知处理失败: {ex.Message}", LogHelper.LogType.Warning); + } } public void Dispose() diff --git a/Ink Canvas/Automation/Services/SystemEventMonitor.cs b/Ink Canvas/Automation/Services/SystemEventMonitor.cs index ae62a06aa..398f057b4 100644 --- a/Ink Canvas/Automation/Services/SystemEventMonitor.cs +++ b/Ink Canvas/Automation/Services/SystemEventMonitor.cs @@ -1,3 +1,4 @@ +using Ink_Canvas.Helpers; using System; using System.Collections.Generic; using System.Diagnostics; @@ -198,7 +199,14 @@ private void OnProcessTimerElapsed(object sender, ElapsedEventArgs e) if (anyChanged) { - ProcessChanged?.Invoke(this, EventArgs.Empty); + try + { + ProcessChanged?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"进程状态变化事件处理失败: {ex.Message}", LogHelper.LogType.Warning); + } } } @@ -221,7 +229,14 @@ private static bool CheckProcessRunning(string processName) private void OnForegroundWindowEvent(HWINEVENTHOOK hWinEventHook, uint eventType, HWND hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) { - ForegroundWindowChanged?.Invoke(this, EventArgs.Empty); + try + { + ForegroundWindowChanged?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"前台窗口变化事件处理失败: {ex.Message}", LogHelper.LogType.Warning); + } } //[DllImport("user32.dll", SetLastError = true)] diff --git a/Ink Canvas/Automation/Triggers/TimerTrigger.cs b/Ink Canvas/Automation/Triggers/TimerTrigger.cs index bfe56d375..be4a02e39 100644 --- a/Ink Canvas/Automation/Triggers/TimerTrigger.cs +++ b/Ink Canvas/Automation/Triggers/TimerTrigger.cs @@ -1,4 +1,5 @@ using Ink_Canvas.WorkflowAutomation.Abstractions; +using System; using System.Timers; namespace Ink_Canvas.WorkflowAutomation.Triggers @@ -50,9 +51,16 @@ public override void UnLoaded() private void OnTimerElapsed(object sender, ElapsedEventArgs e) { - if (Settings.TriggerOnce && _hasTriggered) return; - _hasTriggered = true; - Trigger(); + try + { + if (Settings.TriggerOnce && _hasTriggered) return; + _hasTriggered = true; + Trigger(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"TimerTrigger.OnTimerElapsed: {ex.Message}"); + } } } } diff --git a/Ink Canvas/Helpers/DelayActionHelper.cs b/Ink Canvas/Helpers/DelayActionHelper.cs index 667862481..a47f42263 100644 --- a/Ink Canvas/Helpers/DelayActionHelper.cs +++ b/Ink Canvas/Helpers/DelayActionHelper.cs @@ -25,7 +25,15 @@ public void DebounceAction(int timeMs, ISynchronizeInvoke inv, Action action) // 解除订阅,打破 timer.Elapsed → lambda → timer 循环引用 _timerDebounce.Elapsed -= elapsedHandler; _timerDebounce.Stop(); _timerDebounce.Close(); _timerDebounce = null; - InvokeAction(action, inv); + try + { + InvokeAction(action, inv); + } + catch (Exception ex) + { + // 回调运行在 System.Timers.Timer 线程上,未捕获异常会直接终止进程。 + System.Diagnostics.Debug.WriteLine($"DelayAction 回调异常: {ex.Message}"); + } }; _timerDebounce.Elapsed += elapsedHandler; } diff --git a/Ink Canvas/Helpers/WindowOverviewModel.cs b/Ink Canvas/Helpers/WindowOverviewModel.cs index e3c14e49e..cc43f02bc 100644 --- a/Ink Canvas/Helpers/WindowOverviewModel.cs +++ b/Ink Canvas/Helpers/WindowOverviewModel.cs @@ -432,8 +432,16 @@ public void UpdateWindows() _windows = windows; } - // 触发更新事件 - WindowsUpdated?.Invoke(this, windows); + // 触发更新事件。订阅方(插件等)可能抛异常,而本方法通常运行在 + // System.Threading.Timer 的后台线程上,未捕获的异常会直接终止进程。 + try + { + WindowsUpdated?.Invoke(this, windows); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"窗口概览更新事件处理失败: {ex.Message}", LogHelper.LogType.Warning); + } } /// diff --git a/Ink Canvas/MainWindow.xaml.cs b/Ink Canvas/MainWindow.xaml.cs index 9845abea3..11e1efdf1 100644 --- a/Ink Canvas/MainWindow.xaml.cs +++ b/Ink Canvas/MainWindow.xaml.cs @@ -1696,9 +1696,31 @@ private void Window_Loaded(object sender, RoutedEventArgs e) private void SystemEventsOnDisplaySettingsChanged(object sender, EventArgs e) { - if (!Settings.Advanced.IsEnableResolutionChangeDetection) return; - ShowNotification(string.Format(Properties.MainWindowStrings.Main_DisplayChanged, Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height)); - HandleFloatingBarRecovery(); + // SystemEvents 事件在系统事件专用线程上触发,不能直接访问 WPF UI 对象; + // 必须封送到主线程,否则会因跨线程访问 UI 抛出未处理异常导致进程闪退。 + try + { + if (Dispatcher == null || Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) + return; + + Dispatcher.BeginInvoke(new Action(() => + { + try + { + if (Settings?.Advanced == null || !Settings.Advanced.IsEnableResolutionChangeDetection) return; + ShowNotification(string.Format(Properties.MainWindowStrings.Main_DisplayChanged, Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height)); + HandleFloatingBarRecovery(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"显示器配置变化处理失败: {ex.Message}", LogHelper.LogType.Warning); + } + }), DispatcherPriority.Normal); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"调度显示器配置变化处理失败: {ex.Message}", LogHelper.LogType.Warning); + } } private void MainWindow_OnDpiChanged(object sender, DpiChangedEventArgs e) @@ -1724,14 +1746,39 @@ private void HandleFloatingBarRecovery() isFloatingBarOutsideScreen = IsOutsideOfScreenHelper.IsOutsideOfScreen(ViewboxFloatingBar); isInPPTPresentationMode = IsInPPTPresentationMode; }, DispatcherPriority.Normal, TimeSpan.FromSeconds(5)); - if (isFloatingBarOutsideScreen) dpiChangedDelayAction.DebounceAction(3000, null, () => + if (isFloatingBarOutsideScreen) { - if (!isFloatingBarFolded) + // DelayAction 在 null 同步对象时会在 System.Timers.Timer 线程直接执行回调; + // 该回调若直接访问 WPF 控件会因跨线程访问 UI 导致未处理异常。这里只负责封送。 + dpiChangedDelayAction.DebounceAction(3000, null, () => { - if (isInPPTPresentationMode) ViewboxFloatingBarMarginAnimation(60); - else ViewboxFloatingBarMarginAnimation(100, true); - } - }); + try + { + if (Dispatcher == null || Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) + return; + + Dispatcher.BeginInvoke(new Action(() => + { + try + { + if (!isFloatingBarFolded) + { + if (isInPPTPresentationMode) ViewboxFloatingBarMarginAnimation(60); + else ViewboxFloatingBarMarginAnimation(100, true); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"浮动工具栏恢复动画失败: {ex.Message}", LogHelper.LogType.Warning); + } + }), DispatcherPriority.Normal); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"调度浮动工具栏恢复失败: {ex.Message}", LogHelper.LogType.Warning); + } + }); + } } catch (Exception ex) { @@ -1926,6 +1973,7 @@ private void Window_Closed(object sender, EventArgs e) { RealtimeInkFrameScheduler.Clear(); SystemEvents.DisplaySettingsChanged -= SystemEventsOnDisplaySettingsChanged; + SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged; // 玻璃浮动栏刻意不设 Owner,必须显式关闭,否则残留窗口会挡住进程退出 HideLiquidGlassBar(); diff --git a/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs b/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs index 8ecc6dbbc..885920feb 100644 --- a/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs +++ b/Ink Canvas/MainWindow_cs/MW_AutoTheme.cs @@ -282,19 +282,43 @@ void SetFloatingBarButtonBrush(ToolbarImageButton btn) private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e) { - switch (Settings.Appearance.Theme) + // SystemEvents 事件在系统事件专用线程上触发,不能直接访问 WPF UI 对象; + // 必须封送到主线程,否则会因跨线程访问 UI 抛出未处理异常导致进程闪退。 + try + { + if (Dispatcher == null || Dispatcher.HasShutdownStarted || Dispatcher.HasShutdownFinished) + return; + + Dispatcher.BeginInvoke(new Action(() => + { + try + { + if (Settings?.Appearance == null) return; + + switch (Settings.Appearance.Theme) + { + case 0: + SetTheme(ThemeLight); + break; + case 1: + SetTheme(ThemeDark); + break; + case 2: + // 与 IsCurrentThemeDark / GetEffectiveTheme / 浮动栏一致,统一读 AppsUseLightTheme, + // 否则 SystemUsesLightTheme 与 AppsUseLightTheme 可独立取值时主题会混搭 + SetTheme(ThemeHelper.IsSystemThemeLight() ? ThemeLight : ThemeDark); + break; + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"用户偏好变化(主题)处理失败: {ex.Message}", LogHelper.LogType.Warning); + } + }), DispatcherPriority.Normal); + } + catch (Exception ex) { - case 0: - SetTheme(ThemeLight); - break; - case 1: - SetTheme(ThemeDark); - break; - case 2: - // 与 IsCurrentThemeDark / GetEffectiveTheme / 浮动栏一致,统一读 AppsUseLightTheme, - // 否则 SystemUsesLightTheme 与 AppsUseLightTheme 可独立取值时主题会混搭 - SetTheme(ThemeHelper.IsSystemThemeLight() ? ThemeLight : ThemeDark); - break; + LogHelper.WriteLogToFile($"调度用户偏好变化(主题)失败: {ex.Message}", LogHelper.LogType.Warning); } } diff --git a/Ink Canvas/MainWindow_cs/MW_Timer.cs b/Ink Canvas/MainWindow_cs/MW_Timer.cs index d323206f7..15e8eacfc 100644 --- a/Ink Canvas/MainWindow_cs/MW_Timer.cs +++ b/Ink Canvas/MainWindow_cs/MW_Timer.cs @@ -431,51 +431,58 @@ private async Task TimerNtpSync_ElapsedAsync() /// private void TimerDisplayTime_Elapsed(object sender, ElapsedEventArgs e) { - DateTime localTime = DateTime.Now; - DateTime displayTime = localTime; + try + { + DateTime localTime = DateTime.Now; + DateTime displayTime = localTime; - TimeSpan timeJump = localTime - lastLocalTime; - double timeJumpMinutes = Math.Abs(timeJump.TotalMinutes); + TimeSpan timeJump = localTime - lastLocalTime; + double timeJumpMinutes = Math.Abs(timeJump.TotalMinutes); - if (timeJumpMinutes > 3 && !isNtpSyncing) - { - Task.Run(async () => + if (timeJumpMinutes > 3 && !isNtpSyncing) { - try - { - await TimerNtpSync_ElapsedAsync(); - } - catch (Exception ex) + Task.Run(async () => { - LogHelper.WriteLogToFile($"时间跳跃触发的NTP同步失败: {ex.Message}", LogHelper.LogType.Error); - } - }); - } - lastLocalTime = localTime; - - if (useNetworkTime && networkTimeOffset != TimeSpan.Zero) - { - displayTime = localTime + networkTimeOffset; - } + try + { + await TimerNtpSync_ElapsedAsync(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"时间跳跃触发的NTP同步失败: {ex.Message}", LogHelper.LogType.Error); + } + }); + } + lastLocalTime = localTime; - string timeString; - if (Settings.Appearance.Use24HourTimeFormat) - { - timeString = displayTime.ToString("HH:mm:ss"); - } - else - { - timeString = displayTime.ToString("tt hh'时'mm'分'ss'秒'"); - } + if (useNetworkTime && networkTimeOffset != TimeSpan.Zero) + { + displayTime = localTime + networkTimeOffset; + } - if (timeString != lastDisplayedTime) - { - lastDisplayedTime = timeString; + string timeString; + if (Settings.Appearance.Use24HourTimeFormat) + { + timeString = displayTime.ToString("HH:mm:ss"); + } + else + { + timeString = displayTime.ToString("tt hh'时'mm'分'ss'秒'"); + } - Dispatcher.BeginInvoke(new Action(() => + if (timeString != lastDisplayedTime) { - nowTimeVM.nowTime = timeString; - })); + lastDisplayedTime = timeString; + + Dispatcher.BeginInvoke(new Action(() => + { + nowTimeVM.nowTime = timeString; + })); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"时间显示定时器回调异常: {ex.Message}", LogHelper.LogType.Warning); } } @@ -490,11 +497,18 @@ private void TimerDisplayTime_Elapsed(object sender, ElapsedEventArgs e) /// private void TimerDisplayDate_Elapsed(object sender, ElapsedEventArgs e) { - // 使用BeginInvoke异步更新UI,避免阻塞 - Dispatcher.BeginInvoke(new Action(() => + try { - nowTimeVM.nowDate = DateTime.Now.ToString("yyyy'年'MM'月'dd'日' dddd"); - })); + // 使用BeginInvoke异步更新UI,避免阻塞 + Dispatcher.BeginInvoke(new Action(() => + { + nowTimeVM.nowDate = DateTime.Now.ToString("yyyy'年'MM'月'dd'日' dddd"); + })); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"日期显示定时器回调异常: {ex.Message}", LogHelper.LogType.Warning); + } } private DispatcherTimer _dispatcherTimerForTime;