/** * @file self-restart.test.js * @description After an update, a server our service unit runs is restarted * by the unit, never by a detached replacement: v2.7.2 on the S3 system unit * left the replacement holding the port while `Restart=always` started the * unit every 3 s, each start exiting "already running". Without a unit, the * detached helper's replacement server logs to `data/agenthub-server.log`. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { execFileSync } = require("node:child_process"); const { scheduleRestart, owningUnit } = require("../lib/self-restart"); function deps(unit, restartAnswer = "restarted") { const calls = { restart: [], kill: 0, spawn: 0 }; return { calls, unit, svc: () => ({ restartUserService: (name) => { calls.restart.push(name); return restartAnswer; }, }), kill: () => calls.kill++, spawnHelper: (_exe, args) => { calls.spawn++; calls.helperArgs = args; return { unref() {} }; }, }; } describe("self-restart", () => { it("tells which unit owns this process, from its cgroup or launchd label", () => { const linux = (text) => owningUnit({ platform: "linux", cgroup: () => text }); assert.equal(linux("0::/system.slice/agenthub.service\n"), "systemd"); assert.equal( linux("0::/user.slice/user-1001.slice/user@1001.service/app.slice/agenthub.service\n"), "systemd" ); assert.equal(linux("0::/user.slice/user-1001.slice/session-3.scope\n"), null); assert.equal(linux("0::/system.slice/agenthub.service.d/x\n"), null); assert.equal( owningUnit({ platform: "linux", cgroup: () => { throw new Error("no /proc"); }, }), null ); assert.equal( owningUnit({ platform: "darwin", env: { XPC_SERVICE_NAME: "io.smartgift.agenthub" } }), "launchd" ); assert.equal(owningUnit({ platform: "darwin", env: {} }), null); }); it("asks systemd to restart its unit and starts no replacement", () => { const d = deps("systemd"); assert.equal(scheduleRestart("/r", d), "service"); assert.deepEqual(d.calls.restart, ["agenthub"]); assert.equal(d.calls.spawn, 0); assert.equal(d.calls.kill, 0, "systemd's restart job stops this process"); }); it("exits for Restart=always when systemd cannot be asked — still no replacement", () => { const d = deps("systemd", "failed"); assert.equal(scheduleRestart("/r", d), "exit"); assert.equal(d.calls.kill, 1); assert.equal(d.calls.spawn, 0); }); it("exits under launchd, whose KeepAlive starts the new code", () => { const d = deps("launchd"); assert.equal(scheduleRestart("/r", d), "exit"); assert.equal(d.calls.kill, 1); assert.equal(d.calls.spawn, 0); assert.deepEqual(d.calls.restart, []); }); it("hands over to the detached helper when no unit owns the server", () => { const d = deps(null); assert.equal(scheduleRestart("/r", d), "helper"); assert.equal(d.calls.spawn, 1); assert.equal(d.calls.kill, 1); // The new server logs where `agenthub start` does — never into nothing. assert.equal(d.calls.helperArgs.at(-1), path.join("/r", "data", "agenthub-server.log")); }); it("restart-helper appends the new server's stdout and stderr to the log", async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "restart-helper-")); const entry = path.join(dir, "entry.js"); const log = path.join(dir, "data", "agenthub-server.log"); fs.writeFileSync(entry, 'console.log("up"); throw new Error("boom");\n'); const helper = path.join(__dirname, "..", "..", "scripts", "restart-helper.js"); // pid 0x7fffffff is never alive, so the helper restarts at once. execFileSync(process.execPath, [helper, "2147483647", entry, log]); const deadline = Date.now() + 5000; let text = ""; while (Date.now() < deadline && !/boom/.test(text)) { await new Promise((r) => setTimeout(r, 50)); text = fs.existsSync(log) ? fs.readFileSync(log, "utf8") : ""; } fs.rmSync(dir, { recursive: true, force: true }); assert.match(text, /restart-helper: old server 2147483647 gone/); assert.match(text, /^up$/m); assert.match(text, /Error: boom/); }); });