process-guard.js
3.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
* 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 };