import { fork, spawn, type ChildProcess } from "child_process"; import { appendFileSync } from "node:fs"; type WorkerLifecycleMessage = | { type: "paseo:shutdown"; } | { type: "paseo:restart"; reason?: string; }; type SupervisorOptions = { name: string; startupMessage: string; launchLogPath?: string; resolveWorkerEntry: () => string; workerArgs?: string[]; workerEnv?: NodeJS.ProcessEnv; workerExecArgv?: string[]; resolveWorkerSpawnSpec?: (workerEntry: string) => { command: string; args: string[]; env?: NodeJS.ProcessEnv; } | null; restartOnCrash?: boolean; onSupervisorExit?: () => Promise | void; }; function describeExit(code: number | null, signal: NodeJS.Signals | null): string { return signal ?? (typeof code === "number" ? `code ${code}` : "unknown"); } function parseLifecycleMessage(msg: unknown): WorkerLifecycleMessage | null { if (typeof msg !== "object" || msg === null || !("type" in msg)) { return null; } const type = (msg as { type?: unknown }).type; if (type === "paseo:shutdown") { return { type: "paseo:shutdown" }; } if (type === "paseo:restart") { const reason = (msg as { reason?: unknown }).reason; return { type: "paseo:restart", ...(typeof reason === "string" && reason.trim().length > 0 ? { reason } : {}), }; } return null; } export function runSupervisor(options: SupervisorOptions): void { const restartOnCrash = options.restartOnCrash ?? false; const workerArgs = options.workerArgs ?? process.argv.slice(2); const workerEnv = options.workerEnv ?? process.env; const workerExecArgv = options.workerExecArgv ?? ["--import", "tsx"]; const resolveWorkerSpawnSpec = options.resolveWorkerSpawnSpec; let child: ChildProcess | null = null; let restarting = false; let shuttingDown = false; let exiting = false; const log = (message: string): void => { process.stderr.write(`[${options.name}] ${message}\n`); }; const logLaunch = (message: string, details?: Record): void => { if (!options.launchLogPath) { return; } const line = [ new Date().toISOString(), `[${options.name}]`, `pid=${process.pid}`, message, details ? JSON.stringify(details) : "", ] .filter((part) => part.length > 0) .join(" "); try { appendFileSync(options.launchLogPath, `${line}\n`, "utf-8"); } catch { // Keep launch debugging best-effort so daemon startup behavior is unchanged. } }; const exitSupervisor = (code: number): void => { if (exiting) { return; } exiting = true; Promise.resolve(options.onSupervisorExit?.()) .catch((error) => { const message = error instanceof Error ? error.message : String(error); log(`Supervisor exit cleanup failed: ${message}`); }) .finally(() => { process.exit(code); }); }; const spawnWorker = () => { let workerEntry: string; try { // Resolve at spawn time so restarts pick up current filesystem state. workerEntry = options.resolveWorkerEntry(); } catch (error) { const message = error instanceof Error ? error.message : String(error); log(`Failed to resolve worker entry: ${message}`); exitSupervisor(1); return; } const spawnSpec = resolveWorkerSpawnSpec?.(workerEntry) ?? null; if (spawnSpec) { logLaunch("spawning worker via explicit spawn spec", { workerEntry, command: spawnSpec.command, args: spawnSpec.args, workerArgs, }); child = spawn(spawnSpec.command, spawnSpec.args, { stdio: ["inherit", "inherit", "inherit", "ipc"], env: spawnSpec.env ?? workerEnv, }); } else { logLaunch("spawning worker via fork", { workerEntry, workerArgs, workerExecArgv, }); child = fork(workerEntry, workerArgs, { stdio: "inherit", env: workerEnv, execArgv: workerExecArgv, }); } logLaunch("worker spawn returned", { childPid: child.pid ?? null, connected: child.connected, spawnfile: child.spawnfile, spawnargs: child.spawnargs, }); child.on("spawn", () => { logLaunch("worker emitted spawn", { childPid: child?.pid ?? null, }); }); child.on("error", (error) => { logLaunch("worker emitted error", { childPid: child?.pid ?? null, message: error.message, }); }); child.on("message", (msg: unknown) => { const lifecycleMessage = parseLifecycleMessage(msg); logLaunch("worker emitted ipc message", { childPid: child?.pid ?? null, rawType: typeof msg === "object" && msg !== null && "type" in msg ? (msg as { type?: unknown }).type : typeof msg, }); if (!lifecycleMessage) { return; } if (lifecycleMessage.type === "paseo:shutdown") { requestShutdown("Shutdown requested by worker"); return; } requestRestart("Restart requested by worker"); }); child.on("exit", (code, signal) => { logLaunch("worker emitted exit", { childPid: child?.pid ?? null, code, signal, restarting, shuttingDown, }); const exitDescriptor = describeExit(code, signal); if (shuttingDown) { log(`Worker exited (${exitDescriptor}). Supervisor shutting down.`); exitSupervisor(0); return; } if (restarting || (restartOnCrash && code !== 0 && code !== null)) { restarting = false; log(`Worker exited (${exitDescriptor}). Restarting worker...`); spawnWorker(); return; } log(`Worker exited (${exitDescriptor}). Supervisor exiting.`); exitSupervisor(typeof code === "number" ? code : 0); }); child.on("close", (code, signal) => { logLaunch("worker emitted close", { childPid: child?.pid ?? null, code, signal, }); }); child.on("disconnect", () => { logLaunch("worker emitted disconnect", { childPid: child?.pid ?? null, }); }); }; const requestRestart = (reason: string) => { if (!child || restarting || shuttingDown) { return; } restarting = true; log(`${reason}. Stopping worker for restart...`); child.kill("SIGTERM"); }; const requestShutdown = (reason: string) => { if (shuttingDown) { return; } shuttingDown = true; restarting = false; log(`${reason}. Stopping worker...`); if (!child) { exitSupervisor(0); return; } child.kill("SIGTERM"); }; const forwardSignal = (signal: NodeJS.Signals) => { requestShutdown(`Received ${signal}`); }; process.on("SIGINT", () => forwardSignal("SIGINT")); process.on("SIGTERM", () => forwardSignal("SIGTERM")); logLaunch("supervisor starting", { startupMessage: options.startupMessage, restartOnCrash, workerArgs, workerExecArgv, }); process.stdout.write(`[${options.name}] ${options.startupMessage}\n`); spawnWorker(); }