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:
@@ -56,8 +56,7 @@ const timing = {
|
||||
portUpMs: 45000,
|
||||
};
|
||||
|
||||
const makeErr = (code, message, extra = {}) =>
|
||||
Object.assign(new Error(message), { code }, extra);
|
||||
const makeErr = (code, message, extra = {}) => Object.assign(new Error(message), { code }, extra);
|
||||
|
||||
/** execFile as a promise that never rejects: `{code, stdout, stderr}`. */
|
||||
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. */
|
||||
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";
|
||||
// `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.
|
||||
@@ -189,8 +204,9 @@ async function findProcesses(loc) {
|
||||
// Its server renames itself (`next-server`), so children are found by parent,
|
||||
// plus anything still running from inside the install.
|
||||
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 (let grew = true; grew; ) {
|
||||
for (const { pid, argv } of rows)
|
||||
if (argv.some((a) => a.startsWith(loc.pkgDir + path.sep))) all.add(pid);
|
||||
for (let grew = true; grew;) {
|
||||
grew = false;
|
||||
for (const { pid, ppid } of rows)
|
||||
if (all.has(ppid) && !all.has(pid)) {
|
||||
@@ -243,7 +259,8 @@ const signal = (pids, sig) => {
|
||||
};
|
||||
|
||||
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;
|
||||
try {
|
||||
version = await fetchLatestImpl();
|
||||
@@ -265,20 +282,37 @@ const view = (j) => (j ? JSON.parse(JSON.stringify(j)) : null);
|
||||
async function status({ fresh = false } = {}) {
|
||||
const loc = locate();
|
||||
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([
|
||||
latestVersion({ fresh }),
|
||||
unitActive(),
|
||||
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`;
|
||||
let blocker = null;
|
||||
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) {
|
||||
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) {
|
||||
blocker = latest
|
||||
? { code: "EUPTODATE", message: `9router ${loc.version} is the latest version` }
|
||||
@@ -370,7 +404,15 @@ function runInstall(argv) {
|
||||
async function launchHidden(loc, procs) {
|
||||
const node = aiTools.which("node", versions.jobPath()) || process.execPath;
|
||||
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);
|
||||
const env = { ...userEnv(), ...desktopEnv() };
|
||||
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);
|
||||
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.
|
||||
const failed = await new Promise((resolve) => {
|
||||
const t = setTimeout(() => resolve(false), 1500);
|
||||
@@ -404,7 +455,10 @@ async function launchHidden(loc, procs) {
|
||||
async function doRelaunch(loc, unit, procs) {
|
||||
let port = procs.port || DEFAULT_PORT;
|
||||
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()}`);
|
||||
} else {
|
||||
port = await launchHidden(loc, procs);
|
||||
@@ -431,7 +485,7 @@ async function runJob(loc, npm, mode, unit, procs) {
|
||||
}
|
||||
if (!failed) {
|
||||
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 after = locate();
|
||||
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");
|
||||
const s = await status({ fresh: true });
|
||||
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 npm = versions.npmBin();
|
||||
const mode = await installMode(loc, npm);
|
||||
|
||||
Reference in New Issue
Block a user