const fs = require("fs"); const path = require("path"); const { spawn, spawnSync } = require("child_process"); const ROOT = path.join(__dirname, ".."); const DATA_DIR = path.join(ROOT, "data"); const LOG_DIR = path.join(ROOT, "logs"); const PID_FILE = path.join(DATA_DIR, "server.pid"); const OUT_LOG = path.join(LOG_DIR, "server.out.log"); const ERR_LOG = path.join(LOG_DIR, "server.err.log"); const UPDATE_LOG = path.join(LOG_DIR, "update.log"); fs.mkdirSync(DATA_DIR, { recursive: true }); fs.mkdirSync(LOG_DIR, { recursive: true }); const command = process.argv[2] || "status"; if (command === "start") { start(); } else if (command === "stop") { stop(); } else if (command === "restart") { restart(); } else if (command === "status") { status(); } else if (command === "update") { update(); } else { console.error(`Unknown command: ${command}`); process.exit(1); } function start() { const pid = readPid(); if (pid && isRunning(pid)) { console.log(`RC RaceCore server already running with PID ${pid}`); console.log(`Logs: ${OUT_LOG}`); return; } cleanupStalePid(); const stdoutFd = fs.openSync(OUT_LOG, "a"); const stderrFd = fs.openSync(ERR_LOG, "a"); const child = spawn(process.execPath, ["server.js"], { cwd: ROOT, detached: true, windowsHide: true, stdio: ["ignore", stdoutFd, stderrFd], env: { ...process.env, RC_TIMING_PID_FILE: PID_FILE, }, }); child.unref(); fs.writeFileSync(PID_FILE, `${child.pid}\n`); console.log(`RC RaceCore server started in background`); console.log(`PID: ${child.pid}`); console.log(`Logs: ${OUT_LOG}`); } async function stop() { const pid = readPid(); if (!pid) { console.log("RC RaceCore server is not running"); return; } if (!isRunning(pid)) { cleanupStalePid(); console.log("Removed stale pid file"); return; } try { process.kill(pid, "SIGTERM"); } catch (error) { console.error(`Could not stop PID ${pid}: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } const stopped = await waitForExit(pid, 5000); if (!stopped && process.platform !== "win32") { try { process.kill(pid, "SIGKILL"); } catch { // ignore } await waitForExit(pid, 1000); } cleanupStalePid(); console.log(`RC RaceCore server stopped`); } async function restart() { await stop(); start(); } async function update() { const log = (line) => { const stamped = `[${new Date().toISOString()}] ${line}\n`; process.stdout.write(stamped); try { fs.appendFileSync(UPDATE_LOG, stamped); } catch { // best-effort - the console line above is the fallback record } }; log("Stopping server for update..."); await stop(); // --ff-only refuses to update rather than creating a merge commit or hitting a conflict a // trackside user has no way to resolve - if the local branch has diverged, this is meant to // fail loudly (and still start the server back up with the code that was already there) so a // human looks at it, not silently rewrite history on a live installation. log("Pulling latest code (git pull --ff-only)..."); const pull = spawnSync("git", ["pull", "--ff-only"], { cwd: ROOT, encoding: "utf8" }); log(pull.stdout || ""); if (pull.stderr) log(pull.stderr); if (pull.status !== 0) { log("git pull failed or was not a fast-forward - starting the server back up with the code already installed, no changes made."); start(); return; } log("Installing dependencies (npm install)..."); const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; const install = spawnSync(npmCommand, ["install", "--no-audit", "--no-fund"], { cwd: ROOT, encoding: "utf8" }); log(install.stdout || ""); if (install.stderr) log(install.stderr); if (install.status !== 0) { log("npm install failed - starting the server anyway with whatever dependencies are currently installed."); } log("Starting server..."); start(); log("Update finished."); } function status() { const pid = readPid(); if (pid && isRunning(pid)) { console.log(`RC RaceCore server is running`); console.log(`PID: ${pid}`); console.log(`Logs: ${OUT_LOG}`); return; } cleanupStalePid(); console.log("RC RaceCore server is not running"); } function readPid() { if (!fs.existsSync(PID_FILE)) { return null; } const raw = fs.readFileSync(PID_FILE, "utf8").trim(); const pid = Number(raw); return Number.isFinite(pid) && pid > 0 ? pid : null; } function isRunning(pid) { try { process.kill(pid, 0); return true; } catch { return false; } } function cleanupStalePid() { const pid = readPid(); if (!pid || !isRunning(pid)) { fs.rmSync(PID_FILE, { force: true }); } } function waitForExit(pid, timeoutMs) { return new Promise((resolve) => { const started = Date.now(); const timer = setInterval(() => { if (!isRunning(pid)) { clearInterval(timer); resolve(true); return; } if (Date.now() - started >= timeoutMs) { clearInterval(timer); resolve(false); } }, 150); }); }