ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ClawX 退出时终止 openclaw-acp 子进程:孤儿进程根因分析与 SIGTERM→SIGKILL 两级回收方案

ClawX 退出时终止 openclaw-acp 子进程:孤儿进程根因分析与 SIGTERM→SIGKILL 两级回收方案 人工智能AI 应用桌面应用交互助手【免费下载链接】ClawXClawX is a desktop app that provides a graphical interface for OpenClaw AI agents. It turns CLI-based AI orchestration into a desktop experience without using the terminal. China website is https://clawx.com.cn.项目地址https://gitcode.com/gh_mirrors/cl/ClawX点击查看免费下载导读ClawX 是一个为 OpenClaw AI Agent 提供图形界面的桌面应用其中 ACPAgent Client Protocol原生聊天能力由 Electron Main 进程 fork 出的openclaw-acp子进程承载。本文基于仓库中 fix-acp-child-orphan-on-quit.md 任务文档完整讲解该子进程在应用退出时成为孤儿进程的根因、修复设计有界宽限的 SIGTERM→SIGKILL 分级终止、退出时序集成以及对应的源码实现与测试验证路径。读完本文你将掌握在 Electron 应用生命周期中正确管理会主动重连网关的常驻子进程的通用方案以及如何在 ClawX 中验证这一行为。背景一个在退出时拒绝死亡的子进程openclaw-acp是 ClawX 为提供 ACP 原生聊天体验而 fork 的 OpenClaw 子进程。在正常情况下它的生命周期由AcpChatService.spawnConnection()管理——服务通过node:child_process的fork()启动该进程并与其 stdin/stdout 建立 NDJSON 流式连接参见 acp-chat-service.ts。问题出现在应用退出时ClawX 从未向其 fork 的 openclaw-acp 子进程发送过终止信号于是该子进程在 ClawX 退出后继续存活并通过重新连接 Gateway 的方式起死回生比应用本身活得更久成为典型的孤儿进程。任务文档 fix-acp-child-orphan-on-quit.md 将其定性为一次runtime-bridge类型的缺陷修复期望的用户行为非常明确以任何方式退出 ClawXWindows/Linux 关闭窗口、macOS CmdQ、SIGINT/SIGTERM 信号都必须终止 openclaw-acp 子进程而不是留下孤儿重启后 ACP 聊天继续可用被停止的子进程会在下一次会话加载时被一个新的 fork 实例替换。根因分析三个层面的原因叠加从源码结构看孤儿进程的产生并非单一因素而是三个层面的问题叠加1. 服务层缺少退出清理路径。AcpChatService此前只在一个畸形 stdio 守卫forked.stdin/stdout/stderr缺失时调用forked.kill()见 acp-chat-service.ts之外没有其他杀死子进程的通道。正常生命周期下没有任何代码对子进程调用过kill()。2. 应用层退出只清理了 Gateway。before-quit处理器此前只停止 Gateway UtilityProcess 和扩展extensionRegistry.teardownAll()从未触及 ACP 子进程。3. OpenClaw ACP 代理自身的存活策略。OpenClaw ACP 代理只在收到 SIGINT/SIGTERM 时关闭——它不监听 stdin EOF也不监听父进程 PID并且在 Gateway 消失后会主动重新连接。这意味着父进程死亡对它毫无影响它会无限期地作为孤儿存活并尝试重连。正如源码注释所强调的acp-chat-service.tsThe ACP child only shuts down on SIGINT/SIGTERM and deliberately survives Gateway loss by reconnecting, so quit cleanup must signal it explicitly.—— 它特意设计为在丢失 Gateway 时存活并重连因此退出清理必须显式地向它发送信号。修复设计在 Main 侧接管子进程生命周期任务文档 fix-acp-child-orphan-on-quit.md 明确限定了修复范围在 Main 侧全权接管子进程生命周期为AcpChatService新增一个有界 SIGTERM→SIGKILL 的stop()暴露当前活动实例供退出路径使用在既有before-quit竞态中将 ACP 终止排在gatewayManager.stop()之前不改动ACP 消息流、spawn 选项或渲染进程行为。这个先停 ACP、再停 Gateway的顺序是修复的关键如果先停 Gatewayopenclaw-acp 检测到 Gateway 消失后会立刻进入重连循环让退出清理前功尽弃。只有先让子进程安静地死掉再关闭 Gateway才能彻底切断重连路径。实现一AcpChatService.stop() 的两级终止stop()是本次修复的核心位于 acp-chat-service.ts。它实现了任务验收标准中发送 SIGTERM → 等待有界宽限 → 升级 SIGKILL → 解析完成的完整流程async stop(sigtermGraceMs: number ACP_STOP_SIGTERM_GRACE_MS): Promisevoid { const child this.child; if (!child) return; if (child.exitCode ! null || child.signalCode) { this.dropConnectionForChild(child); return; } this.trace(connection/stop:start, {}); const exited this.waitForChildExit(child); child.kill(SIGTERM); const stoppedGracefully await Promise.race([ exited.then(() true), new Promisesigterm-timeout((resolve) { setTimeout(() resolve(sigterm-timeout), sigtermGraceMs).unref(); }), ]); if (stoppedGracefully true) return; this.trace(connection/stop:timeout, { details: { sigtermGraceMs } }); try { child.kill(SIGKILL); } catch (error) { logger.warn([acp-chat] ACP SIGKILL failed: ${String(error)}); return; } await Promise.race([ exited, new Promise((resolve) { setTimeout(resolve, ACP_STOP_SIGKILL_WAIT_MS).unref(); }), ]); }与之配套的两个常量定义了终止节奏acp-chat-service.ts常量值含义ACP_STOP_SIGTERM_GRACE_MS3_000发送 SIGTERM 后等待子进程优雅退出的宽限期ACP_STOP_SIGKILL_WAIT_MS1_000升级 SIGKILL 后再等待其退出的上限各分支行为与验收标准的对应关系无子进程时为 no-opthis.child为null时直接返回不做任何信号操作已退出子进程不再补刀若exitCode或signalCode已非空只走dropConnectionForChild(child)清理连接状态不重复发送信号宽限期内正常退出Promise.race中exited先落定方法在 SIGTERM 阶段即返回SIGKILL 永远不被触发宽限期超时升级 SIGKILLsigterm-timeout分支先落定时发送SIGKILL并最多再等待ACP_STOP_SIGKILL_WAIT_MS1 秒让进程退出避免stop()无限挂起阻塞退出流程SIGKILL 发送失败进程已消失等竞态记录 warn 日志后返回不让退出流程被异常中断。副作用处理清理连接状态与取消权限等待者stop()的另一个职责是同步清理内部状态。当子进程退出时AcpChatService会通过其既有的 child-exit 路径调用dropConnectionForChild(child)acp-chat-service.ts该路径会以cancelledPermissionResponse()解析并清空所有 pending 的permissionWaiters确保渲染进程中等待权限响应的 Promise 不会被永久悬挂将initialized、connection、child、loadedSessionKey、loadedAcpSessionId、historicalSessionKey、historicalGeneration、permissionsEnabled、sessionListSupported全部复位并清空livePrompts。由于dropConnectionForChild是服务既有的子进程退出统一出口stop()无需新增任何额外的等待者取消逻辑天然满足验收标准中通过既有 child-exit drop 路径将 pending 权限等待者以 cancelled 解析的要求。实例注册退出路径零新布线为了让before-quit无需通过 chat-api 新增布线即可拿到服务实例模块级新增了活动实例注册机制acp-chat-service.tslet activeInstance: AcpChatService | null null; export function getActiveAcpChatService(): AcpChatService | null { return activeInstance; } export function createAcpChatService( mainWindow: MainWindowLike, accessRegistry: AcpSessionAccessRegistry, gateway?: GatewayPairingRpcClient, ): AcpChatService { const service new AcpChatService(mainWindow, accessRegistry, undefined, gateway); activeInstance service; return service; }createAcpChatService()创建服务实例的同时将其登记为activeInstance退出路径通过getActiveAcpChatService()?.stop()即可拿到当前实例并触发终止全程不需要任何 IPC 通道或 chat-api 的配合。未创建服务时例如应用刚启动、从未加载过 ACP 会话getActiveAcpChatService()返回null可选链调用自然退化为 no-op。实现二before-quit 中的有序清理退出集成位于 electron/main/index.ts。before-quit处理器按以下顺序工作调用setQuitting()并取消 DingTalk DWS OAuth通过requestQuitLifecycleAction()检查退出生命周期状态若状态为allow-quit则放行若已有清理在进行中则等待否则event.preventDefault()启动extensionRegistry.teardownAll()构建stopPromise其中 ACP 清理与 Gateway 清理被显式编排为有序对const stopPromise Promise.all([ (async () { // Stop ACP before Gateway so the child cannot reconnect and outlive the app. try { await getActiveAcpChatService()?.stop(); } catch (err) { logger.warn(AcpChatService.stop() error during quit:, err); } try { await gatewayManager.stop(); } catch (err) { logger.warn(gatewayManager.stop() error during quit:, err); } })(), // Computer Use cleanup runs independently of this ordered pair. (async () { /* computerUseApi.stop() */ })(), ]);源码注释直接记录了设计意图Stop ACP before Gateway so the child cannot reconnect and outlive the app.。Computer Use 运行时清理与此有序对相互独立可并行执行。整个清理过程受既有5 秒退出竞态预算约束stopPromise与一个setTimeout(..., 5000)的超时 Promise 竞争electron/main/index.ts。若超时先落定则记录 warn 并调用gatewayManager.forceTerminateOwnedProcessForQuit()强制终止 Gateway 进程然后markQuitCleanupCompleted()并再次app.quit()。这里体现了修复对既有约束的尊重ACP 终止不会引入新的无限等待stop()自身的 SIGTERM 宽限3s SIGKILL 等待1s上限约 4 秒天然落在 5 秒预算之内退出总时长不被破坏。关联机制fork 时的环境契约虽然本次修复不改动 spawn 选项但理解子进程的存活策略离不开 fork 环境契约。spawnConnection()通过getOpenClawEmbeddedForkSpec([acp])获取 fork 规格并注入两个关键环境变量acp-chat-service.tsOPENCLAW_GATEWAY_TOKENACP 是本地 Gateway 客户端必须使用启动本 ClawX 自有 Gateway 的 token而不是依赖配置文件回退OPENCLAW_ACP_RECOVERY_GRACE_ENV来自 recovery-budget.ts 的策略值用于有界地等待 Gateway 恢复。而 openclaw-cli.ts 中的getOpenClawEmbeddedForkSpec()还会注入OPENCLAW_NO_RESPAWN1、OPENCLAW_EMBEDDED_INClawX、OPENCLAW_EXEC_SHELL_SNAPSHOT0并按打包形态选择ELECTRON_RUN_AS_NODE或内置 Node 可执行文件。这些环境变量共同决定了子进程知道自己是嵌入进程、发生故障时不自行 respawn——但即便如此它依然不会因为父进程退出而自杀这正是必须显式发信号的深层原因。测试验证从单测到回归单元测试本次修复在 tests/unit/acp-chat-service.test.ts 中新增了五个针对性用例完整覆盖stop()的所有分支测试用例验证点stop() resolves without signalling when no ACP child was spawned未 spawn 子进程时stop()直接 resolve不产生任何信号stop() signals the ACP child with SIGTERM, drops connection state, and cancels permission waitersSIGTERM 恰好调用一次子进程 exit 后连接状态被丢弃、pending 权限等待者以 cancelled 解析随后再次loadSession会 fork 新子进程并重新初始化验证停止后重启可继续工作stop() escalates to SIGKILL when the ACP child ignores SIGTERM用vi.useFakeTimers()推进 3 秒断言第二次kill以SIGKILL发出随后子进程以SIGKILL退出时stop()resolvestop() does not signal an already-exited ACP child子进程已退出后调用stop()断言kill未被调用registers the created service as the active instance for quit-time cleanupcreateAcpChatService()后getActiveAcpChatService()返回同一实例其中 SIGTERM 用例还额外验证了停止后重新加载会话会 fork 新进程expect(childProcessMock.fork).toHaveBeenCalledTimes(2)这正是任务文档ACP chat keeps working after relaunch; a stopped child is replaced by a fresh fork on the next session load的直接代码级印证。建议的完整验证命令任务文档 fix-acp-child-orphan-on-quit.md 的requiredTests给出了完整的回归清单pnpm exec vitest run tests/unit/acp-chat-service.test.ts # ACP 服务单元测试 pnpm run typecheck # 全量类型检查 pnpm run comms:replay # 通信录制回放 pnpm run comms:compare # 通信基线对比comms:replay/comms:compare用于守护 Gateway 后端通信行为不因本次修改产生漂移对应 comms-regression.md 规则而renderer-main-boundary与backend-communication-boundary规则则确保本次Main 侧接管子进程生命周期的改动没有把任何 ACP 消息流、渲染逻辑或普通 Gateway RPC 路径牵扯进来。手动复现与验收检查若要在本地观察修复前后的行为差异可按以下思路检查启动 ClawX进入任一 ACP 聊天会话触发一次真实的会话加载确保openclaw-acp子进程已 fork以任意方式退出应用关闭窗口、CmdQ、或向主进程发送 SIGINT/SIGTERM退出后检查系统进程表中是否还存在残留的 openclaw-acp 进程例如ps aux | grep -i acp——修复后不应再有任何残留重新启动 ClawX加载同一会话确认聊天历史与继续对话正常新 fork 的子进程按需重建。验收要点与任务文档的acceptance逐条对应stop()对无子进程/已退出子进程是安全的 no-op权限等待者在终止时以 cancelled 收尾ACP 清理严格排在gatewayManager.stop()之前活动实例通过getActiveAcpChatService()获取退出路径零新布线。小结本次修复为 ClawX 的 ACP 原生聊天引入了一条完整、有界、可测试的子进程退出路径根因openclaw-acp 只在 SIGINT/SIGTERM 时退出且会在 Gateway 消失后主动重连父进程死亡不会自动带走它方案AcpChatService.stop()以 3 秒 SIGTERM 宽限 1 秒 SIGKILL 等待的两级终止接管子进程无子进程与已退出场景安全 no-op权限等待者沿既有 drop 路径取消集成before-quit在gatewayManager.stop()之前等待 ACP 终止避免子进程进入 Gateway 重连循环整体仍受既有 5 秒退出预算约束验证五个单元测试覆盖全部分支加上 typecheck 与 comms 回放/对比守护回归。这套识别不会随父进程死亡的常驻子进程 → 显式发信号 → 有界宽限 → 强制升级 → 排序清理的模式同样适用于其他会主动重连服务的嵌入子进程场景。延伸阅读ACP Chat 架构参考ACP 聊天的整体架构、历史权威与时间线模型acp-chat-state-and-history.mdACP 会话状态与历史规则backend-communication-boundary.mdMain/渲染层通信边界comms-regression.md通信回归守护规则acp-chat-service.tsstop()与 spawn 的完整实现electron/main/index.tsbefore-quit退出编排tests/unit/acp-chat-service.test.tsstop()单元测试赞分享人工智能AI 应用桌面应用交互助手【免费下载链接】ClawXClawX is a desktop app that provides a graphical interface for OpenClaw AI agents. It turns CLI-based AI orchestration into a desktop experience without using the terminal. China website is https://clawx.com.cn.项目地址https://gitcode.com/gh_mirrors/cl/ClawX点击查看免费下载相关推荐claude-mem Windows 进程树治理Chroma 子进程链的完整回收与孤儿进程防线claude mem Windows 进程树治理Chroma 子进程链的完整回收与孤儿进程防线 本文基于计划文档 plans/2026 08 18 chrom人工智能Agent 记忆RAGMCP 服务知识图谱AI 插件no-mistakes 子进程生命周期治理进程树边界、孤儿回收与 Windows 加固实践no mistakes 子进程生命周期治理进程树边界、孤儿回收与 Windows 加固实践 本篇技术指南以 no mistakes 仓库内部技能文档 .age开发工具CLIAI 应用质量保障Oh My OpenAgent CodeGraph 僵尸进程清扫孤儿进程匹配、进程树终止与实机 QA 证据链Oh My OpenAgent CodeGraph 僵尸进程清扫孤儿进程匹配、进程树终止与实机 QA 证据链 本文围绕 oh my openagent 仓库中人工智能AI Agent代码智能体多智能体MCP ClientsAgent 编排上一篇深入解析Typora插件json_rpc的使用与实现下一篇解决PyBaMM依赖困境5个优雅方案实现可选依赖项测试全覆盖创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表