79 lines
2.8 KiB
JavaScript
79 lines
2.8 KiB
JavaScript
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);
|
|
}
|
|
}
|