bug(desktop): Windows 按需插件 Node worker 空闲回收后二次启动超时
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
### 问题描述 / What happened
Windows 正式版中,按需启动的插件 Node worker 在 Cindy 重启后的首次调用可以正常启动;空闲回收后再次调用时,宿主等待约 10 秒仍收不到 bootstrap `ready`,随后返回 `PROCESS_START_FAILED`。XD Sites 将其映射为 `XD_SITES_WORKER_START_FAILED`。
实际行为:
- 重启 Cindy 后,`sites_detect` / `sites_deploy` 首次调用成功。
- 距最后一次 Node 请求超过插件配置的 120 秒空闲时间后,再次调用需要 Node worker 的工具,约 10 秒后失败。
- 同一 Cindy 进程内继续重试仍失败;再次重启 Cindy 后首次调用恢复。
- 同一份日志中,另一个使用 Node worker 的插件 `etheria-sentiment-sync` 也出现相同的 10 秒启动超时。
期望行为:
- 按需 worker 空闲退出后,后续请求应能可靠地重新启动 worker。
- 如果旧进程尚未真实退出,应等待退出或返回明确的停止错误,不应让后续启动持续失败直至重启 Cindy。
### 环境 / Environment
- Cindy 版本或 commit / version or commit: 0.1.52、0.1.57、0.1.58 均观察到;两个完整重启周期分别在 0.1.57 和 0.1.58 复现
- 平台与版本 / platform & OS version: Windows x64;导出的 main 日志未记录具体 Windows 小版本
- 安装方式 / install method: 正式安装包并通过应用内更新,`isDev=false`
- 相关插件: XD Sites;同类现象也出现在 `etheria-sentiment-sync`
### 复现步骤 / Steps to reproduce
1. 在 Windows 正式版启用 XD Sites。
2. 重启 Cindy。
3. 调用 `sites_detect` 或 `sites_deploy`,确认 Node worker 首次启动成功。
4. 等最后一次 Node 请求完成后超过 120 秒,使 on-demand worker 进入空闲回收。
5. 再次调用 `sites_detect` 或 `sites_deploy`。
6. 观察调用约 10 秒后返回 `XD_SITES_WORKER_START_FAILED`;继续重试仍失败。
7. 重启 Cindy,再次调用,首次启动恢复。
导出的日志中两个重启周期均呈现该模式;尚未在独立 Windows 开发环境构造最小程序验证。
### 日志与截图 / Logs & screenshots
以下为脱敏后的最小日志摘要。完整日志未公开,避免包含用户路径、会话标识和站点信息。
```text
# Cindy 0.1.57,重启后首次成功
10:45:10.753 ghost node process started
ghostId: xd-sites
entry: node/worker.cjs
protocol: json-rpc-stdio
10:45:40.069 ghost tool call completed
ghostId: xd-sites
tool: sites_deploy
ok: true
# 空闲回收后的下一次启动
11:10:10.753 ghost node start attempt failed
ghostId: xd-sites
entry: node/worker.cjs
attempt: 1
message: Node 工作进程启动超时
11:10:10.754 ghost tool call completed
errorCode: XD_SITES_WORKER_START_FAILED
totalMs: 10071
11:10:10.755 child-process-gone
type=Utility reason=killed name=cindy-ghost-node:xd-sites
```
```text
# Cindy 0.1.58,第二次重启后同样先成功
11:28:32.555 ghost node process started
ghostId: xd-sites
entry: node/worker.cjs
11:29:03.255 sites_deploy ok=true
# 随后普通 Sites HTTP 查询仍为 200,但需要 Node worker 的部署再次失败
14:57:10.999 sites_info ok=true
14:57:31.008 ghost node start attempt failed
message: Node 工作进程启动超时
14:57:31.009 sites_deploy errorCode=XD_SITES_WORKER_START_FAILED totalMs=10084
```
失败记录中没有 `ghost node stderr`,也没有 `EPERM`、`EACCES` 或 `ENOENT`。超时后的 `child-process-gone reason=killed` 是宿主清理结果,不是启动失败的前置原因。Sites 列表和详情请求返回 200,成功部署返回 201,因此未发现网络、鉴权或 Sites 服务端异常。
### 初步代码定位
已确认的调用链:
1. XD Sites 仅把宿主的 `PROCESS_START_FAILED` 映射为 `XD_SITES_WORKER_START_FAILED`:
`xd-sites/main.js:342-354`。
2. `createUtilityNodeWorkerProcess` 不使用 Electron 原生 `spawn` 表示就绪;只有收到子进程 `{ type: "ready" }` 后才合成 broker 的 `spawn`:
`apps/desktop/src/main/cindy-brain/nodeRuntimeBroker.ts:331-340`。
3. broker 等待该合成事件 10 秒,超时后强制终止新进程:
`nodeRuntimeBroker.ts:1268-1316`。
4. bootstrap 在 `require(entryPath)` 之前发送 `ready`:
`nodeRuntimeWorkerProcess.ts:206-234`。
因此当前证据把故障范围收敛在 Electron UtilityProcess 的原生启动、bootstrap 早期执行或 parentPort 就绪链;不支持把它归因于 XD Sites 的 `worker.cjs` 业务逻辑。
最相关的生命周期缺口:
- XD Sites 配置为 `lifecycle: on-demand`、`idleTimeoutSeconds: 120`。
- idle timer 调用 `stopWorker`:
`nodeRuntimeBroker.ts:1718-1731`。
- `stopWorker` 先从 `workers` 删除 entry,再调用异步 `kill()`,没有等待真实 `exit`:
`nodeRuntimeBroker.ts:1052-1079`。
- 后续 `ensureWorker` 只检查 `workers` 和 `startingWorkers`,不检查 `liveProcesses`,因此旧 UtilityProcess 尚未确认退出时也允许同 key 再次 fork:
`nodeRuntimeBroker.ts:1101-1128`。
- 原位更新路径已有 `stopAndWait`,明确以真实 `exit` 为完成条件:
`nodeRuntimeBroker.ts:948-990`。
这与“重启后首次成功、空闲回收后二次启动失败”高度相关,但现有日志没有记录原生 Electron `spawn`、idle kill 返回值和旧进程 `exit`,因此尚不能证明旧进程实际残留,也不能断言这是唯一根因。
相关文件在 `v0.1.52`、`v0.1.57`、`v0.1.58-beta` 与当前 HEAD 的内容一致,升级未改变这条启动与回收链。
### 测试缺口与建议
当前空闲回收测试只验证 120 秒后调用了 `kill`、状态变为 off,没有验证第二次请求;测试假进程的 `kill()` 会立即在 microtask 发出 `exit`,无法覆盖 Windows 的真实退出延迟:
`apps/desktop/src/main/cindy-brain/__tests__/nodeRuntimeBroker.test.ts:16-54,417-431`。
建议:
1. 为每个 worker key 增加 stopping/exit 屏障,idle stop 确认真正 `exit` 后才允许再次 fork。
2. 记录 Electron 原生 `spawn`、bootstrap `ready`、PID、`kill()` 返回值、SIGKILL fallback、`exit` 及各阶段耗时。
3. 增加“首次启动 → idle stop 未立即 exit → 第二次请求”的回归测试,断言旧进程退出前不会双开。
4. 增加 Windows packaged-app 集成测试,覆盖真实 `utilityProcess.fork`。
5. 同时覆盖 `kill()` 返回 false 和 exit 永不到达的错误路径。
Contributor guide
Research direction
Start with apps/desktop/src/main/cindy-brain/nodeRuntimeBroker.ts, especially stopWorker, ensureWorker, and the existing stopAndWait path, then inspect nodeRuntimeWorkerProcess.ts for bootstrap readiness. Run the idle-lifecycle cases in apps/desktop/src/main/cindy-brain/__tests__/nodeRuntimeBroker.test.ts and extend investigation to delayed exit behavior. Done means the second request is covered by regression tests and worker restart behavior is reliable after idle shutdown.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, node.js, typescript
- Domain
- desktop, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100