@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 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
- console.log(" Restarting fleet...");
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, never gated. Otherwise the LOADED value must be 0.
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
- // Gated like KillMode (#1113): a restart whose unit systemd has not
1829
- // loaded with CoredumpFilter=0 is the restart that can leave GB core
1830
- // dumps behind. Only where systemd knows the directive (246+): an
1831
- // older systemd ignores it, and refusing every restart there would
1832
- // cost far more than the dump size.
1833
- let systemdVersion = 0;
1834
- try {
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
- if (supported) {
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
- let alive = false;
1893
- if (oldPid) {
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 { /* dead or not ours */ }
1899
- }
1900
- if (alive) {
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
- try {
1915
- process.kill(oldPid, "SIGKILL");
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
  }