diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 8d16ef5fa..7b5b21d95 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -115,10 +115,13 @@ pub async fn install_update_and_restart(app: AppHandle) -> Result #[cfg(target_os = "windows")] { - // Windows updater 会在 install() 内启动安装器并直接退出当前进程。 - // 因此清理只能放在 install 前执行。 + // Windows updater 会在 install() 内启动安装器并直接退出当前进程 + // (插件内部 std::process::exit(0),绕过 TrayIcon::drop、不发 + // NIM_DELETE,会残留死图标——与托盘"退出"路径相同的问题)。 + // 因此清理只能放在 install 前执行,且必须显式移除托盘图标。 crate::save_window_state_before_exit(&app); crate::cleanup_before_exit(&app).await; + crate::remove_tray_icon_before_exit(&app); crate::destroy_single_instance_lock(&app); tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; update.install(bytes).map_err(|e| { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index aad7f40a4..eeebed147 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1646,7 +1646,7 @@ pub async fn cleanup_before_exit(app_handle: &tauri::AppHandle) { /// 触发 tray-icon 内部的 `remove_tray_icon` → `Shell_NotifyIconW(NIM_DELETE)`, /// 在进程结束前干净地把图标摘掉。其它平台 `set_visible(false)` 也是 /// 正常的隐藏/移除语义,作为跨平台兜底也安全。 -fn remove_tray_icon_before_exit(app_handle: &tauri::AppHandle) { +pub(crate) fn remove_tray_icon_before_exit(app_handle: &tauri::AppHandle) { if let Some(tray) = app_handle.tray_by_id(tray::TRAY_ID) { if let Err(e) = tray.set_visible(false) { log::warn!("退出时移除托盘图标失败: {e}"); @@ -1973,8 +1973,18 @@ pub fn destroy_single_instance_lock(app_handle: &tauri::AppHandle) { tauri_plugin_single_instance::destroy(app_handle); } -/// 释放 single-instance 锁后重启当前应用。 +/// 清理托盘图标、释放 single-instance 锁后重启当前应用。 +/// +/// 直接走 `tauri::process::restart`(spawn 新进程 + `exit(0)`),不经过事件 +/// 循环退出,因此 Tauri 内部的 `cleanup_before_exit` 和各插件的 +/// `RunEvent::Exit` 钩子都不会执行。需要的清理由调用方与本函数显式补偿: +/// 窗口状态、代理/Live 恢复(调用方);托盘图标、single-instance 锁(本函数)。 +/// +/// 有意不调 `AppHandle::cleanup_before_exit()`:它会在调用线程上 Drop 托盘 +/// 图标,而 macOS 的 NSStatusItem 操作要求主线程;`set_visible(false)` 走 +/// `run_item_main_thread` 代理,跨线程安全(见 `remove_tray_icon_before_exit`)。 pub fn restart_process(app_handle: &tauri::AppHandle) -> ! { + remove_tray_icon_before_exit(app_handle); destroy_single_instance_lock(app_handle); tauri::process::restart(&app_handle.env()); }