Skip to content

fix(single-instance): 修复 Windows 重复启动会开出第二个窗口 - #239

Merged
feigeCode merged 3 commits into
feigeCode:mainfrom
paofu-cium:fix/windows-single-instance-forwarding
Sep 19, 2026
Merged

feigeCode merged 3 commits into
feigeCode:mainfrom
paofu-cium:fix/windows-single-instance-forwarding

Conversation

@paofu-cium

Copy link
Copy Markdown
Contributor

Windows 单实例此前有三层叠加缺陷,任何一层单独存在都足以让「双击两次」得到两个 GUI:

  1. 占用判定用错了错误码。claim_or_forward 按 io::ErrorKind::AddrInUse 判断「名称已被 占用」,但 interprocess 首次建实例带 FILE_FLAG_FIRST_PIPE_INSTANCE,名称冲突时 OS 返回 ERROR_ACCESS_DENIED(5),Rust std 把它映射为 PermissionDenied;而全 crate 唯一 的 AddrInUse 处理在 Unix UDS 路径上。于是这个分支在 Windows 上永远不会命中,转发 逻辑一次都没有被执行过。
  2. 转发链路在 Windows 上不可用。接收端与发送端都调用 interprocess 的 set_recv_timeout / set_send_timeout,而它在 Windows 命名管道上恒返回 ErrorKind::Unsupported ("named pipes do not support I/O timeouts")。这是被第 1 条挡住的潜伏缺陷,第 1 条 一修就会立刻暴露出来。
  3. 启动门禁把「建不了单实例」解释成「可以再开一个」:main.rs 只有转发成功才 return, Err 分支只记一条 warn 就继续初始化 GUI。

本次按 tokio 命名管道重写 IPC 层,并收紧启动门禁与 ACK 语义:

  • 互斥只由 ServerOptions::first_pipe_instance(true) 提供,且只给首个实例;服务循环补建 的实例一律不带该标志,否则会把自己也挡在门外。
  • 占用与连接错误全部按原始 Win32 错误码分类:ERROR_ACCESS_DENIED(5) 表示名称已被占用; 客户端可重试码为 2/5/231(231 在 std 里落到 Uncategorized,按 kind 判会漏掉)。
  • 所有 I/O 用 tokio::time::timeout 加时间边界,每条连接独立 task:一个半开连接不会拖住 后续启动请求。接受循环先补建下一个实例、再移交当前连接,名称不会出现无主窗口;循环 异常退出时保持名称被占用并停驻(fail-closed)——宁可让后续启动转发失败后明确报错退出, 也不允许第二个 GUI 进来。
  • ACK 收紧为「启动请求已入队」:入队失败回 [0],不再假成功。
  • 门禁区分「转发失败」与「监听失败」并给出各自的提示文案,Err 一律提示后 exit(1)。 release 是 windows_subsystem = "windows"、没有控制台,用户看不到 eprintln,提示走 MessageBoxW。
  • 顺带修掉一个空转断言:windows_single_instance_gate_precedes_application_creation 里 assert!(!source.contains("continuing startup")) 的字面量本身就在 include_str! 的范围内, 会自己满足自己。现在断言只看 production_source()(截掉测试模块的生产代码),并补上 exit(1) 与提示函数的断言。

依赖侧:main 不再使用 interprocess,从 main 的依赖表中摘除(工作区声明保留,因为 extension-driver 与 extension-host 仍在使用,Cargo.lock 相应少一行)。tokio 增加 io-util / net 两个 feature,二者已由 public_mcp、sftp 启用,不引入重编。

验证:

  • cargo test --offline:9 passed。测试放在一个独立 crate 里,用 #[path] 直接引用 main/src/windows_single_instance.rs 本体(不是副本),依赖按 Cargo.lock 精确版本锁定。 其中 5 个是真实 Windows 命名管道传输测试:转发成功且主实例确实收到请求、入队失败时 ACK 必须为拒绝、半个请求超时断开且不阻塞后续请求、4 线程并发抢占只有 1 个取得主实例 资格、不同配置目录各自独立。
  • cargo clippy --offline --all-targets -- -D warnings:0 warning。
  • rustfmt --edition 2024:windows_single_instance.rs 无格式差异,main.rs 新增片段无差异。
  • 原始故障的真机探针复现:第 2 次 create_sync 返回 PermissionDenied(raw 5)、 kind() == AddrInUse 命中为 false、set_recv_timeout 返回 Unsupported。
  • 未验证:main crate 本体的编译,以及真机双击验收(第二次启动只唤起已有窗口)。原因: 依赖图含 git 依赖 quickjs-jit,其 rev 需联网获取,而本地构建缓存被清空后正在做全量 编译,尚未跑完;托管 CI 在 Windows 平台会编译 main 本体,可作为该缺口的补充验证。

Closes #[issue number]

Description

Describe in English for the changes made in this pull request and the problem it solves.
Please keep 1 PR to solve 1 problem, and keep Small improvements should be small modifications to make PR easier to review and to merge.

Screenshot

Before After
[Put Before Screenshot here] [Put After Screenshot here]

Break Changes

Describe any breaking changes introduced by this pull request. If none, remove this section.

  • Change 1
- Old code snippet
+ New code snippet

How to Test

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

Windows 单实例此前有三层叠加缺陷,任何一层单独存在都足以让「双击两次」得到两个 GUI:

1. 占用判定用错了错误码。claim_or_forward 按 io::ErrorKind::AddrInUse 判断「名称已被
   占用」,但 interprocess 首次建实例带 FILE_FLAG_FIRST_PIPE_INSTANCE,名称冲突时 OS
   返回 ERROR_ACCESS_DENIED(5),Rust std 把它映射为 PermissionDenied;而全 crate 唯一
   的 AddrInUse 处理在 Unix UDS 路径上。于是这个分支在 Windows 上永远不会命中,转发
   逻辑一次都没有被执行过。
2. 转发链路在 Windows 上不可用。接收端与发送端都调用 interprocess 的 set_recv_timeout /
   set_send_timeout,而它在 Windows 命名管道上恒返回 ErrorKind::Unsupported
   ("named pipes do not support I/O timeouts")。这是被第 1 条挡住的潜伏缺陷,第 1 条
   一修就会立刻暴露出来。
3. 启动门禁把「建不了单实例」解释成「可以再开一个」:main.rs 只有转发成功才 return,
   Err 分支只记一条 warn 就继续初始化 GUI。

本次按 tokio 命名管道重写 IPC 层,并收紧启动门禁与 ACK 语义:

- 互斥只由 ServerOptions::first_pipe_instance(true) 提供,且只给首个实例;服务循环补建
  的实例一律不带该标志,否则会把自己也挡在门外。
- 占用与连接错误全部按原始 Win32 错误码分类:ERROR_ACCESS_DENIED(5) 表示名称已被占用;
  客户端可重试码为 2/5/231(231 在 std 里落到 Uncategorized,按 kind 判会漏掉)。
- 所有 I/O 用 tokio::time::timeout 加时间边界,每条连接独立 task:一个半开连接不会拖住
  后续启动请求。接受循环先补建下一个实例、再移交当前连接,名称不会出现无主窗口;循环
  异常退出时保持名称被占用并停驻(fail-closed)——宁可让后续启动转发失败后明确报错退出,
  也不允许第二个 GUI 进来。
- ACK 收紧为「启动请求已入队」:入队失败回 [0],不再假成功。
- 门禁区分「转发失败」与「监听失败」并给出各自的提示文案,Err 一律提示后 exit(1)。
  release 是 windows_subsystem = "windows"、没有控制台,用户看不到 eprintln,提示走
  MessageBoxW。
- 顺带修掉一个空转断言:windows_single_instance_gate_precedes_application_creation 里
  assert!(!source.contains("continuing startup")) 的字面量本身就在 include_str! 的范围内,
  会自己满足自己。现在断言只看 production_source()(截掉测试模块的生产代码),并补上
  exit(1) 与提示函数的断言。

依赖侧:main 不再使用 interprocess,从 main 的依赖表中摘除(工作区声明保留,因为
extension-driver 与 extension-host 仍在使用,Cargo.lock 相应少一行)。tokio 增加
io-util / net 两个 feature,二者已由 public_mcp、sftp 启用,不引入重编。

验证:
- cargo test --offline:9 passed。测试放在一个独立 crate 里,用 #[path] 直接引用
  main/src/windows_single_instance.rs 本体(不是副本),依赖按 Cargo.lock 精确版本锁定。
  其中 5 个是真实 Windows 命名管道传输测试:转发成功且主实例确实收到请求、入队失败时
  ACK 必须为拒绝、半个请求超时断开且不阻塞后续请求、4 线程并发抢占只有 1 个取得主实例
  资格、不同配置目录各自独立。
- cargo clippy --offline --all-targets -- -D warnings:0 warning。
- rustfmt --edition 2024:windows_single_instance.rs 无格式差异,main.rs 新增片段无差异。
- 原始故障的真机探针复现:第 2 次 create_sync 返回 PermissionDenied(raw 5)、
  kind() == AddrInUse 命中为 false、set_recv_timeout 返回 Unsupported。
- 未验证:main crate 本体的编译,以及真机双击验收(第二次启动只唤起已有窗口)。原因:
  依赖图含 git 依赖 quickjs-jit,其 rev 需联网获取,而本地构建缓存被清空后正在做全量
  编译,尚未跑完;托管 CI 在 Windows 平台会编译 main 本体,可作为该缺口的补充验证。
@feigeCode feigeCode closed this Sep 19, 2026
@feigeCode feigeCode reopened this Sep 19, 2026
feigeCode and others added 2 commits September 19, 2026 23:04
解决与 PR feigeCode#228(feat(tray): 关闭按钮询问「最小化到托盘 / 退出应用」,并修复
Windows 重复启动)的冲突,让本 PR 重新可合。

- main/src/windows_single_instance.rs:采用 PR 侧重写(SingleInstanceError 枚举、
  worker + 决策通道、Claim/ForwardAttempt、create_pipe_instance)。main 侧
  bd34cc4 新增的 instance_name_taken 语义已由 PR 的 is_name_in_use(Windows 专用,
  认 ERROR_ACCESS_DENIED=5)与 is_connect_retryable(认 2/231/5)覆盖;其被自动合并
  保留在非冲突区的定义一并删除,否则留下 3 个 dead_code 警告
- Cargo.lock:按合并后的 Cargo.toml 由 cargo 同步(保留 main 侧新增依赖,
  去掉 PR 已移除的 interprocess)
- main/src/main.rs、main/Cargo.toml:自动合并无冲突

验证:cargo check -p main --all-targets(0 警告)、cargo test -p main --bin navop
(634 passed, 0 failed)。Windows 侧属 CI 覆盖范围(本机无 MSVC 工具链,跨编译
在 libz-sys 处止步)。
@feigeCode
feigeCode merged commit 984b009 into feigeCode:main Sep 19, 2026
7 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