process-guard.js 3.7 KB
/**
 * 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 };