import { spawn } from "node:child_process"; import { dirname } from "node:path"; import { writePidFile, cleanupPidFile, killAllSubprocesses } from "../utils/pid.mjs"; const CRASH_LOG_LINES = 50; const RESTART_RESET_MS = 30_000; export class ServerSupervisor { constructor({ serverPath, env, maxRestarts = 2, memoryLimit = 512, onCrashCallback }) { this.serverPath = serverPath; this.env = env; this.maxRestarts = maxRestarts; this.memoryLimit = memoryLimit; this.onCrashCallback = onCrashCallback; this.restartCount = 0; this.startedAt = 0; this.crashLog = []; this.child = null; this.isShuttingDown = false; } start() { this.startedAt = Date.now(); this.crashLog = []; const showLog = process.env.OMNIROUTE_SHOW_LOG === "1"; this.child = spawn("node", [`--max-old-space-size=${this.memoryLimit}`, this.serverPath], { cwd: dirname(this.serverPath), env: this.env, stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"], }); writePidFile("server", this.child.pid); if (this.child.stderr) { this.child.stderr.on("data", (data) => { const lines = data.toString().split("\n").filter(Boolean); this.crashLog.push(...lines); if (this.crashLog.length > CRASH_LOG_LINES) { this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES); } }); } this.child.on("error", (err) => this.handleExit(err.code ?? -1, err)); this.child.on("exit", (code) => this.handleExit(code)); return this.child; } handleExit(code) { cleanupPidFile("server"); if (this.isShuttingDown || code === 0) { process.exit(code || 0); return; } const aliveMs = Date.now() - this.startedAt; if (aliveMs >= RESTART_RESET_MS) this.restartCount = 0; if (this.restartCount >= this.maxRestarts) { console.error(`\n⚠ Server crashed ${this.maxRestarts} times in <30s.`); if (this.onCrashCallback) { const action = this.onCrashCallback(this.crashLog); if (action === "disable-mitm-and-retry") { console.error("⚠ Disabling MITM and retrying...\n"); this.restartCount = 0; this.start(); return; } } this.dumpCrashLog(); process.exit(code ?? 1); return; } this.restartCount++; const delay = Math.min(1000 * 2 ** (this.restartCount - 1), 10_000); console.error( `\n⚠ Server exited (code=${code ?? "?"}). Restarting in ${delay / 1000}s... (${this.restartCount}/${this.maxRestarts})` ); if (this.crashLog.length) this.dumpCrashLog(); setTimeout(() => this.start(), delay); } dumpCrashLog() { console.error("\n--- Server crash log ---"); this.crashLog.forEach((l) => console.error(l)); console.error("--- End crash log ---\n"); } stop() { this.isShuttingDown = true; if (this.child?.pid) { try { process.kill(this.child.pid, "SIGTERM"); } catch {} setTimeout(() => { try { process.kill(this.child.pid, "SIGKILL"); } catch {} }, 5000); } killAllSubprocesses(); } } export function detectMitmCrash(crashLog) { const text = crashLog.join("\n").toLowerCase(); const signals = ["mitm", "tls socket", "certificate", "hosts", "eaccess"]; return signals.filter((s) => text.includes(s)).length >= 2; }