import { spawn } from 'node:child_process'; import { mkdirSync, createWriteStream } from 'node:fs'; import { logsDir } from './paths.js'; export class ProcessSupervisor { processes = new Map(); info = new Map(); // Crash event subscribers crashHandlers = []; onCrash(handler) { this.crashHandlers.push(handler); } getInfo(id) { return this.info.get(id); } async start(id, exePath, args = []) { if (this.processes.has(id)) { throw new Error(`Server ${id} already running`); } mkdirSync(logsDir(id), { recursive: true }); const stdoutLog = createWriteStream(`${logsDir(id)}/stdout.log`, { flags: 'a' }); const stderrLog = createWriteStream(`${logsDir(id)}/stderr.log`, { flags: 'a' }); const child = spawn('wine64', [exePath, ...args], { env: process.env, }); this.processes.set(id, child); this.info.set(id, { id, pid: child.pid ?? null, status: 'starting' }); child.stdout.on('data', (chunk) => stdoutLog.write(chunk)); child.stderr.on('data', (chunk) => stderrLog.write(chunk)); child.on('spawn', () => { const current = this.info.get(id); if (current) { current.status = 'running'; current.pid = child.pid ?? null; this.info.set(id, current); } }); child.on('exit', (code) => { this.processes.delete(id); const current = this.info.get(id); if (current) { current.status = 'stopped'; current.lastExitCode = code ?? null; this.info.set(id, current); } // Crash detection: non-zero exit code if (code !== 0) { this.crashHandlers.forEach((fn) => fn(id, code ?? -1)); } }); child.on('error', (err) => { console.error(`Server ${id} process error:`, err); const current = this.info.get(id); if (current) { current.status = 'error'; this.info.set(id, current); } this.processes.delete(id); // Treat process errors as crashes with exitCode = -1 this.crashHandlers.forEach((fn) => fn(id, -1)); }); } async stop(id) { const child = this.processes.get(id); if (!child) return; child.kill('SIGTERM'); this.processes.delete(id); const current = this.info.get(id); if (current) { current.status = 'stopped'; this.info.set(id, current); } } async restart(id, exePath, args = []) { await this.stop(id); await this.start(id, exePath, args); } }