/** * @file Triggers the one self-restart path this dashboard has: after * `POST /api/updates/apply` fast-forwards and rebuilds the checkout, the * running process needs to relaunch itself on the new code. Reuses the * existing SIGTERM graceful-shutdown path in `server/index.js` (closes * websockets, drains the HTTP server, closes the DB) instead of duplicating * it, and hands the "wait for the old PID to actually die, then spawn the * replacement" job to a detached helper script — the dying process can't do * that itself without racing its own replacement for the port. * * A server our service unit runs (systemd `agenthub.service`, system or user; * the launchd job) is restarted BY that manager instead: a detached * replacement would hold the port while `Restart=always` starts the unit * again every few seconds, each start exiting "already running" — forever. * @author Nguyễn Ngọc Trí Vĩ */ const fs = require("fs"); const path = require("path"); const { spawn } = require("child_process"); const UNIT = "agenthub"; /** * "systemd" when this process lives in our unit's cgroup, "launchd" when it * is our LaunchAgent job, else null. Pure reads: no process is spawned. */ function owningUnit({ platform = process.platform, cgroup = () => fs.readFileSync("/proc/self/cgroup", "utf8"), env = process.env, } = {}) { if (platform === "darwin") { return env.XPC_SERVICE_NAME === `io.smartgift.${UNIT}` ? "launchd" : null; } try { return cgroup() .split("\n") .some((l) => l.trim().endsWith(`/${UNIT}.service`)) ? "systemd" : null; } catch { return null; } } /** * @param {string} [root] repo root the helper should run from. * @returns {"service"|"exit"|"helper"} how the restart was handed over. */ function scheduleRestart(root = path.join(__dirname, "..", ".."), deps = {}) { const { unit = owningUnit(), svc = () => require("../../scripts/service-manager"), kill = () => process.kill(process.pid, "SIGTERM"), spawnHelper = spawn, } = deps; // systemd: one non-blocking restart job (sudo -n for the system unit). If // that cannot be asked, a plain exit still comes back — the unit is // Restart=always — but never with a second server beside it. if (unit === "systemd") { let restarted = false; try { restarted = svc().restartUserService(UNIT) === "restarted"; } catch { /* fall through to the exit */ } if (restarted) return "service"; kill(); return "exit"; } // launchd: KeepAlive restarts the job on exit; a `bootout` from inside the // job would wait for this very process. if (unit === "launchd") { kill(); return "exit"; } const helperPath = path.join(root, "scripts", "restart-helper.js"); const entry = process.argv[1] || path.join(root, "server", "index.js"); const helper = spawnHelper(process.execPath, [helperPath, String(process.pid), entry], { cwd: root, detached: true, stdio: "ignore", env: process.env, }); helper.unref(); kill(); return "helper"; } module.exports = { scheduleRestart, owningUnit };