Files
nntrivi2001 f460fff406 feat: pet chat window replaces the Sagi panel; Windows pane attach no longer crashes the server
- 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.
2026-10-16 10:27:22 +07:00

95 lines
3.3 KiB
JavaScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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");
// The same log `agenthub start` writes: without it every server started by
// an update ran with no stdout/stderr at all, and a crash left no trace.
const logFile = path.join(root, "data", "agenthub-server.log");
const helper = spawnHelper(process.execPath, [helperPath, String(process.pid), entry, logFile], {
cwd: root,
detached: true,
stdio: "ignore",
env: process.env,
});
helper.unref();
kill();
return "helper";
}
module.exports = { scheduleRestart, owningUnit };