feat: add structured logging, process watchdog, and systemd hardening
- Add lib/logger.js: timestamped structured logging with 5MB x 5 file rotation - Add lib/watchdog.js: generic child process supervisor with rate-limited restarts - Enhance client.js: WS ping/pong liveness detection, uncaughtException/unhandledRejection handlers, systemd sd-notify integration - Refactor frpc.js: FrpcManager now delegates to Watchdog instead of manual spawn/exit - Enhance install.sh: environment file, log directory, systemd resource limits, security hardening, WatchdogSec=60 - Replace all console.log/warn/error with structured logger across modules Made-with: Cursor
This commit is contained in:
121
lib/watchdog.js
Normal file
121
lib/watchdog.js
Normal file
@@ -0,0 +1,121 @@
|
||||
'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 proc = spawn(this._bin, this._args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
...this._spawnOpts,
|
||||
});
|
||||
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 };
|
||||
Reference in New Issue
Block a user