From 4aaafb8f41dcdd989e20c03001fedffdba3e6b15 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 3 Oct 2026 20:30:36 +0800 Subject: [PATCH] fix(exit): point clients back to direct when macOS quits the app Cmd+Q, Dock "Quit" and logout/shutdown go through applicationWillTerminate, which Tauri reports only as RunEvent::Exit, never as ExitRequested. The exit handler only ran the cleanup on ExitRequested, so quitting this way left Claude Code / Codex / Gemini / Grok Build pointed at the stopped local proxy until CC Switch was started again. Run the same cleanup on RunEvent::Exit: it is spawned onto the async runtime and the main thread waits for it with an 8 s cap, so a step that needs the main thread cannot hang the quit. Restart requests also end in RunEvent::Exit; they are flagged and skipped so the restart keeps Tauri's default path. --- src-tauri/src/lib.rs | 39 ++++++++++++++++++++++++++++++++++++++- src-tauri/src/tray.rs | 3 ++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8a9f5f254..08abaf422 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -76,7 +76,6 @@ pub use store::AppState; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; -#[cfg(target_os = "windows")] use std::sync::atomic::{AtomicBool, Ordering}; use std::{fmt, sync::Arc}; use tauri::tray::{TrayIconBuilder, TrayIconEvent}; @@ -1774,6 +1773,7 @@ pub fn run() { // 与所有 Tauri 应用默认重启路径的行为一致,无需额外等待 ExitRequestAction::DeferToTauriRestart => { log::info!("收到重启请求 (code={code:?}),交由 Tauri 默认重启流程 re-exec"); + RESTART_REQUESTED.store(true, Ordering::SeqCst); return; } // 其它 Some(_):用户主动调用 app.exit() 退出(如托盘菜单"退出"), @@ -1804,6 +1804,16 @@ pub fn run() { return; } + // macOS ⌘Q、Dock「退出」、注销关机走系统 terminate,不发 ExitRequested、只发 + // RunEvent::Exit,回调一返回进程就结束,只能在这里同步补做退出清理。重启也会走到 + // 这里,照上面 DeferToTauriRestart 的约定交还 Tauri 默认流程,不清理。 + if matches!(event, RunEvent::Exit) { + if !RESTART_REQUESTED.load(Ordering::SeqCst) { + cleanup_before_system_exit(app_handle); + } + return; + } + #[cfg(target_os = "macos")] { match event { @@ -1916,6 +1926,30 @@ pub async fn cleanup_before_exit(app_handle: &tauri::AppHandle) { } } +/// 系统终止应用时最多等退出清理这么久:停代理服务自带 5 秒超时,指回直连只是写几个文件。 +const SYSTEM_EXIT_CLEANUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(8); + +/// 系统直接终止应用时(macOS ⌘Q、Dock「退出」、注销关机)的退出清理。 +/// +/// 这条路没有 `ExitRequested` 可以 `prevent_exit()` 再异步清理,只能在主线程上等清理做完。 +/// 清理放到异步运行时的线程上跑、主线程限时等:万一里面有步骤要等主线程,超时后照常退出, +/// 不会把进程卡住。 +fn cleanup_before_system_exit(app_handle: &tauri::AppHandle) { + log::info!("系统终止应用,开始退出清理..."); + let handle = app_handle.clone(); + let task = tauri::async_runtime::spawn(async move { cleanup_before_exit(&handle).await }); + // timeout 要在运行时里构造(它取当前运行时的计时器),所以包一层 async。 + let finished = tauri::async_runtime::block_on(async move { + tokio::time::timeout(SYSTEM_EXIT_CLEANUP_TIMEOUT, task).await + }); + if finished.is_err() { + log::warn!( + "退出清理 {} 秒内没做完,直接退出", + SYSTEM_EXIT_CLEANUP_TIMEOUT.as_secs() + ); + } +} + /// 主动从系统托盘移除托盘图标。 /// /// `std::process::exit` 会绕过 Tauri 运行时,触发不了 `TrayIcon::drop()`, @@ -2170,6 +2204,9 @@ enum ExitRequestAction { CleanupAndExit, } +/// 收到过重启请求。重启时 Tauri 也会发 `RunEvent::Exit`,靠它跳过系统终止那条清理。 +static RESTART_REQUESTED: AtomicBool = AtomicBool::new(false); + fn classify_exit_request(code: Option) -> ExitRequestAction { match code { None => ExitRequestAction::StayInTray, diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 82ef2b1fe..f2fd33a50 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -1916,7 +1916,8 @@ fn build_menu_model( )); menu.push(TrayEntry::Separator); menu.push(TrayEntry::item("open_website", texts.open_website)); - // 不能换成系统自带的 quit:它会跳过 app.exit(0) 的清理(客户端指回直连、停服务)。 + // 不换成系统自带的 quit:它不经过 app.exit(0) 的退出流程,客户端指回直连、停服务只能在 + // RunEvent::Exit 里限时补做(见 lib.rs 的 cleanup_before_system_exit)。 // 有应用在用路由服务时把这个后果写在「退出」上(后果不进说明,写在按钮文字里)。 menu.push(TrayEntry::item( "quit",