@songsid/agend 2.1.10-beta.2 → 2.1.10-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +80 -50
- package/dist/cli.js.map +1 -1
- package/dist/coredump-filter.d.ts +21 -0
- package/dist/coredump-filter.js +64 -0
- package/dist/coredump-filter.js.map +1 -0
- package/dist/daemon.js +5 -1
- package/dist/daemon.js.map +1 -1
- package/dist/paths.d.ts +15 -3
- package/dist/paths.js +45 -25
- package/dist/paths.js.map +1 -1
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +31 -0
- package/dist/update-check.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,16 +8,17 @@ import { Command } from "commander";
|
|
|
8
8
|
import { join, dirname } from "node:path";
|
|
9
9
|
import { SchedulerDb } from "./scheduler/db.js";
|
|
10
10
|
import { Cron } from "croner";
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, mkdirSync, chmodSync, readdirSync, rmSync, } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, mkdirSync, chmodSync, readdirSync, rmSync, statSync, } from "node:fs";
|
|
12
12
|
import { homedir, totalmem, freemem, userInfo } from "node:os";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { spawn, execSync, execFileSync } from "node:child_process";
|
|
15
15
|
import { getAgendHome, getTmuxSocketName } from "./paths.js";
|
|
16
16
|
import { hasPausedMarker } from "./pause-marker.js";
|
|
17
17
|
import { COMPLETION_SHELLS, completionScript } from "./completion.js";
|
|
18
|
-
import { getUpdateSelector, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, } from "./update-check.js";
|
|
18
|
+
import { getUpdateSelector, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, runningFleetPredatesInstall, processStartMs, } from "./update-check.js";
|
|
19
19
|
import { clearUpdateMarker, markUpdateInProgress, setUpdateProgressStage } from "./update-marker.js";
|
|
20
|
-
import { acquireFleetLock, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
|
|
20
|
+
import { acquireFleetLock, isFleetStartCommandLine, readProcessCommandLine, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
|
|
21
|
+
import { limitFleetCoreDumps } from "./coredump-filter.js";
|
|
21
22
|
import { SYSTEMD_RESTART_TIMEOUT_MS } from "./service-installer.js";
|
|
22
23
|
import { selectSystemdRestartTarget, SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE, } from "./service-restart-selection.js";
|
|
23
24
|
import { loadRawFleetConfig } from "./config.js";
|
|
@@ -46,6 +47,9 @@ function claimFleetSingleton() {
|
|
|
46
47
|
try {
|
|
47
48
|
setProcessFleetLock(acquireFleetLock(DATA_DIR));
|
|
48
49
|
process.once("exit", () => { releaseProcessFleetLock(); });
|
|
50
|
+
// Before anything is spawned, so a tmux server the fleet starts inherits it
|
|
51
|
+
// (each CLI launch also sets it in its pane: coredumpFilterLaunchPrefix).
|
|
52
|
+
limitFleetCoreDumps({ info: m => console.log(m), error: m => console.error(m) });
|
|
49
53
|
return true;
|
|
50
54
|
}
|
|
51
55
|
catch (err) {
|
|
@@ -1335,7 +1339,53 @@ program
|
|
|
1335
1339
|
const tag = getUpdateSelector(opts);
|
|
1336
1340
|
const pkg = `@songsid/agend@${tag}`;
|
|
1337
1341
|
const targetVersion = lookupTargetVersion(tag);
|
|
1342
|
+
// The restart stage, shared by a real update and by the stale-fleet case.
|
|
1343
|
+
const restartFleetForUpdate = (command, args, version) => {
|
|
1344
|
+
console.log(" Restarting fleet...");
|
|
1345
|
+
setUpdateProgressStage(DATA_DIR, "stopping", { version });
|
|
1346
|
+
// `agend restart` may synchronously wait for a Type=notify service to finish
|
|
1347
|
+
// a multi-minute stop/start and send READY=1. Keep this parent wrapper alive
|
|
1348
|
+
// longer than the restart command's own bounded wait.
|
|
1349
|
+
const restartResult = spawnSync(command, [...args, "restart"], {
|
|
1350
|
+
encoding: "utf-8",
|
|
1351
|
+
timeout: SYSTEMD_RESTART_TIMEOUT_MS + 60_000,
|
|
1352
|
+
stdio: "inherit",
|
|
1353
|
+
});
|
|
1354
|
+
if (!reportUpdateRestart(restartResult.status)) {
|
|
1355
|
+
// No new fleet is coming up to clear the marker — do it here, or the next
|
|
1356
|
+
// 15 minutes of genuine crashes would go unreported.
|
|
1357
|
+
if (!setUpdateProgressStage(DATA_DIR, "failed", { error: "fleet restart failed" })) {
|
|
1358
|
+
clearUpdateMarker(DATA_DIR);
|
|
1359
|
+
}
|
|
1360
|
+
process.exitCode = 1;
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1338
1363
|
if (shouldSkipUpdate(pkgVersion, targetVersion, opts.force)) {
|
|
1364
|
+
// Installed already — but is the running fleet? An update whose restart
|
|
1365
|
+
// failed leaves the old process running against the new files (#1113
|
|
1366
|
+
// hotfix); "already up to date" must not leave it there.
|
|
1367
|
+
let fleetPid = null;
|
|
1368
|
+
try {
|
|
1369
|
+
fleetPid = Number.parseInt(readFileSync(join(DATA_DIR, "fleet.pid"), "utf-8").trim(), 10) || null;
|
|
1370
|
+
}
|
|
1371
|
+
catch { /* not running */ }
|
|
1372
|
+
let installedAtMs = Number.NaN;
|
|
1373
|
+
// When this code landed on disk: npm writes the package's files at install.
|
|
1374
|
+
try {
|
|
1375
|
+
installedAtMs = statSync(fileURLToPath(import.meta.url)).mtimeMs;
|
|
1376
|
+
}
|
|
1377
|
+
catch { /* unknown */ }
|
|
1378
|
+
if (runningFleetPredatesInstall({
|
|
1379
|
+
pid: fleetPid,
|
|
1380
|
+
installedAtMs,
|
|
1381
|
+
processStartMs: pid => processStartMs(pid),
|
|
1382
|
+
isFleetProcess: pid => isFleetStartCommandLine(readProcessCommandLine(pid)),
|
|
1383
|
+
})) {
|
|
1384
|
+
console.log(`\n ✓ v${pkgVersion} is installed, but the running fleet started before it was — restarting it onto v${pkgVersion}.\n`);
|
|
1385
|
+
markUpdateInProgress(DATA_DIR);
|
|
1386
|
+
restartFleetForUpdate(process.execPath, [process.argv[1]], pkgVersion);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1339
1389
|
console.log(`\n ✓ Already up to date (v${pkgVersion})\n`);
|
|
1340
1390
|
setUpdateProgressStage(DATA_DIR, "complete", { version: pkgVersion });
|
|
1341
1391
|
return;
|
|
@@ -1488,24 +1538,7 @@ program
|
|
|
1488
1538
|
// that may be missing or buggy on the version being upgraded from. `agend
|
|
1489
1539
|
// restart` (new binary) does the 4-environment service detection (system
|
|
1490
1540
|
// systemd → user systemd → launchd → detached pid).
|
|
1491
|
-
|
|
1492
|
-
setUpdateProgressStage(DATA_DIR, "stopping", { version: newVersion.replace(/^v/, "") });
|
|
1493
|
-
// `agend restart` may synchronously wait for a Type=notify service to finish
|
|
1494
|
-
// a multi-minute stop/start and send READY=1. Keep this parent wrapper alive
|
|
1495
|
-
// longer than the restart command's own bounded wait.
|
|
1496
|
-
const restartResult = spawnSync(agendPath, ["restart"], {
|
|
1497
|
-
encoding: "utf-8",
|
|
1498
|
-
timeout: SYSTEMD_RESTART_TIMEOUT_MS + 60_000,
|
|
1499
|
-
stdio: "inherit",
|
|
1500
|
-
});
|
|
1501
|
-
if (!reportUpdateRestart(restartResult.status)) {
|
|
1502
|
-
// No new fleet is coming up to clear the marker — do it here, or the next
|
|
1503
|
-
// 15 minutes of genuine crashes would go unreported.
|
|
1504
|
-
if (!setUpdateProgressStage(DATA_DIR, "failed", { error: "fleet restart failed" })) {
|
|
1505
|
-
clearUpdateMarker(DATA_DIR);
|
|
1506
|
-
}
|
|
1507
|
-
process.exitCode = 1;
|
|
1508
|
-
}
|
|
1541
|
+
restartFleetForUpdate(agendPath, [], newVersion.replace(/^v/, ""));
|
|
1509
1542
|
});
|
|
1510
1543
|
program
|
|
1511
1544
|
.command("reload")
|
|
@@ -1816,7 +1849,7 @@ program
|
|
|
1816
1849
|
// Re-judged after the reload, against what systemd now applies: an
|
|
1817
1850
|
// operator's own mask (anywhere — main file, unit or type drop-in, or a
|
|
1818
1851
|
// trailing empty assignment that restores the inherited one) is warned
|
|
1819
|
-
// about
|
|
1852
|
+
// about. Nothing about CoredumpFilter gates the restart.
|
|
1820
1853
|
let filterCustom = hardening.CoredumpFilter === "custom";
|
|
1821
1854
|
if (unitPath && hardening.CoredumpFilter) {
|
|
1822
1855
|
try {
|
|
@@ -1825,31 +1858,20 @@ program
|
|
|
1825
1858
|
catch { /* keep the pre-reload answer */ }
|
|
1826
1859
|
}
|
|
1827
1860
|
if (hardening.CoredumpFilter && !filterCustom) {
|
|
1828
|
-
//
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1832
|
-
//
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
const banner = execSync("systemctl --version", { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 });
|
|
1836
|
-
systemdVersion = Number(/^systemd\s+(\d+)/m.exec(banner)?.[1] ?? 0);
|
|
1837
|
-
}
|
|
1838
|
-
catch { /* unknown → treated as supporting it */ }
|
|
1839
|
-
const supported = systemdVersion === 0 || systemdVersion >= 246;
|
|
1861
|
+
// Informational only — NOT a gate (#1113 hotfix). systemd 249 silently
|
|
1862
|
+
// ignores CoredumpFilter= in a unit file (a transient `systemd-run -p`
|
|
1863
|
+
// honours it, a reloaded unit file does not), so a loaded-value gate
|
|
1864
|
+
// refused every restart there and stranded `agend update` on the old
|
|
1865
|
+
// fleet. The fleet now sets /proc/self/coredump_filter to 0 itself at
|
|
1866
|
+
// startup and in every CLI launch command; the unit line
|
|
1867
|
+
// stays for systemd versions that do apply it.
|
|
1840
1868
|
let filter = "";
|
|
1841
1869
|
try {
|
|
1842
1870
|
filter = execSync(`systemctl${scopeFlag} show -p CoredumpFilter --value ${systemdTarget.unit}`, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
1843
1871
|
}
|
|
1844
1872
|
catch { /* unknown */ }
|
|
1845
1873
|
if (!/^(?:0x)?0+$/i.test(filter)) {
|
|
1846
|
-
|
|
1847
|
-
console.error(` ✗ systemd has ${filter ? `CoredumpFilter=${filter}` : "an unknown CoredumpFilter"} loaded for ${systemdTarget.unit}${reloaded ? "" : ` (\`${reloadCmd}\` failed)`}, so a crash could still write a multi-GB core dump (#1113).`);
|
|
1848
|
-
console.error(` Not restarting. Make sure ${unitPath ?? "the unit"} has CoredumpFilter=0 under [Service], run \`${reloadCmd}\`, then \`agend restart\`.`);
|
|
1849
|
-
process.exitCode = 1;
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
console.log(` ⚠ systemd ${systemdVersion} predates CoredumpFilter (246); crashes may still write full-size core dumps (#1113). Restarting anyway.`);
|
|
1874
|
+
console.log(` ℹ systemd has ${filter ? `CoredumpFilter=${filter}` : "no CoredumpFilter"} loaded for ${systemdTarget.unit} (some systemd versions ignore it in unit files); the fleet sets coredump_filter=0 itself at startup (#1113).`);
|
|
1853
1875
|
}
|
|
1854
1876
|
}
|
|
1855
1877
|
if (filterCustom)
|
|
@@ -1889,15 +1911,19 @@ program
|
|
|
1889
1911
|
// 4. Detached process tracked by fleet.pid (no service manager)
|
|
1890
1912
|
if (existsSync(pidPath)) {
|
|
1891
1913
|
const oldPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
1892
|
-
|
|
1893
|
-
|
|
1914
|
+
// Only signal a process confirmed to BE this fleet (#1125 review): a
|
|
1915
|
+
// stale fleet.pid may name a recycled pid — an unrelated process. Not
|
|
1916
|
+
// confirmed (gone, or something else) is treated as a stale pid file.
|
|
1917
|
+
const isOurFleet = () => {
|
|
1894
1918
|
try {
|
|
1895
1919
|
process.kill(oldPid, 0);
|
|
1896
|
-
alive = true;
|
|
1897
1920
|
}
|
|
1898
|
-
catch {
|
|
1899
|
-
|
|
1900
|
-
|
|
1921
|
+
catch {
|
|
1922
|
+
return false;
|
|
1923
|
+
}
|
|
1924
|
+
return isFleetStartCommandLine(readProcessCommandLine(oldPid));
|
|
1925
|
+
};
|
|
1926
|
+
if (oldPid && isOurFleet()) {
|
|
1901
1927
|
try {
|
|
1902
1928
|
process.kill(oldPid, "SIGTERM");
|
|
1903
1929
|
}
|
|
@@ -1911,10 +1937,14 @@ program
|
|
|
1911
1937
|
}
|
|
1912
1938
|
spawnSync("sleep", ["0.5"]);
|
|
1913
1939
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1940
|
+
// Re-confirmed right before the SIGKILL: the pid may have been reused
|
|
1941
|
+
// during the wait.
|
|
1942
|
+
if (isOurFleet()) {
|
|
1943
|
+
try {
|
|
1944
|
+
process.kill(oldPid, "SIGKILL");
|
|
1945
|
+
}
|
|
1946
|
+
catch { /* already gone */ }
|
|
1916
1947
|
}
|
|
1917
|
-
catch { /* already gone */ }
|
|
1918
1948
|
try {
|
|
1919
1949
|
unlinkSync(pidPath);
|
|
1920
1950
|
}
|