fix(updater): clear tray icon on the direct-restart update path

restart_process re-execs via tauri::process::restart (spawn + exit(0)),
which skips Tauri's internal cleanup_before_exit and all RunEvent::Exit
plugin hooks. Window state, proxy/live restore and the single-instance
lock were already compensated explicitly; the tray icon was not.

On macOS/Linux the OS drops the status item when the process dies, so
the gap there was cosmetic at most. The real residue risk is the
Windows branch, which never reaches restart_process at all:
update.install() exits the process inside the updater plugin
(std::process::exit(0)), bypassing TrayIcon::drop — no NIM_DELETE is
sent and a stale icon lingers in the shell until hovered, the same
failure remove_tray_icon_before_exit was originally added for on the
quit path.

Call remove_tray_icon_before_exit (set_visible(false), proxied to the
main thread via run_item_main_thread) in restart_process and before
the Windows install. Deliberately not AppHandle::cleanup_before_exit():
it drops tray icons on the calling thread, which is not safe off the
main thread on macOS (NSStatusItem).
This commit is contained in:
Jason
2026-06-11 19:55:48 +08:00
Unverified
parent 7d0eacac33
commit 7e1c85feb6
2 changed files with 17 additions and 4 deletions
+5 -2
View File
@@ -115,10 +115,13 @@ pub async fn install_update_and_restart(app: AppHandle) -> Result<bool, String>
#[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| {
+12 -2
View File
@@ -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());
}