Skip to content

fix(update): 更新替换后等旧实例退出再拉起新版本 - #243

Merged
feigeCode merged 4 commits into
feigeCode:mainfrom
paofu-cium:fix/update-restart-wait-previous-instance
Sep 20, 2026
Merged

feigeCode merged 4 commits into
feigeCode:mainfrom
paofu-cium:fix/update-restart-wait-previous-instance

Conversation

@paofu-cium

Copy link
Copy Markdown
Contributor

Windows 允许重命名正在运行的 exe(实测 MoveFileW 返回 0),所以
replace_target_with_backup 的 rename + 拷贝新文件能在旧实例仍然存活时跑完
而原来的 let _ = remove_file_if_exists(&backup_path) 恰好丢掉了"旧进程是否已退出"
的唯一信号:helper 替换完立刻 restart_application(),新版本在旧实例仍持有 Windows
单实例管道时启动,就会走进"转发给已有实例"分支、拿到确认后立刻退出,用户看到的是
"更新完成后应用不再出现"。

运行中的 exe 无法被删除(实测 DeleteFileW 返回 ERROR_ACCESS_DENIED(5),进程
退出后转为成功),因此"备份文件变成可删除"就是旧进程已退出的可靠探针。新增
wait_for_previous_instance_exit():有界等待(30s / 200ms 轮询)后再拉起新版本,
超时只记录并继续,避免旧进程异常不退时把更新卡在替换之后;首次安装(无备份)
不等待。

"仍被占用"按原始错误码判定(file_is_still_in_use()),不能用 ErrorKind
std 把 5 映射成 PermissionDenied,却把 32 映射成 Uncategorized,按 kind 判会漏掉
ERROR_SHARING_VIOLATION。这与单实例模块按原始码分类的理由一致。

这条链路是单实例门禁收紧后才暴露的:在此之前转发恒失败、门禁放行,新版本反而
"能起来",掩盖了旧实例与新版本并存的时序问题。

验证:

  • cargo test -p main --bin navop --config profile.dev.package.main.debug=0:585 passed /
    1 failed,测试本体 3.33s。唯一失败是既有
    home_tab::tests::rendering::recent_section_does_not_participate_in_search(纯源文本契约,
    输入 main/src/home_tab/content.rs 未被本提交触碰;它 split 的函数实际在 home_layout.rs
    上游已改断言),本分支基点即为红,与本改动无关。本提交新增的
    restart_waits_until_the_previous_instance_releases_the_executable 通过。
  • 独立探针 .workbuddy/tmp/share-mode-probe/(std-only 独立 crate,与产品逻辑同构,
    秒级编译):3/3 PASS —— 备份被占用时消耗完整个 timeout 且不删除(410ms/400ms)、
    释放后 819µs 内放行并清理备份、无旧文件时不等待(132µs);并复现 std 的错误码映射
    raw 5 -> PermissionDenied / raw 32 -> Uncategorized(按 ErrorKind 判会漏掉 32)。
  • 原始文件语义探针 .workbuddy/tmp/update_race_probe_raw.py(裸 MoveFileW /
    DeleteFileW):运行中的 exe 可重命名(winerror=0)、不可删除
    ERROR_ACCESS_DENIED=5)、进程退出后删除成功。
  • 未验证:真机跑一次完整更新安装(需要一个真实 release 包替换正在运行的 exe,
    无法在不干扰用户真实安装的前提下构造)。

paofu-cium and others added 4 commits September 20, 2026 10:43
Windows 允许重命名正在运行的 exe(实测 `MoveFileW` 返回 0),所以
`replace_target_with_backup` 的 rename + 拷贝新文件**能在旧实例仍然存活时跑完**,
而原来的 `let _ = remove_file_if_exists(&backup_path)` 恰好丢掉了"旧进程是否已退出"
的唯一信号:helper 替换完立刻 `restart_application()`,新版本在旧实例仍持有 Windows
单实例管道时启动,就会走进"转发给已有实例"分支、拿到确认后立刻退出,用户看到的是
"更新完成后应用不再出现"。

运行中的 exe 无法被删除(实测 `DeleteFileW` 返回 `ERROR_ACCESS_DENIED(5)`,进程
退出后转为成功),因此"备份文件变成可删除"就是旧进程已退出的可靠探针。新增
`wait_for_previous_instance_exit()`:有界等待(30s / 200ms 轮询)后再拉起新版本,
超时只记录并继续,避免旧进程异常不退时把更新卡在替换之后;首次安装(无备份)
不等待。

"仍被占用"按**原始错误码**判定(`file_is_still_in_use()`),不能用 `ErrorKind`:
std 把 5 映射成 `PermissionDenied`,却把 32 映射成 `Uncategorized`,按 kind 判会漏掉
`ERROR_SHARING_VIOLATION`。这与单实例模块按原始码分类的理由一致。

这条链路是单实例门禁收紧后才暴露的:在此之前转发恒失败、门禁放行,新版本反而
"能起来",掩盖了旧实例与新版本并存的时序问题。

验证:
- `cargo test -p main --bin navop --config profile.dev.package.main.debug=0`:585 passed /
  1 failed,测试本体 3.33s。唯一失败是**既有**的
  `home_tab::tests::rendering::recent_section_does_not_participate_in_search`(纯源文本契约,
  输入 `main/src/home_tab/content.rs` 未被本提交触碰;它 split 的函数实际在 `home_layout.rs`,
  上游已改断言),本分支基点即为红,与本改动无关。本提交新增的
  `restart_waits_until_the_previous_instance_releases_the_executable` 通过。
- 独立探针 `.workbuddy/tmp/share-mode-probe/`(std-only 独立 crate,与产品逻辑同构,
  秒级编译):3/3 PASS —— 备份被占用时消耗完整个 timeout 且不删除(410ms/400ms)、
  释放后 819µs 内放行并清理备份、无旧文件时不等待(132µs);并复现 std 的错误码映射
  `raw 5 -> PermissionDenied` / `raw 32 -> Uncategorized`(按 `ErrorKind` 判会漏掉 32)。
- 原始文件语义探针 `.workbuddy/tmp/update_race_probe_raw.py`(裸 `MoveFileW` /
  `DeleteFileW`):运行中的 exe 可重命名(winerror=0)、不可删除
  (`ERROR_ACCESS_DENIED=5`)、进程退出后删除成功。
- 未验证:真机跑一次完整更新安装(需要一个真实 release 包替换正在运行的 exe,
  无法在不干扰用户真实安装的前提下构造)。
超时(10s,原 30s)或探测失败统一视为「无法确认旧实例已退出」,不再拉起新版本:新版本在旧实例仍占着单实例管道时启动,要么转发后自己退出(用户看不到任何窗口),要么弹「启动请求没能交给它」。改为弹系统对话框提示用户结束 Navop 进程后手动启动——更新 helper 没有窗口,release 又是 windows_subsystem = windows,eprintln 用户看不到。
@feigeCode
feigeCode merged commit 9cb13ae into feigeCode:main Sep 20, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants