Files
Live_RC/scripts/serverctl.js
larssand 4c463485e0 Add in-app update checker and one-click updater
Settings now shows when a newer version is available, with the real
version number read from the remote's package.json (via the git
binary already required for this deployment model, so it works
against any remote, not just GitHub), and an "Uppdatera nu" button
that stops the server, pulls, reinstalls dependencies and restarts
automatically - warning explicitly if a race is currently active.
The existing app-version poller already reloads the page once a
restart is detected, so no new client-side polling was needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:05:13 +02:00

199 lines
5.1 KiB
JavaScript

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);
});
}