fix(9router): a sudo update gives root's npm its own cache
The AgentHub unit makes /root inaccessible (InaccessiblePaths=), so root's npm failed at mkdir /root/.npm with ENOENT (exit 254). A sudo run now passes --cache /var/cache/agenthub-9router-npm; the sudoers rule names that argv.
This commit is contained in:
+1
-1
@@ -3059,7 +3059,7 @@ A **proxy profile** is an endpoint that stands in front of Claude Code or Codex
|
|||||||
|
|
||||||
**Update 9router** (`server/lib/nine-router-update.js`; the AI Proxy page's button beside Config 9router, an admin's only — `/ai-proxies/9router` and `/ai-routers/9router` are in `ADMIN_WRITE_PREFIXES`). `GET /api/ai-proxies/9router/update` answers `{ installed, version, latest, updateAvailable, running, managedBy: "systemd"|"process"|null, blocker: {code, message, command?}|null, job }`. `POST` starts one job (202; `EBUSY` while one runs, else 409 with the blocker's code — checked BEFORE anything is stopped) of three steps, each `pending|running|done|failed|skipped`:
|
**Update 9router** (`server/lib/nine-router-update.js`; the AI Proxy page's button beside Config 9router, an admin's only — `/ai-proxies/9router` and `/ai-routers/9router` are in `ADMIN_WRITE_PREFIXES`). `GET /api/ai-proxies/9router/update` answers `{ installed, version, latest, updateAvailable, running, managedBy: "systemd"|"process"|null, blocker: {code, message, command?}|null, job }`. `POST` starts one job (202; `EBUSY` while one runs, else 409 with the blocker's code — checked BEFORE anything is stopped) of three steps, each `pending|running|done|failed|skipped`:
|
||||||
1. **shutdown** — a running `9router.service` user unit is `systemctl --user stop`ped; otherwise this account's processes whose argv names the install's `cli.js` get SIGTERM, then they and their descendants SIGKILL after 5 s; the step waits until the port is free. Skipped when nothing ran.
|
1. **shutdown** — a running `9router.service` user unit is `systemctl --user stop`ped; otherwise this account's processes whose argv names the install's `cli.js` get SIGTERM, then they and their descendants SIGKILL after 5 s; the step waits until the port is free. Skipped when nothing ran.
|
||||||
2. **update** — `npm install -g --prefix <the install's own prefix> --prefer-online 9router@latest`; a prefix this account cannot write runs it under `sudo -n`, only when `sudo -n <npm> --version` succeeds (else `ENOPERM` with `command: "sudo npm install -g 9router@latest"`).
|
2. **update** — `npm install -g --prefix <the install's own prefix> --prefer-online 9router@latest`; a prefix this account cannot write runs it under `sudo -n` with `--cache /var/cache/agenthub-9router-npm` (root's HOME is inaccessible inside the AgentHub unit; the sudoers rule must name that exact argv), only when `sudo -n <npm> --version` succeeds (else `ENOPERM` with `command: "sudo npm install -g 9router@latest"`).
|
||||||
3. **relaunch** — only when 9router was running before: `systemctl --user start 9router.service`, or `node cli.js --tray --no-browser --skip-update -p <old port> [-H <old host>]` detached in its own `systemd-run --user --scope` (plain detached spawn without one); done once the port answers. Runs even after a failed update.
|
3. **relaunch** — only when 9router was running before: `systemctl --user start 9router.service`, or `node cli.js --tray --no-browser --skip-update -p <old port> [-H <old host>]` detached in its own `systemd-run --user --scope` (plain detached spawn without one); done once the port answers. Runs even after a failed update.
|
||||||
|
|
||||||
**`POST /api/ai-proxies/test`** answers the question saving cannot: does this endpoint, with this key, actually serve this model — for each CLI? It sends, in parallel and with a 15 s timeout each, one Anthropic-style message capped at a single output token to `<baseUrl>/v1/messages` (what Claude Code calls) and one OpenAI Responses call (`max_output_tokens: 16`) to `<baseUrl>/v1/responses` (what Codex calls). A proxy that answers is a `200` here whatever it replies: `{ claude: { ok, status, message }, codex: { ok, status, message } }`, each carrying the proxy's own words (truncated to 300 characters), `status` `null` when nothing answered at all. A proxy that serves only one wire shape shows one green line and one red. Only bad input is a 4xx.
|
**`POST /api/ai-proxies/test`** answers the question saving cannot: does this endpoint, with this key, actually serve this model — for each CLI? It sends, in parallel and with a 15 s timeout each, one Anthropic-style message capped at a single output token to `<baseUrl>/v1/messages` (what Claude Code calls) and one OpenAI Responses call (`max_output_tokens: 16`) to `<baseUrl>/v1/responses` (what Codex calls). A proxy that answers is a `200` here whatever it replies: `{ claude: { ok, status, message }, codex: { ok, status, message } }`, each carrying the proxy's own words (truncated to 300 characters), `status` `null` when nothing answered at all. A proxy that serves only one wire shape shows one green line and one red. Only bad input is a 4xx.
|
||||||
|
|||||||
@@ -104,10 +104,14 @@ function killFakes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startFake(port) {
|
function startFake(port) {
|
||||||
const child = spawn(process.execPath, [path.join(PKG_DIR, "cli.js"), "--tray", "-p", String(port)], {
|
const child = spawn(
|
||||||
stdio: "ignore",
|
process.execPath,
|
||||||
detached: true,
|
[path.join(PKG_DIR, "cli.js"), "--tray", "-p", String(port)],
|
||||||
});
|
{
|
||||||
|
stdio: "ignore",
|
||||||
|
detached: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
child.unref();
|
child.unref();
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
@@ -128,7 +132,9 @@ before(async () => {
|
|||||||
fs.symlinkSync("../lib/node_modules/9router/cli.js", path.join(BIN_DIR, "9router"));
|
fs.symlinkSync("../lib/node_modules/9router/cli.js", path.join(BIN_DIR, "9router"));
|
||||||
fs.writeFileSync(path.join(NPM_DIR, "npm"), NPM, { mode: 0o755 });
|
fs.writeFileSync(path.join(NPM_DIR, "npm"), NPM, { mode: 0o755 });
|
||||||
// Only the fake bins, node's own dir and the basics — never /usr/local/bin.
|
// Only the fake bins, node's own dir and the basics — never /usr/local/bin.
|
||||||
process.env.PATH = [BIN_DIR, NPM_DIR, path.dirname(process.execPath), "/bin", "/usr/bin"].join(":");
|
process.env.PATH = [BIN_DIR, NPM_DIR, path.dirname(process.execPath), "/bin", "/usr/bin"].join(
|
||||||
|
":"
|
||||||
|
);
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use("/api/ai-proxies", router);
|
app.use("/api/ai-proxies", router);
|
||||||
@@ -141,7 +147,10 @@ before(async () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
killFakes();
|
killFakes();
|
||||||
update.__reset();
|
update.__reset();
|
||||||
fs.writeFileSync(path.join(PKG_DIR, "package.json"), JSON.stringify({ name: "9router", version: "1.0.0" }));
|
fs.writeFileSync(
|
||||||
|
path.join(PKG_DIR, "package.json"),
|
||||||
|
JSON.stringify({ name: "9router", version: "1.0.0" })
|
||||||
|
);
|
||||||
fs.rmSync(path.join(TMP, "npm-fail"), { force: true });
|
fs.rmSync(path.join(TMP, "npm-fail"), { force: true });
|
||||||
update.__setSeams({
|
update.__setSeams({
|
||||||
useSystemd: false,
|
useSystemd: false,
|
||||||
@@ -181,17 +190,43 @@ describe("nine-router-update", () => {
|
|||||||
fs.chmodSync(path.join(PREFIX, "lib", "node_modules"), 0o555);
|
fs.chmodSync(path.join(PREFIX, "lib", "node_modules"), 0o555);
|
||||||
update.__setSeams({
|
update.__setSeams({
|
||||||
exec: async (file, args) =>
|
exec: async (file, args) =>
|
||||||
file === "sudo" ? { code: 1, stdout: "", stderr: "a password is required" } : { code: 0, stdout: "", stderr: "" },
|
file === "sudo"
|
||||||
|
? { code: 1, stdout: "", stderr: "a password is required" }
|
||||||
|
: { code: 0, stdout: "", stderr: "" },
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
if (process.getuid?.() === 0) return; // root writes anyway
|
if (process.getuid?.() === 0) return; // root writes anyway
|
||||||
await assert.rejects(update.start(), (e) => e.code === "ENOPERM" && /sudo npm install -g 9router@latest/.test(e.command));
|
await assert.rejects(
|
||||||
|
update.start(),
|
||||||
|
(e) => e.code === "ENOPERM" && /sudo npm install -g 9router@latest/.test(e.command)
|
||||||
|
);
|
||||||
assert.equal(update.current(), null, "no job, so 9router was never stopped");
|
assert.equal(update.current(), null, "no job, so 9router was never stopped");
|
||||||
} finally {
|
} finally {
|
||||||
fs.chmodSync(path.join(PREFIX, "lib", "node_modules"), 0o755);
|
fs.chmodSync(path.join(PREFIX, "lib", "node_modules"), 0o755);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("a sudo update gives root's npm its own cache, never root's HOME", async () => {
|
||||||
|
if (process.getuid?.() === 0) return; // root writes the prefix directly
|
||||||
|
fs.chmodSync(path.join(PREFIX, "lib", "node_modules"), 0o555);
|
||||||
|
// A fake sudo first on PATH: drops `-n --` and runs the rest as us.
|
||||||
|
fs.writeFileSync(path.join(NPM_DIR, "sudo"), '#!/bin/sh\nshift; shift; exec "$@"\n', {
|
||||||
|
mode: 0o755,
|
||||||
|
});
|
||||||
|
update.__setSeams({ exec: async () => ({ code: 0, stdout: "", stderr: "" }) });
|
||||||
|
try {
|
||||||
|
await update.start();
|
||||||
|
const done = await finished();
|
||||||
|
assert.match(
|
||||||
|
done.log,
|
||||||
|
/\$ sudo -n -- \S+ install -g --prefix \S+ --prefer-online --cache \/var\/cache\/agenthub-9router-npm 9router@latest/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.chmodSync(path.join(PREFIX, "lib", "node_modules"), 0o755);
|
||||||
|
fs.rmSync(path.join(NPM_DIR, "sudo"), { force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("stops a running 9router (child included), updates it, and starts it again hidden", async () => {
|
it("stops a running 9router (child included), updates it, and starts it again hidden", async () => {
|
||||||
const port = await freePort();
|
const port = await freePort();
|
||||||
startFake(port);
|
startFake(port);
|
||||||
@@ -204,7 +239,10 @@ describe("nine-router-update", () => {
|
|||||||
assert.equal(job.managedBy, "process");
|
assert.equal(job.managedBy, "process");
|
||||||
const done = await finished();
|
const done = await finished();
|
||||||
assert.equal(done.status, "done", done.log + JSON.stringify(done.steps));
|
assert.equal(done.status, "done", done.log + JSON.stringify(done.steps));
|
||||||
assert.deepEqual(done.steps.map((s) => s.status), ["done", "done", "done"]);
|
assert.deepEqual(
|
||||||
|
done.steps.map((s) => s.status),
|
||||||
|
["done", "done", "done"]
|
||||||
|
);
|
||||||
assert.equal(done.from, "1.0.0");
|
assert.equal(done.from, "1.0.0");
|
||||||
assert.equal(done.to, "9.9.9");
|
assert.equal(done.to, "9.9.9");
|
||||||
for (const pid of before) assert.ok(!fakePids().includes(pid), `old pid ${pid} is gone`);
|
for (const pid of before) assert.ok(!fakePids().includes(pid), `old pid ${pid} is gone`);
|
||||||
@@ -213,14 +251,18 @@ describe("nine-router-update", () => {
|
|||||||
const argv = fakePids()
|
const argv = fakePids()
|
||||||
.map((p) => fs.readFileSync(`/proc/${p}/cmdline`, "utf8").split("\0"))
|
.map((p) => fs.readFileSync(`/proc/${p}/cmdline`, "utf8").split("\0"))
|
||||||
.find((a) => a.includes(path.join(PKG_DIR, "cli.js")));
|
.find((a) => a.includes(path.join(PKG_DIR, "cli.js")));
|
||||||
for (const flag of ["--tray", "--no-browser", "--skip-update"]) assert.ok(argv.includes(flag), flag);
|
for (const flag of ["--tray", "--no-browser", "--skip-update"])
|
||||||
|
assert.ok(argv.includes(flag), flag);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("leaves 9router off when it was not running", async () => {
|
it("leaves 9router off when it was not running", async () => {
|
||||||
await update.start();
|
await update.start();
|
||||||
const done = await finished();
|
const done = await finished();
|
||||||
assert.equal(done.status, "done");
|
assert.equal(done.status, "done");
|
||||||
assert.deepEqual(done.steps.map((s) => s.status), ["skipped", "done", "skipped"]);
|
assert.deepEqual(
|
||||||
|
done.steps.map((s) => s.status),
|
||||||
|
["skipped", "done", "skipped"]
|
||||||
|
);
|
||||||
assert.equal(fakePids().length, 0);
|
assert.equal(fakePids().length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +274,10 @@ describe("nine-router-update", () => {
|
|||||||
await update.start();
|
await update.start();
|
||||||
const done = await finished();
|
const done = await finished();
|
||||||
assert.equal(done.status, "failed");
|
assert.equal(done.status, "failed");
|
||||||
assert.deepEqual(done.steps.map((s) => s.status), ["done", "failed", "done"]);
|
assert.deepEqual(
|
||||||
|
done.steps.map((s) => s.status),
|
||||||
|
["done", "failed", "done"]
|
||||||
|
);
|
||||||
assert.match(done.log, /boom/);
|
assert.match(done.log, /boom/);
|
||||||
assert.ok(await portOpen(port));
|
assert.ok(await portOpen(port));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ const timing = {
|
|||||||
portUpMs: 45000,
|
portUpMs: 45000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeErr = (code, message, extra = {}) =>
|
const makeErr = (code, message, extra = {}) => Object.assign(new Error(message), { code }, extra);
|
||||||
Object.assign(new Error(message), { code }, extra);
|
|
||||||
|
|
||||||
/** execFile as a promise that never rejects: `{code, stdout, stderr}`. */
|
/** execFile as a promise that never rejects: `{code, stdout, stderr}`. */
|
||||||
const realExec = (file, args, opts = {}) =>
|
const realExec = (file, args, opts = {}) =>
|
||||||
@@ -137,11 +136,27 @@ function canWrite(dir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const npmArgs = (prefix) => ["install", "-g", "--prefix", prefix, "--prefer-online", `${PKG}@latest`];
|
// Root's npm cache for a sudo run. Never root's HOME: the AgentHub unit makes
|
||||||
|
// `/root` inaccessible (`InaccessiblePaths=`), so `mkdir /root/.npm` fails with
|
||||||
|
// ENOENT (npm exit 254). A root-owned path, never one in /tmp a user could plant.
|
||||||
|
const SUDO_NPM_CACHE = "/var/cache/agenthub-9router-npm";
|
||||||
|
|
||||||
|
const npmArgs = (prefix, sudo = false) => [
|
||||||
|
"install",
|
||||||
|
"-g",
|
||||||
|
"--prefix",
|
||||||
|
prefix,
|
||||||
|
"--prefer-online",
|
||||||
|
...(sudo ? ["--cache", SUDO_NPM_CACHE] : []),
|
||||||
|
`${PKG}@latest`,
|
||||||
|
];
|
||||||
|
|
||||||
/** `direct` (we can write the prefix), `sudo` (passwordless sudo allows it), or null. */
|
/** `direct` (we can write the prefix), `sudo` (passwordless sudo allows it), or null. */
|
||||||
async function installMode(loc, npm) {
|
async function installMode(loc, npm) {
|
||||||
if (canWrite(path.join(loc.prefix, "lib", "node_modules")) && canWrite(path.join(loc.prefix, "bin")))
|
if (
|
||||||
|
canWrite(path.join(loc.prefix, "lib", "node_modules")) &&
|
||||||
|
canWrite(path.join(loc.prefix, "bin"))
|
||||||
|
)
|
||||||
return "direct";
|
return "direct";
|
||||||
// `sudo -l` would answer yes for a rule that still asks for a password, so
|
// `sudo -l` would answer yes for a rule that still asks for a password, so
|
||||||
// the probe is a harmless real run of the same npm.
|
// the probe is a harmless real run of the same npm.
|
||||||
@@ -189,8 +204,9 @@ async function findProcesses(loc) {
|
|||||||
// Its server renames itself (`next-server`), so children are found by parent,
|
// Its server renames itself (`next-server`), so children are found by parent,
|
||||||
// plus anything still running from inside the install.
|
// plus anything still running from inside the install.
|
||||||
const all = new Set(roots);
|
const all = new Set(roots);
|
||||||
for (const { pid, argv } of rows) if (argv.some((a) => a.startsWith(loc.pkgDir + path.sep))) all.add(pid);
|
for (const { pid, argv } of rows)
|
||||||
for (let grew = true; grew; ) {
|
if (argv.some((a) => a.startsWith(loc.pkgDir + path.sep))) all.add(pid);
|
||||||
|
for (let grew = true; grew;) {
|
||||||
grew = false;
|
grew = false;
|
||||||
for (const { pid, ppid } of rows)
|
for (const { pid, ppid } of rows)
|
||||||
if (all.has(ppid) && !all.has(pid)) {
|
if (all.has(ppid) && !all.has(pid)) {
|
||||||
@@ -243,7 +259,8 @@ const signal = (pids, sig) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function latestVersion({ fresh = false } = {}) {
|
async function latestVersion({ fresh = false } = {}) {
|
||||||
if (!fresh && latestCache && Date.now() - latestCache.at < LATEST_CACHE_MS) return latestCache.version;
|
if (!fresh && latestCache && Date.now() - latestCache.at < LATEST_CACHE_MS)
|
||||||
|
return latestCache.version;
|
||||||
let version = null;
|
let version = null;
|
||||||
try {
|
try {
|
||||||
version = await fetchLatestImpl();
|
version = await fetchLatestImpl();
|
||||||
@@ -265,20 +282,37 @@ const view = (j) => (j ? JSON.parse(JSON.stringify(j)) : null);
|
|||||||
async function status({ fresh = false } = {}) {
|
async function status({ fresh = false } = {}) {
|
||||||
const loc = locate();
|
const loc = locate();
|
||||||
if (!loc) {
|
if (!loc) {
|
||||||
return { installed: false, version: null, latest: null, updateAvailable: false, running: false, managedBy: null, blocker: { code: "ENOTINSTALLED", message: "9router is not installed on this machine" }, job: view(job) };
|
return {
|
||||||
|
installed: false,
|
||||||
|
version: null,
|
||||||
|
latest: null,
|
||||||
|
updateAvailable: false,
|
||||||
|
running: false,
|
||||||
|
managedBy: null,
|
||||||
|
blocker: { code: "ENOTINSTALLED", message: "9router is not installed on this machine" },
|
||||||
|
job: view(job),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const [latest, unit, procs] = await Promise.all([
|
const [latest, unit, procs] = await Promise.all([
|
||||||
latestVersion({ fresh }),
|
latestVersion({ fresh }),
|
||||||
unitActive(),
|
unitActive(),
|
||||||
findProcesses(loc),
|
findProcesses(loc),
|
||||||
]);
|
]);
|
||||||
const updateAvailable = Boolean(latest && loc.version && compareVersions(latest, loc.version) > 0);
|
const updateAvailable = Boolean(
|
||||||
|
latest && loc.version && compareVersions(latest, loc.version) > 0
|
||||||
|
);
|
||||||
const manual = `sudo npm install -g ${PKG}@latest`;
|
const manual = `sudo npm install -g ${PKG}@latest`;
|
||||||
let blocker = null;
|
let blocker = null;
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
blocker = { code: "EUNSUPPORTED", message: "updating 9router from AgentHub is not supported on Windows" };
|
blocker = {
|
||||||
|
code: "EUNSUPPORTED",
|
||||||
|
message: "updating 9router from AgentHub is not supported on Windows",
|
||||||
|
};
|
||||||
} else if (!loc.prefix) {
|
} else if (!loc.prefix) {
|
||||||
blocker = { code: "EUNSUPPORTED", message: `9router at ${loc.pkgDir} is not an npm global install` };
|
blocker = {
|
||||||
|
code: "EUNSUPPORTED",
|
||||||
|
message: `9router at ${loc.pkgDir} is not an npm global install`,
|
||||||
|
};
|
||||||
} else if (!updateAvailable) {
|
} else if (!updateAvailable) {
|
||||||
blocker = latest
|
blocker = latest
|
||||||
? { code: "EUPTODATE", message: `9router ${loc.version} is the latest version` }
|
? { code: "EUPTODATE", message: `9router ${loc.version} is the latest version` }
|
||||||
@@ -370,7 +404,15 @@ function runInstall(argv) {
|
|||||||
async function launchHidden(loc, procs) {
|
async function launchHidden(loc, procs) {
|
||||||
const node = aiTools.which("node", versions.jobPath()) || process.execPath;
|
const node = aiTools.which("node", versions.jobPath()) || process.execPath;
|
||||||
const port = procs.port || DEFAULT_PORT;
|
const port = procs.port || DEFAULT_PORT;
|
||||||
const args = ["--dns-result-order=ipv4first", loc.cli, "--tray", "--no-browser", "--skip-update", "-p", String(port)];
|
const args = [
|
||||||
|
"--dns-result-order=ipv4first",
|
||||||
|
loc.cli,
|
||||||
|
"--tray",
|
||||||
|
"--no-browser",
|
||||||
|
"--skip-update",
|
||||||
|
"-p",
|
||||||
|
String(port),
|
||||||
|
];
|
||||||
if (procs.host) args.push("-H", procs.host);
|
if (procs.host) args.push("-H", procs.host);
|
||||||
const env = { ...userEnv(), ...desktopEnv() };
|
const env = { ...userEnv(), ...desktopEnv() };
|
||||||
delete env.TRAY_MODE;
|
delete env.TRAY_MODE;
|
||||||
@@ -381,7 +423,16 @@ async function launchHidden(loc, procs) {
|
|||||||
};
|
};
|
||||||
const systemdRun = useSystemd && env.XDG_RUNTIME_DIR && aiTools.which("systemd-run", env.PATH);
|
const systemdRun = useSystemd && env.XDG_RUNTIME_DIR && aiTools.which("systemd-run", env.PATH);
|
||||||
if (systemdRun) {
|
if (systemdRun) {
|
||||||
const child = spawnDetached(systemdRun, ["--user", "--scope", "--quiet", "--collect", "--description=9Router", "--", node, ...args]);
|
const child = spawnDetached(systemdRun, [
|
||||||
|
"--user",
|
||||||
|
"--scope",
|
||||||
|
"--quiet",
|
||||||
|
"--collect",
|
||||||
|
"--description=9Router",
|
||||||
|
"--",
|
||||||
|
node,
|
||||||
|
...args,
|
||||||
|
]);
|
||||||
// A scope that cannot be created exits at once; anything else is 9router running.
|
// A scope that cannot be created exits at once; anything else is 9router running.
|
||||||
const failed = await new Promise((resolve) => {
|
const failed = await new Promise((resolve) => {
|
||||||
const t = setTimeout(() => resolve(false), 1500);
|
const t = setTimeout(() => resolve(false), 1500);
|
||||||
@@ -404,7 +455,10 @@ async function launchHidden(loc, procs) {
|
|||||||
async function doRelaunch(loc, unit, procs) {
|
async function doRelaunch(loc, unit, procs) {
|
||||||
let port = procs.port || DEFAULT_PORT;
|
let port = procs.port || DEFAULT_PORT;
|
||||||
if (unit) {
|
if (unit) {
|
||||||
const r = await exec("systemctl", ["--user", "start", UNIT], { env: userEnv(), timeout: 30000 });
|
const r = await exec("systemctl", ["--user", "start", UNIT], {
|
||||||
|
env: userEnv(),
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
if (r.code !== 0) throw makeErr("ESTART", `systemctl --user start ${UNIT}: ${r.stderr.trim()}`);
|
if (r.code !== 0) throw makeErr("ESTART", `systemctl --user start ${UNIT}: ${r.stderr.trim()}`);
|
||||||
} else {
|
} else {
|
||||||
port = await launchHidden(loc, procs);
|
port = await launchHidden(loc, procs);
|
||||||
@@ -431,7 +485,7 @@ async function runJob(loc, npm, mode, unit, procs) {
|
|||||||
}
|
}
|
||||||
if (!failed) {
|
if (!failed) {
|
||||||
setStep("update", "running");
|
setStep("update", "running");
|
||||||
const argv = [npm, ...npmArgs(loc.prefix)];
|
const argv = [npm, ...npmArgs(loc.prefix, mode === "sudo")];
|
||||||
const code = await runInstall(mode === "sudo" ? ["sudo", "-n", "--", ...argv] : argv);
|
const code = await runInstall(mode === "sudo" ? ["sudo", "-n", "--", ...argv] : argv);
|
||||||
const after = locate();
|
const after = locate();
|
||||||
job.to = after?.version || null;
|
job.to = after?.version || null;
|
||||||
@@ -462,7 +516,12 @@ async function start() {
|
|||||||
if (job?.status === "running") throw makeErr("EBUSY", "a 9router update is already running");
|
if (job?.status === "running") throw makeErr("EBUSY", "a 9router update is already running");
|
||||||
const s = await status({ fresh: true });
|
const s = await status({ fresh: true });
|
||||||
if (job?.status === "running") throw makeErr("EBUSY", "a 9router update is already running");
|
if (job?.status === "running") throw makeErr("EBUSY", "a 9router update is already running");
|
||||||
if (s.blocker) throw makeErr(s.blocker.code, s.blocker.message, s.blocker.command ? { command: s.blocker.command } : {});
|
if (s.blocker)
|
||||||
|
throw makeErr(
|
||||||
|
s.blocker.code,
|
||||||
|
s.blocker.message,
|
||||||
|
s.blocker.command ? { command: s.blocker.command } : {}
|
||||||
|
);
|
||||||
const loc = locate();
|
const loc = locate();
|
||||||
const npm = versions.npmBin();
|
const npm = versions.npmBin();
|
||||||
const mode = await installMode(loc, npm);
|
const mode = await installMode(loc, npm);
|
||||||
|
|||||||
Reference in New Issue
Block a user