import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { serverRoot, serverInstallDir, serverConfigDir, serverConfigFile, worldConfigFile, serverVersionFile, serverEventsFile, } from './paths.js'; import { installOrUpdateServer, readInstalledVersion } from './SteamCmd.js'; import { ProcessSupervisor } from './ProcessSupervisor.js'; export class ServerManager { defaultAppId; servers = new Map(); supervisor = new ProcessSupervisor(); constructor(defaultAppId) { this.defaultAppId = defaultAppId; // Crash detection + event logging + conditional auto-restart this.supervisor.onCrash((id, exitCode) => { const cfg = this.servers.get(id); if (!cfg) return; // Log crash event this.appendEvent(id, { type: 'crash', exitCode, autoRestart: cfg.autoRestart, }); // Conditional auto-restart if (cfg.autoRestart) { this.restart(id).catch((err) => { this.appendEvent(id, { type: 'autoRestartFailed', error: err.message, }); }); } }); } list() { return Array.from(this.servers.values()).map((cfg) => ({ ...cfg, process: this.supervisor.getInfo(cfg.id) ?? null, version: this.readVersion(cfg.id), })); } get(id) { const cfg = this.servers.get(id); if (!cfg) return null; return { ...cfg, process: this.supervisor.getInfo(id) ?? null, version: this.readVersion(id), }; } readVersion(id) { if (!existsSync(serverVersionFile(id))) return null; return JSON.parse(readFileSync(serverVersionFile(id), 'utf-8')); } writeVersion(id, version) { writeFileSync(serverVersionFile(id), JSON.stringify(version, null, 2)); } appendEvent(id, event) { const file = serverEventsFile(id); let events = []; if (existsSync(file)) { events = JSON.parse(readFileSync(file, 'utf-8')); } events.push({ timestamp: Date.now(), ...event, }); writeFileSync(file, JSON.stringify(events, null, 2)); } async getEvents(id) { const file = serverEventsFile(id); if (!existsSync(file)) return []; return JSON.parse(readFileSync(file, 'utf-8')); } async create(id, name) { if (this.servers.has(id)) { throw new Error(`Server ${id} already exists`); } mkdirSync(serverRoot(id), { recursive: true }); mkdirSync(serverInstallDir(id), { recursive: true }); mkdirSync(serverConfigDir(id), { recursive: true }); const cfg = { id, name: name ?? `Windrose Server ${id}`, installDir: serverInstallDir(id), configDir: serverConfigDir(id), appId: this.defaultAppId, enabled: true, autoRestart: false, // default }; this.servers.set(id, cfg); if (!existsSync(serverConfigFile(id))) { writeFileSync(serverConfigFile(id), JSON.stringify({ serverName: cfg.name }, null, 2)); } if (!existsSync(worldConfigFile(id))) { writeFileSync(worldConfigFile(id), JSON.stringify({ worldName: `${cfg.name} World` }, null, 2)); } await installOrUpdateServer(id, cfg.appId); const version = await readInstalledVersion(id); this.writeVersion(id, version); return this.get(id); } // --------------------------------------------------------- // CONFIG READ // --------------------------------------------------------- readConfig(id) { if (!this.servers.has(id)) { throw new Error(`Server ${id} not found`); } const serverCfg = JSON.parse(readFileSync(serverConfigFile(id), 'utf-8')); const worldCfg = JSON.parse(readFileSync(worldConfigFile(id), 'utf-8')); return { serverCfg, worldCfg }; } // --------------------------------------------------------- // CONFIG UPDATE // --------------------------------------------------------- async updateConfig(id, serverCfg, worldCfg) { if (!this.servers.has(id)) { throw new Error(`Server ${id} not found`); } writeFileSync(serverConfigFile(id), JSON.stringify(serverCfg, null, 2)); writeFileSync(worldConfigFile(id), JSON.stringify(worldCfg, null, 2)); return this.get(id); } async setAutoRestart(id, enabled) { const cfg = this.servers.get(id); if (!cfg) throw new Error(`Server ${id} not found`); cfg.autoRestart = enabled; this.servers.set(id, cfg); this.appendEvent(id, { type: 'autoRestartChanged', enabled, }); return this.get(id); } async start(id) { const cfg = this.servers.get(id); if (!cfg) throw new Error(`Server ${id} not found`); const exePath = join(cfg.installDir, 'R5', 'Binaries', 'Win64', 'WindroseServer-Win64-Shipping.exe'); await installOrUpdateServer(id, cfg.appId); const version = await readInstalledVersion(id); this.writeVersion(id, version); await this.supervisor.start(id, exePath, []); return this.get(id); } async stop(id) { await this.supervisor.stop(id); return this.get(id); } async restart(id) { const cfg = this.servers.get(id); if (!cfg) throw new Error(`Server ${id} not found`); const exePath = join(cfg.installDir, 'R5', 'Binaries', 'Win64', 'WindroseServer-Win64-Shipping.exe'); await installOrUpdateServer(id, cfg.appId); const version = await readInstalledVersion(id); this.writeVersion(id, version); await this.supervisor.restart(id, exePath, []); return this.get(id); } async delete(id) { await this.stop(id); this.servers.delete(id); return true; } }