Files
clawd/lib/watchdog.js
stswangzhiping 04dd1017bb fix(network): wired ping probe, AP/WS and systemd notify hardening
- Add hasWiredInternetProbe and export; AP mode uses it with hasInternet
- systemd-env: strip NOTIFY_SOCKET from env early; client uses unix_dgram
- Strip NOTIFY_SOCKET from frpc/ttyd spawn env in watchdog and frpc
- WS: pong miss debounce; AP net monitor consecutive-fail debounce

Made-with: Cursor
2026-03-28 14:37:56 +08:00

126 lines
3.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const { spawn } = require('child_process');
const log = require('./logger');
const DEFAULT_MAX_RESTARTS = 10;
const DEFAULT_WINDOW_MS = 300_000; // 5 min
const DEFAULT_RESTART_DELAY = 3_000;
/**
* 通用子进程守护:崩溃自动重启、速率限制、健康回调。
*
* 用法:
* const wd = new Watchdog('frpc', '/path/to/frpc', ['-c', 'frpc.toml'], {
* maxRestarts: 10,
* windowMs: 300_000,
* onStdout: (line) => { ... },
* });
* wd.start();
* wd.stop();
*/
class Watchdog {
constructor(name, bin, args = [], opts = {}) {
this._name = name;
this._bin = bin;
this._args = args;
this._proc = null;
this._stopped = false;
this._restartTimer = null;
this._onStdout = opts.onStdout || null;
this._onStderr = opts.onStderr || null;
this._onExit = opts.onExit || null;
this._spawnOpts = opts.spawnOpts || {};
this._maxRestarts = opts.maxRestarts ?? DEFAULT_MAX_RESTARTS;
this._windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;
this._restartDelay = opts.restartDelay ?? DEFAULT_RESTART_DELAY;
this._restartTimes = []; // timestamps of recent restarts
}
get running() {
return !!(this._proc && !this._proc.killed);
}
start() {
this._stopped = false;
this._spawn();
}
stop() {
this._stopped = true;
if (this._restartTimer) {
clearTimeout(this._restartTimer);
this._restartTimer = null;
}
if (this._proc) {
this._proc.kill('SIGTERM');
// 强杀兜底
const p = this._proc;
setTimeout(() => { try { p.kill('SIGKILL'); } catch (_) {} }, 5000);
this._proc = null;
}
}
_spawn() {
if (this._stopped) return;
log.info(this._name, '启动进程...');
const { env: optsEnv, ...restSpawn } = this._spawnOpts;
const env = { ...process.env, ...optsEnv };
delete env.NOTIFY_SOCKET; // 避免 frpc 等子进程向 systemd 发 notify触发非主 PID 拒收
const proc = spawn(this._bin, this._args, {
stdio: ['ignore', 'pipe', 'pipe'],
...restSpawn,
env,
});
this._proc = proc;
proc.stdout.on('data', (d) => {
const line = d.toString().trim();
if (!line) return;
if (this._onStdout) this._onStdout(line);
else log.info(this._name, line);
});
proc.stderr.on('data', (d) => {
const line = d.toString().trim();
if (!line) return;
if (this._onStderr) this._onStderr(line);
else log.warn(this._name, line);
});
proc.on('error', (err) => {
log.error(this._name, '进程启动失败:', err.message);
});
proc.on('exit', (code, signal) => {
log.warn(this._name, `进程退出 code=${code} signal=${signal}`);
this._proc = null;
if (this._onExit) this._onExit(code, signal);
if (!this._stopped) this._scheduleRestart();
});
}
_scheduleRestart() {
const now = Date.now();
this._restartTimes.push(now);
// 只保留窗口内的记录
this._restartTimes = this._restartTimes.filter(t => now - t < this._windowMs);
if (this._restartTimes.length > this._maxRestarts) {
log.error(this._name,
`${this._windowMs / 1000}s 内重启 ${this._restartTimes.length} 次,超过上限 ${this._maxRestarts},停止守护`);
return;
}
const delay = this._restartDelay * Math.min(this._restartTimes.length, 5);
log.info(this._name, `${delay / 1000}s 后重启... (窗口内第 ${this._restartTimes.length} 次)`);
this._restartTimer = setTimeout(() => this._spawn(), delay);
}
}
module.exports = { Watchdog };