- 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
126 lines
3.6 KiB
JavaScript
126 lines
3.6 KiB
JavaScript
'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 };
|