修复 Windows 托盘恢复卡死#288
Conversation
KilimiaoSix
left a comment
There was a problem hiding this comment.
本轮 code review 结论:暂不建议合并,建议先修复后再重新 review。
主要原因如下:
-
恢复失败后可能导致应用无法正常退出
在
apps/src-tauri/src/app_shell/window.rs中,KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE只在window.show()成功后才会被清除。若 lightweight close 后恢复主窗口时ensure_main_window()或window.show()失败,该 flag 会继续保持为true。后续
RunEvent::ExitRequested会因为should_keep_alive_for_lightweight_close()返回 true 而prevent_exit(),可能导致应用进入无主窗口但仍无法正常退出的状态。这是我认为当前最需要先修复的阻塞问题。 -
app_show_main_window的成功返回语义不再可靠app_show_main_window现在调用request_show_main_window()后立即返回Ok(()),但实际恢复窗口的动作是在 detached thread 中 sleep 50ms 后再投递到主线程执行。这意味着前端拿到
Ok(())时,窗口可能尚未显示,甚至后续可能因为run_on_main_thread、窗口创建、show()或focus失败而没有恢复。调用方无法感知失败,也无法重试或提示用户。 -
延迟恢复任务缺少退出态保护
request_show_main_window()延迟执行前后没有检查应用是否已经进入退出流程。若用户发起显示主窗口后立刻从托盘退出,延迟任务仍可能在退出过程中尝试创建、显示或聚焦主窗口,带来 shutdown race 和偶发异常。 -
每次恢复请求都会创建新的 sleeping OS thread,且没有合并重复请求
托盘菜单、single-instance、macOS Reopen、前端命令都会走
request_show_main_window()。当前每次调用都会std::thread::spawn一个线程并 sleep 50ms。连续点击托盘、重复启动应用等场景会产生多个重复的恢复任务,放大窗口生命周期竞态。 -
移除二次启动弹窗后,恢复失败没有用户可见 fallback
旧逻辑至少会通过弹窗提示用户应用已在运行。新逻辑只记录日志。如果异步恢复失败或 OS 拒绝前台聚焦,用户可能看到二次启动“无反应”。移除阻塞弹窗方向可以理解,但建议补充非阻塞的失败处理或更可靠的恢复结果处理。
建议修复方向:
- 恢复流程开始或失败时明确收敛
KEEP_ALIVE_FOR_LIGHTWEIGHT_CLOSE状态,避免失败后卡在 keep-alive。 request_show_main_window()在执行前检查APP_EXIT_REQUESTED,退出中则跳过。- 为恢复请求增加 pending/coalescing 机制,避免重复线程和重复 show/focus。
- 明确
app_show_main_window的语义:如果只是“恢复请求已提交”,前端和命名应体现这一点;如果语义是“窗口已显示”,则需要返回真实执行结果。 - 尽量避免固定
50ms sleep作为核心同步机制,或者至少用注释说明该延迟的必要性和边界。
因此本轮建议 Request changes,暂缓合并。
变更摘要
app_show_main_window命令统一走异步恢复入口,避免在 single-instance 回调中同步创建/显示窗口导致回调卡住。改动范围
主要文件
apps/src-tauri/src/app_shell/window.rsapps/src-tauri/src/app_shell/lifecycle.rsapps/src-tauri/src/app_shell/tray.rsapps/src-tauri/src/lib.rsapps/src-tauri/src/commands/system.rsapps/src-tauri/src/app_shell/mod.rs验证
pnpm -C apps run testpnpm -C apps run buildpnpm -C apps run test:uicargo test --workspace已执行的实际验证:
未执行的验证与原因:
风险与影响面
Reopen入口改为同一异步恢复入口,行为应保持为显示主窗口。备注