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.
This commit is contained in:
Jason committed 2026-10-03 23:29:00 +08:00
1 parent 31e5ae3f6a
commit 4aaafb8f41
2 files changed
+40 -2

No files matched your search

+38 -1
View File
@@ -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<i32>) -> ExitRequestAction {
match code {
None => ExitRequestAction::StayInTray,
+2 -1
View File
@@ -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",