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:
@@ -104,10 +104,14 @@ function killFakes() {
|
||||
}
|
||||
|
||||
function startFake(port) {
|
||||
const child = spawn(process.execPath, [path.join(PKG_DIR, "cli.js"), "--tray", "-p", String(port)], {
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
});
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[path.join(PKG_DIR, "cli.js"), "--tray", "-p", String(port)],
|
||||
{
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
}
|
||||
);
|
||||
child.unref();
|
||||
return child;
|
||||
}
|
||||
@@ -128,7 +132,9 @@ before(async () => {
|
||||
fs.symlinkSync("../lib/node_modules/9router/cli.js", path.join(BIN_DIR, "9router"));
|
||||
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.
|
||||
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();
|
||||
app.use(express.json());
|
||||
app.use("/api/ai-proxies", router);
|
||||
@@ -141,7 +147,10 @@ before(async () => {
|
||||
beforeEach(() => {
|
||||
killFakes();
|
||||
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 });
|
||||
update.__setSeams({
|
||||
useSystemd: false,
|
||||
@@ -181,17 +190,43 @@ describe("nine-router-update", () => {
|
||||
fs.chmodSync(path.join(PREFIX, "lib", "node_modules"), 0o555);
|
||||
update.__setSeams({
|
||||
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 {
|
||||
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");
|
||||
} finally {
|
||||
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 () => {
|
||||
const port = await freePort();
|
||||
startFake(port);
|
||||
@@ -204,7 +239,10 @@ describe("nine-router-update", () => {
|
||||
assert.equal(job.managedBy, "process");
|
||||
const done = await finished();
|
||||
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.to, "9.9.9");
|
||||
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()
|
||||
.map((p) => fs.readFileSync(`/proc/${p}/cmdline`, "utf8").split("\0"))
|
||||
.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 () => {
|
||||
await update.start();
|
||||
const done = await finished();
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -232,7 +274,10 @@ describe("nine-router-update", () => {
|
||||
await update.start();
|
||||
const done = await finished();
|
||||
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.ok(await portOpen(port));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user