171 lines
6.2 KiB
JavaScript
171 lines
6.2 KiB
JavaScript
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;
|
|
}
|
|
}
|