/** * ProcessGuard - 进程双层超时守护 * * 参考 openclaw/src/process/supervisor/supervisor.ts * * 两层保护: * 1. Overall Timeout — 进程总运行时限 * 2. No-Output Watchdog — 持续无输出时限(检测「卡死」) * 内部复用 IdleTimer,统一看门狗 touch/cancel 语义 * * 看门狗区分 fresh (新会话) 和 resume (恢复会话) 两种 profile, * resume 场景阈值更短,因为恢复不应该需要太长时间。 */ const { EventEmitter } = require('events'); const { IdleTimer } = require('./idle-timer'); /** 新会话看门狗默认值 */ const FRESH_DEFAULTS = { noOutputTimeoutRatio: 0.8, minMs: 180_000, // 3 min maxMs: 600_000, // 10 min }; /** 恢复会话看门狗默认值 */ const RESUME_DEFAULTS = { noOutputTimeoutRatio: 0.3, minMs: 60_000, // 1 min maxMs: 180_000, // 3 min }; class ProcessGuard extends EventEmitter { /** * @param {import('child_process').ChildProcess} proc - 被守护的子进程 * @param {object} [opts] * @param {number} [opts.overallTimeoutMs=600000] - 总超时 * @param {number} [opts.noOutputTimeoutMs] - 无输出超时 (不设则自动计算) * @param {boolean} [opts.isResume=false] - 是否为恢复会话 * @param {string} [opts.runId] - 运行标识 (用于日志) */ constructor(proc, opts = {}) { super(); this.proc = proc; this.runId = opts.runId || `run-${Date.now()}`; this.killed = false; this.terminationReason = null; const overallMs = opts.overallTimeoutMs || 600_000; const profile = opts.isResume ? RESUME_DEFAULTS : FRESH_DEFAULTS; // 计算看门狗超时: clamp(overall * ratio, min, max) const computedWatchdog = Math.round(overallMs * profile.noOutputTimeoutRatio); this.noOutputTimeoutMs = opts.noOutputTimeoutMs || Math.max(profile.minMs, Math.min(profile.maxMs, computedWatchdog)); this.overallTimeoutMs = overallMs; // Layer 1: Overall 超时 this._overallTimer = setTimeout(() => { this._kill('overall-timeout'); }, overallMs); // Layer 2: Watchdog — 复用 IdleTimer this._watchdog = new IdleTimer(this.noOutputTimeoutMs, () => { this._kill('no-output-timeout'); }, { label: `process-guard:${this.runId}` }); this._watchdog.start(); // 进程退出时自动清理 const cleanup = () => this._clearTimers(); proc.on('close', cleanup); proc.on('error', cleanup); } /** 每次收到进程输出时调用,刷新看门狗 */ touch() { if (!this.killed) { this._watchdog.touch(); } } /** 手动取消守护(正常结束时调用) */ cancel() { this._clearTimers(); } /** 手动终止进程 */ terminate() { this._kill('manual-cancel'); } /** @returns {boolean} 进程是否被守护系统杀死 */ get wasKilled() { return this.killed; } _kill(reason) { if (this.killed) return; this.killed = true; this.terminationReason = reason; this._clearTimers(); this.emit('timeout', { reason, runId: this.runId }); // 优雅关闭: SIGTERM → 2s 后 SIGKILL if (this.proc && !this.proc.killed) { this.proc.kill('SIGTERM'); this._killTimer = setTimeout(() => { try { if (!this.proc.killed) { this.proc.kill('SIGKILL'); } } catch { // 进程可能已经退出 } }, 2000); } } _clearTimers() { if (this._overallTimer) { clearTimeout(this._overallTimer); this._overallTimer = null; } this._watchdog.cancel(); if (this._killTimer) { clearTimeout(this._killTimer); this._killTimer = null; } } } module.exports = { ProcessGuard, FRESH_DEFAULTS, RESUME_DEFAULTS };