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:
stswangzhiping
2026-03-16 07:31:19 +08:00
parent 42d1d361dc
commit b3770d21d4
9 changed files with 545 additions and 149 deletions

View File

@@ -5,15 +5,15 @@ const fs = require('fs');
const os = require('os');
const path = require('path');
const https = require('https');
const log = require('./logger');
const { Watchdog } = require('./watchdog');
// frpc 配置目录(与 clawd config 同目录)
const CONFIG_DIR = process.env.CLAWD_CONFIG_DIR
|| (process.getuid && process.getuid() === 0 ? '/etc/clawd' : path.join(os.homedir(), '.clawd'));
const FRPC_BIN = path.join(CONFIG_DIR, 'frpc');
const FRPC_CONFIG = path.join(CONFIG_DIR, 'frpc.toml');
const TTYD_BIN = path.join(CONFIG_DIR, 'ttyd');
// frp / ttyd 版本
const FRP_VERSION = '0.62.0';
const TTYD_VERSION = '1.7.7';
const TTYD_PORT = 7681;
@@ -27,14 +27,12 @@ function getDashboardInfo() {
return new Promise((resolve) => {
const tmpLog = '/tmp/clawd-dashboard.log';
// 后台启动 dashboard输出重定向到日志文件
try {
execSync(`openclaw dashboard > ${tmpLog} 2>&1 &`, { shell: true, timeout: 3000 });
} catch (e) {
// 已在运行或命令不存在,继续轮询
}
// 每秒读一次日志文件,最多等 10 秒
let attempts = 0;
const interval = setInterval(() => {
attempts++;
@@ -45,7 +43,7 @@ function getDashboardInfo() {
clearInterval(interval);
const port = parseInt(match[1], 10);
const token = match[2];
console.log(`[frpc] openclaw dashboard: port=${port}, token=${token.substring(0, 8)}...`);
log.info('dashboard', `openclaw dashboard: port=${port}, token=${token.substring(0, 8)}...`);
resolve({ dashboard_port: port, dashboard_token: token });
return;
}
@@ -53,18 +51,16 @@ function getDashboardInfo() {
if (attempts >= 10) {
clearInterval(interval);
log.debug('dashboard', 'openclaw dashboard 未检测到,跳过');
resolve({});
}
}, 1000);
});
}
/**
* 根据当前系统架构下载对应的 frpc 二进制。
*/
async function downloadFrpc() {
const arch = os.arch(); // 'x64', 'arm64', 'arm', ...
const platform = os.platform(); // 'linux'
const arch = os.arch();
const platform = os.platform();
const archMap = {
x64: 'amd64', arm64: 'arm64',
@@ -76,56 +72,50 @@ async function downloadFrpc() {
const url = `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/${filename}`;
const tmpFile = `/tmp/${filename}`;
console.log(`[frpc] 下载 frpc ${FRP_VERSION} (${platform}/${frpArch})...`);
log.info('frpc', `下载 frpc ${FRP_VERSION} (${platform}/${frpArch})...`);
await downloadFile(url, tmpFile);
// 解压并复制 frpc
fs.mkdirSync(CONFIG_DIR, { recursive: true });
execSync(`tar -xzf ${tmpFile} -C /tmp && cp /tmp/frp_${FRP_VERSION}_${platform}_${frpArch}/frpc ${FRPC_BIN}`, {
stdio: 'inherit'
});
fs.chmodSync(FRPC_BIN, 0o755);
console.log(`[frpc] frpc 已安装到 ${FRPC_BIN}`);
log.info('frpc', `frpc 已安装到 ${FRPC_BIN}`);
}
/**
* 下载 ttyd 静态二进制。
*/
async function downloadTtyd() {
const arch = os.arch();
const archMap = { arm64: 'aarch64', x64: 'x86_64', arm: 'armv7l', ia32: 'i686' };
const ttydArch = archMap[arch] || 'x86_64';
const url = `https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttydArch}`;
console.log(`[ttyd] 下载 ttyd ${TTYD_VERSION} (${ttydArch})...`);
log.info('ttyd', `下载 ttyd ${TTYD_VERSION} (${ttydArch})...`);
fs.mkdirSync(CONFIG_DIR, { recursive: true });
await downloadFile(url, TTYD_BIN);
fs.chmodSync(TTYD_BIN, 0o755);
console.log(`[ttyd] ttyd 已安装到 ${TTYD_BIN}`);
log.info('ttyd', `ttyd 已安装到 ${TTYD_BIN}`);
}
/**
* 启动 ttyd如未安装先下载
* ttyd 绑定 127.0.0.1:7681供 frpc 代理。
* 返回 true 表示启动成功false 表示失败。
*/
async function startTtyd() {
if (!fs.existsSync(TTYD_BIN)) {
try {
await downloadTtyd();
} catch (e) {
console.warn('[ttyd] 下载失败:', e.message);
log.warn('ttyd', '下载失败:', e.message);
return false;
}
}
// 终止旧进程(重启 clawd 时可能残留)
// 终止旧进程
try {
execSync(`pkill -f "${TTYD_BIN}"`, { timeout: 3000 });
// 稍等旧进程退出
await new Promise(r => setTimeout(r, 500));
} catch (_) { /* 无进程可杀,忽略 */ }
} catch (_) {}
try {
const shell = fs.existsSync('/bin/bash') ? '/bin/bash' : '/bin/sh';
@@ -134,10 +124,10 @@ async function startTtyd() {
detached: true,
});
proc.unref();
console.log(`[ttyd] 已启动,端口 ${TTYD_PORT}shell=${shell}`);
log.info('ttyd', `已启动,端口 ${TTYD_PORT}shell=${shell}`);
return true;
} catch (e) {
console.warn('[ttyd] 启动失败:', e.message);
log.warn('ttyd', '启动失败:', e.message);
return false;
}
}
@@ -156,12 +146,6 @@ function downloadFile(url, dest) {
});
}
/**
* 生成 frpc.toml 配置文件。
* 包含两条代理:
* - dashboard-{clawId} → openclaw dashboard
* - tty-{clawId} → ttyd 终端
*/
function writeFrpcConfig(clawId, frpConfig) {
const { server, port, auth_token, dashboard_local_port = 18789 } = frpConfig;
const ttyRemotePort = 10000 + Number(clawId);
@@ -187,66 +171,44 @@ remotePort = ${ttyRemotePort}
`;
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(FRPC_CONFIG, toml, 'utf8');
console.log(`[frpc] frpc.toml 已写入: dashboard subdomain=${clawId}, tty tcp-port=${ttyRemotePort}`);
log.info('frpc', `frpc.toml 已写入: dashboard subdomain=${clawId}, tty tcp-port=${ttyRemotePort}`);
}
/**
* FrpcManager —— 基于 Watchdog 的 frpc 进程管理器。
* 崩溃自动重启5 分钟内最多重启 10 次。
*/
class FrpcManager {
constructor() {
this._proc = null;
this._stopped = false;
this._restartTimer = null;
this._watchdog = null;
}
/**
* 启动 frpc如未安装先下载写配置然后 spawn。
*/
async start(clawId, frpConfig) {
this._stopped = false;
this.stop();
// 下载 frpc如果不存在
if (!fs.existsSync(FRPC_BIN)) {
try {
await downloadFrpc();
} catch (e) {
console.error('[frpc] 下载 frpc 失败:', e.message);
log.error('frpc', '下载 frpc 失败:', e.message);
return;
}
}
writeFrpcConfig(clawId, frpConfig);
this._spawn();
}
_spawn() {
if (this._stopped) return;
console.log('[frpc] 启动 frpc...');
this._proc = spawn(FRPC_BIN, ['-c', FRPC_CONFIG], {
stdio: ['ignore', 'pipe', 'pipe'],
});
this._proc.stdout.on('data', d => {
const line = d.toString().trim();
if (line) console.log(`[frpc] ${line}`);
});
this._proc.stderr.on('data', d => {
const line = d.toString().trim();
if (line) console.warn(`[frpc] ${line}`);
});
this._proc.on('exit', (code) => {
console.warn(`[frpc] 进程退出 (code=${code})`);
if (!this._stopped) {
this._restartTimer = setTimeout(() => this._spawn(), 5000);
}
this._watchdog = new Watchdog('frpc', FRPC_BIN, ['-c', FRPC_CONFIG], {
maxRestarts: 10,
windowMs: 300_000,
restartDelay: 5_000,
});
this._watchdog.start();
}
stop() {
this._stopped = true;
if (this._restartTimer) clearTimeout(this._restartTimer);
if (this._proc) {
this._proc.kill('SIGTERM');
this._proc = null;
if (this._watchdog) {
this._watchdog.stop();
this._watchdog = null;
}
}
}