npm run agc 按 Ctrl+C 会残留上个 worktree 的后端,切换工作树时复用旧后端 #314

Closed
opened 2026-09-09 19:08:30 +08:00 by suzmii · 0 comments
Member

现象

npm run agc 在终端按 Ctrl+C 后,有概率上个工作树的 api-server.exe / SpacetimeDB 仍在监听 8082 / 8083 / 3101。切到另一个 worktree 再启动 AGC 时,前端仍然连到上个工作树的后端;在改过数据库 / schema 的工作树上会串库,风险很高。

根因(三层叠加)

  1. 清理锚点错了:Windows 下所有长驻服务都经 Node shell: truecmd.exe /d /s /c 包装层启动。Ctrl+C 会先杀掉包装层(退出码 0xC000013A),而 scripts/dev.mjsstopProcess() 见到直接子进程已退出就直接 returnstart-dev-stack.mjs / start-tauri-dev.mjs 对已退出 PID 执行 taskkill /PID <pid> /T /F 只会失败(stopped: false)。更深的 cargo → api-server.exe 没有任何人收。
  2. 按 PID 遍历会断链:遍历依赖进程快照里的父子链。当中间层(包装层)先从快照消失时,链路断开,遍历只能拿到根 PID,深处的后端不可达。实测:cmd(10) → wrapper(20) → api-server(30),快照只剩 30 时,从 10 遍历结果只有 [10]
  3. 复用判据只看端口健康isBackendReady() 只用 .app/dev-stack.json 的 status 加 /healthz/readyz/v1/ping,从不校验端口上的进程属于哪个工作树。残留后端照样“健康”,于是被当前工作树当成自己的后端复用。

复现步骤

  1. 在 worktree A 执行 npm run agc,等配套后端就绪;
  2. 终端按 Ctrl+C(概率性触发;Rust 侧 api-serverwith_graceful_shutdown 没有 deadline,长连接 / 流式请求在飞时更容易拖住退出);
  3. Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'api-server.exe' } 仍能看到 A 的 server-rs\target\debug\api-server.exe
  4. 切到 worktree B 执行 npm run agc,AGC 直接复用 A 的后端(B 的 Vite 代理指向 A 的 8082)。

修复方案(已在本地实现,待提交)

  • 新增 scripts/dev-windows-process.mjs:同时提供「按根 PID 遍历」与「按身份匹配」两条独立清理路径(server-rs/target/debug/api-server.exe 绝对路径、SpacetimeDB --data-dir),后者不依赖任何存活的包装层;
  • scripts/dev.mjs:直接子进程已退出时也继续清理;退出时按身份兜底清扫本工作树后端(复用他人 standalone 时不清理);
  • apps/ai-game-creator-shell/scripts/start-dev-stack.mjs:复用前校验端口监听进程归属,无法证明归属就不复用,改为启动本工作树自己的后端并允许端口漂移;收到信号与 finally 各兜底清扫一次本工作树 api-server.exetaskkill 失败时降级为按 PID 遍历;
  • 文档同步 docs/project-memory/shared-memory/pitfalls.mddocs/【开发运维】本地开发验证与生产运维-2026-05-15.md

验证

  • 伪造 api-server.exe 进程:按身份精确命中并杀掉(matched=[17284] stopped=[17284]);
  • 3 个真实监听进程下的归属判定:owned / api-server-owner-mismatch / spacetime-owner-mismatch 均正确;
  • npx vitest run:dev 栈相关套件 1066 passed(唯一失败为 HEAD 上已存在的 Windows 文件权限用例);
  • node --checkeslint --max-warnings 0prettier --checknpm run check:encodinggit diff --check 全部通过。

备注

Rust 侧 api-server 的优雅退出没有超时上限,是“有概率”的来源之一;本次只在 Node 侧收口。是否给 with_graceful_shutdown 加 deadline 可以另行评估。

## 现象 `npm run agc` 在终端按 Ctrl+C 后,有概率上个工作树的 `api-server.exe` / SpacetimeDB 仍在监听 `8082` / `8083` / `3101`。切到另一个 worktree 再启动 AGC 时,前端仍然连到**上个工作树的后端**;在改过数据库 / schema 的工作树上会串库,风险很高。 ## 根因(三层叠加) 1. **清理锚点错了**:Windows 下所有长驻服务都经 Node `shell: true` 的 `cmd.exe /d /s /c` 包装层启动。Ctrl+C 会先杀掉包装层(退出码 `0xC000013A`),而 `scripts/dev.mjs` 的 `stopProcess()` 见到直接子进程已退出就直接 `return`;`start-dev-stack.mjs` / `start-tauri-dev.mjs` 对已退出 PID 执行 `taskkill /PID <pid> /T /F` 只会失败(`stopped: false`)。更深的 `cargo → api-server.exe` 没有任何人收。 2. **按 PID 遍历会断链**:遍历依赖进程快照里的父子链。当中间层(包装层)先从快照消失时,链路断开,遍历只能拿到根 PID,深处的后端不可达。实测:`cmd(10) → wrapper(20) → api-server(30)`,快照只剩 30 时,从 10 遍历结果只有 `[10]`。 3. **复用判据只看端口健康**:`isBackendReady()` 只用 `.app/dev-stack.json` 的 status 加 `/healthz`、`/readyz`、`/v1/ping`,从不校验端口上的进程属于哪个工作树。残留后端照样“健康”,于是被当前工作树当成自己的后端复用。 ## 复现步骤 1. 在 worktree A 执行 `npm run agc`,等配套后端就绪; 2. 终端按 Ctrl+C(概率性触发;Rust 侧 `api-server` 的 `with_graceful_shutdown` 没有 deadline,长连接 / 流式请求在飞时更容易拖住退出); 3. `Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'api-server.exe' }` 仍能看到 A 的 `server-rs\target\debug\api-server.exe`; 4. 切到 worktree B 执行 `npm run agc`,AGC 直接复用 A 的后端(B 的 Vite 代理指向 A 的 `8082`)。 ## 修复方案(已在本地实现,待提交) - 新增 `scripts/dev-windows-process.mjs`:同时提供「按根 PID 遍历」与「按身份匹配」两条独立清理路径(`server-rs/target/debug/api-server.exe` 绝对路径、SpacetimeDB `--data-dir`),后者不依赖任何存活的包装层; - `scripts/dev.mjs`:直接子进程已退出时也继续清理;退出时按身份兜底清扫本工作树后端(复用他人 standalone 时不清理); - `apps/ai-game-creator-shell/scripts/start-dev-stack.mjs`:复用前校验端口监听进程归属,无法证明归属就不复用,改为启动本工作树自己的后端并允许端口漂移;收到信号与 `finally` 各兜底清扫一次本工作树 `api-server.exe`;`taskkill` 失败时降级为按 PID 遍历; - 文档同步 `docs/project-memory/shared-memory/pitfalls.md` 与 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 ## 验证 - 伪造 `api-server.exe` 进程:按身份精确命中并杀掉(`matched=[17284] stopped=[17284]`); - 3 个真实监听进程下的归属判定:`owned` / `api-server-owner-mismatch` / `spacetime-owner-mismatch` 均正确; - `npx vitest run`:dev 栈相关套件 1066 passed(唯一失败为 HEAD 上已存在的 Windows 文件权限用例); - `node --check`、`eslint --max-warnings 0`、`prettier --check`、`npm run check:encoding`、`git diff --check` 全部通过。 ## 备注 Rust 侧 `api-server` 的优雅退出没有超时上限,是“有概率”的来源之一;本次只在 Node 侧收口。是否给 `with_graceful_shutdown` 加 deadline 可以另行评估。
suzmii added the
Priority
High
2
Kind/Bug
labels 2026-09-09 19:08:30 +08:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#314