Skip to content

fix(installer): 安装器先请求运行中的实例正常退出,修复覆盖安装被占用 (#2460) - #2558

Closed
yhan1203 wants to merge 1 commit into
BigPizzaV3:mainfrom
yhan1203:fix/installer-quit-for-install
Closed

yhan1203 wants to merge 1 commit into
BigPizzaV3:mainfrom
yhan1203:fix/installer-quit-for-install

Conversation

@yhan1203

@yhan1203 yhan1203 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2460

问题

Windows 应用内更新时,安装器等不到旧管理工具退出,弹「旧管理工具仍被占用。请退出 Codex++ 后重新安装」,报告里连续 4 次重试全部失败,版本停在 1.7.1。

主程序退出时要恢复系统浏览器并清理后台服务,安装器不能强制结束进程——现有 CodexPlusPlus.nsi 明确不使用 taskkill,注释里也写了这个约束。所以 10 秒等待超时之后,安装器只能提示用户自己去托盘退出再重试,没有任何自动补救。

改动

把「提示用户手动退出」改成「先自己请求退出」,复用仓库里已有的单实例请求文件机制:

  1. 新增 --quit-for-install。安装器用它拉起第二个进程,该进程只写一个退出请求文件就返回,不启动界面。
  2. 运行中的实例在已有的 activation 监听线程里发现该请求后,走与托盘「退出程序」完全相同的路径:APP_EXITING + app.exit(0)。因此 RunEvent::ExitRequested 里的 auto_takeover::stop_supervisor() 与 codex_plus_launcher::shutdown() 照常执行,清理不是被跳过的。
  3. NSI 在 10 秒等待超时后先 Exec 这个请求,再给 30 秒清理时间;仍不退出才回退到原来的手动提示。wait_retry / wait_cancel 分支未改动。

边界

  • 全新安装:安装目录还没有主程序,没有可请求的实例,IfFileExists 直接跳到手动提示,不会去请求一个不存在的进程。
  • 不等待 Exec 返回:如果被请求的实例恰好在这时自行退出,被拉起的进程不会阻塞安装器。
  • 旧安装目标:不认识这个参数的旧版本,被拉起的第二个进程会被单实例守卫直接结束且不会打开界面,行为退化成今天的样子,不会更糟。
  • 请求文件带端口后缀(install-quit-{manager_guard_port}.request),与已有的 activation 请求文件同一套命名,不会跨实例串台。
  • 复用 write_manager_request 统一两处请求文件的写入,避免同一逻辑写两遍。

本地验证

按 CONTRIBUTING 的检查项跑的:

  • cargo test --all-features --no-fail-fast -p codex-plus-core -p codex-plus-manager → 所有测试二进制通过。唯一失败是 dictation::tests::dictation_bridge_cancel_drops_delayed_upstream_connection:去掉本 PR 的改动后在基线上同样失败,与本次改动无关。
  • windows_installer_waits_for_normal_exit_and_cancels_without_writing_apps(--test installers,14 passed)补了四条守卫:--quit-for-install 的 Exec 行、IfFileExists 前置判断、30 秒清理轮询(IntCmp $2 60)、以及回退提示所在的 wait_manual_prompt 标签。
  • cargo test -p codex-plus-manager --bin codex-plus-plus → 2 passed,含新增的 installer_quit_flag_only_matches_its_own_argument:--autostart、--show-update、--quit 都不会误触发退出。
  • cargo clippy -p codex-plus-manager -p codex-plus-core --all-targets:本 PR 的两个 Rust 文件没有新增告警。src-tauri/src/main.rs 无告警;src-tauri/src/lib.rs 的 7 条告警行(250 / 340 / 345 / 380 / 383 / 424 / 437)全部落在本 PR 未触碰的代码里。tests/installers.rs:7 的 PathBuf 未使用告警与当前 main 上完全相同,是 macOS-only 辅助函数在 Windows 上未使用所致。仓库整体存在既有 clippy 与 rustfmt 漂移,本 PR 不改变这一点,改动范围内保持干净。
  • git diff --check 干净。

需要说明的验证边界:以上是安装器脚本守卫测试、静态检查与代码路径核对,我没有在真机上跑一次 1.7.1 → 1.7.5 的应用内覆盖安装。真实安装器由 CI 的 Windows artifacts 构建产出,NSIS 打包会在那里被实际执行。

这个补丁最初是我在自己 fork 上为 1.7.7 打的,现在按当前 main 重新落地;main() 顶部与新增的 --codex-plus-multi-worker 分发做了合并,两个参数互不影响。

Windows 应用内更新时,安装器等不到旧管理工具退出就弹「旧管理工具仍被占用,请退出
Codex++ 后重新安装」,用户只能自己去托盘退出再重试,4 次重试全部失败(BigPizzaV3#2460)。
主程序退出时要恢复系统浏览器并清理后台服务,安装器不能强制结束进程,所以现有
NSI 明确不使用 taskkill,只能提示用户手动退出。

这里把「提示用户手动退出」改成「先自己请求退出」,复用已有的单实例请求文件机制:

- 新增 `--quit-for-install`:安装器用它拉起第二个进程,该进程只写一个退出请求文件
  就返回,不打开界面;
- 运行中的实例在已有的 activation 监听线程里发现请求后,走与托盘「退出程序」完全
  相同的路径(`APP_EXITING` + `app.exit(0)`),因此 `RunEvent::ExitRequested` 里的
  `stop_supervisor()` 与 `launcher::shutdown()` 清理照常执行;
- NSI 在 10 秒等待超时后先 `Exec` 该请求,再给 30 秒清理时间;仍不退出才回退到原来
  的手动提示,`wait_retry` / `wait_cancel` 分支保持不变;
- 全新安装时安装目录还没有主程序,`IfFileExists` 直接跳到手动提示,不会去请求一个
  不存在的实例;
- 用 `Exec` 而不等待返回:若被请求的实例恰好在这时退出,新进程不会阻塞安装器。

旧版本不认识这个参数,被拉起的第二个进程会被单实例守卫直接结束且不会打开界面,
因此对旧安装目标只是退化成今天的行为,不会更糟。

测试:

- `cargo test -p codex-plus-core --test installers` 14 passed,其中
  `windows_installer_waits_for_normal_exit_and_cancels_without_writing_apps` 补了
  `--quit-for-install`、`IfFileExists` 前置判断、30 秒清理轮询与手动提示标签的守卫;
- `--quit-for-install` 只匹配自己的参数,`--autostart`、`--show-update`、`--quit`
  都不会误触发。
@yhan1203

Copy link
Copy Markdown
Contributor Author

先关闭这个 PR。这条改动目前只完成到代码级核对:退出路径与托盘「退出程序」走同一条 \�pp.exit(0)\ → \RunEvent::ExitRequested\ → \prevent_exit()\ 链路,请求文件也沿用了 \window-activation-{port}.request\ 的现有约定。但覆盖安装的实际效果必须在真实 Windows 实例上跑一次安装器才能确认,这个端到端验证我目前做不了,不想让一条没验证完的改动占着 review。等补上安装器的实测记录再重新提。分支 \ ix/installer-quit-for-install\ 先保留,随时可以恢复。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant