f460fff406
- Sagi: clicking the pet opens its chat window directly (PetChat + SagiInfo status strip, Needs you, Mute / Clear alerts); SagiPanel removed. - Chat view answers Claude's unnumbered folder-trust prompt (parseBareMenu). - pty-attach: resize/write after a Windows attach exited, and node-pty's pipe errors, no longer throw out of the WebSocket handler. - restart-helper: the server started by an update logs to data/agenthub-server.log instead of discarding its output.
117 lines
4.4 KiB
JavaScript
117 lines
4.4 KiB
JavaScript
/**
|
|
* @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ĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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/);
|
|
});
|
|
});
|