c8ctl-plugin-nano 1.35.1 → 1.35.2
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/c8ctl-plugin.js +78 -1
- package/package.json +8 -8
package/c8ctl-plugin.js
CHANGED
|
@@ -4404,6 +4404,16 @@ async function workAgent(req, flags) {
|
|
|
4404
4404
|
// path via NANO_SUPERVISOR_ACTIVITY_FILE; a standalone `nano work` has no such
|
|
4405
4405
|
// env var and writes nothing (this is entirely advisory).
|
|
4406
4406
|
const activityFile = process.env.NANO_SUPERVISOR_ACTIVITY_FILE || null;
|
|
4407
|
+
// Supervised workers (spawned by the daemon, hence NANO_SUPERVISOR_ACTIVITY_FILE)
|
|
4408
|
+
// arm a parent-death watchdog: if the daemon dies *ungracefully* (SIGKILL /
|
|
4409
|
+
// crash / its host force-killed) it can't run its child-reaping shutdown, and
|
|
4410
|
+
// this worker would otherwise idle forever as an orphan reparented to init.
|
|
4411
|
+
// The watchdog makes the child self-exit the moment it is reparented. A
|
|
4412
|
+
// standalone `nano work` (no activity file) keeps classic nohup semantics.
|
|
4413
|
+
if (activityFile) {
|
|
4414
|
+
const daemonPid = Number.parseInt(process.env.NANO_SUPERVISOR_DAEMON_PID ?? '', 10);
|
|
4415
|
+
installParentDeathWatchdog({ parentPid: Number.isInteger(daemonPid) ? daemonPid : undefined });
|
|
4416
|
+
}
|
|
4407
4417
|
const activeJobs = new Map(); // jobKey -> { type, since (ms epoch) }
|
|
4408
4418
|
const writeActivity = () => {
|
|
4409
4419
|
if (!activityFile) return;
|
|
@@ -5549,6 +5559,63 @@ function waitForChildExit(child, timeoutMs) {
|
|
|
5549
5559
|
});
|
|
5550
5560
|
}
|
|
5551
5561
|
|
|
5562
|
+
/**
|
|
5563
|
+
* Self-terminate a supervised worker if its parent daemon dies *ungracefully*.
|
|
5564
|
+
*
|
|
5565
|
+
* The daemon reaps its `nano work` children on a clean stop/SIGTERM/SIGINT (see
|
|
5566
|
+
* `shutdown`). But a `SIGKILL`, a crash, or the daemon's host process being
|
|
5567
|
+
* force-killed cannot run that path, and the children — spawned attached, with
|
|
5568
|
+
* no controlling TTY — would otherwise survive forever as orphans reparented to
|
|
5569
|
+
* init (ppid 1). That is exactly how a test/agent run that force-kills its
|
|
5570
|
+
* supervisor leaks a whole idle worker fleet.
|
|
5571
|
+
*
|
|
5572
|
+
* The watchdog closes that gap from the child's side. The parent to watch is the
|
|
5573
|
+
* daemon pid the spawn recorded (`parentPid`, forwarded via
|
|
5574
|
+
* NANO_SUPERVISOR_DAEMON_PID) rather than a late `process.ppid` sample: a worker
|
|
5575
|
+
* whose ~async startup (an 8k-line dynamic import) is outrun by the daemon's
|
|
5576
|
+
* death is *already* reparented to init by the time it arms, so a bare
|
|
5577
|
+
* `process.ppid` read would miss the very race that leaks. Given the known pid,
|
|
5578
|
+
* the worker self-reaps whenever it is reparented away from it OR that pid is
|
|
5579
|
+
* gone — and does so *immediately* if it was orphaned during its own startup.
|
|
5580
|
+
*
|
|
5581
|
+
* The poll timer is unref'd so it never keeps an otherwise-idle worker alive;
|
|
5582
|
+
* the real work loop (or, in tests, an explicit keep-alive) holds the event loop
|
|
5583
|
+
* open while the worker is meant to run.
|
|
5584
|
+
*
|
|
5585
|
+
* Unix-only: reparent-to-init is a POSIX semantic; Windows job objects are the
|
|
5586
|
+
* equivalent lifecycle tie and are out of scope here (matching the daemon's
|
|
5587
|
+
* other `win32` guards). Returns a canceller so callers/tests can stop it.
|
|
5588
|
+
*/
|
|
5589
|
+
function installParentDeathWatchdog({ intervalMs = 2000, parentPid, onOrphan, readPpid } = {}) {
|
|
5590
|
+
if (osPlatform() === 'win32') return () => {};
|
|
5591
|
+
// `readPpid` is an injectable seam (defaults to the live value) so the pid-1
|
|
5592
|
+
// container case — which a normal test process can't reproduce, since its own
|
|
5593
|
+
// ppid is never 1 — is unit-testable.
|
|
5594
|
+
const ppid = typeof readPpid === 'function' ? readPpid : () => process.ppid;
|
|
5595
|
+
// Prefer the daemon pid the spawn recorded (`explicit`); fall back to the
|
|
5596
|
+
// current parent. An explicit pid is authoritative even if it is 1 — the
|
|
5597
|
+
// daemon may legitimately run as PID 1 (a container entrypoint) — so we must
|
|
5598
|
+
// NOT treat that as "orphaned". A *fallback* ppid of 1, by contrast, means we
|
|
5599
|
+
// were already reparented to init with no daemon left to watch.
|
|
5600
|
+
const explicit = Number.isInteger(parentPid) && parentPid > 0;
|
|
5601
|
+
const watched = explicit ? parentPid : ppid();
|
|
5602
|
+
const orphaned = typeof onOrphan === 'function' ? onOrphan : () => process.exit(0);
|
|
5603
|
+
const isOrphan = () => {
|
|
5604
|
+
// Reparented away from the daemon (typically to init, pid 1) → orphaned.
|
|
5605
|
+
if (ppid() !== watched) return true;
|
|
5606
|
+
// Defensive: the daemon pid is gone even though ppid still names it (a
|
|
5607
|
+
// zombie/racey read). ESRCH ⇒ gone; EPERM ⇒ alive but not ours to signal.
|
|
5608
|
+
try { process.kill(watched, 0); return false; } catch (err) { return err.code !== 'EPERM'; }
|
|
5609
|
+
};
|
|
5610
|
+
// Orphaned already — self-reap now rather than idle forever. Either we fell
|
|
5611
|
+
// back to ppid and it is already init (no known parent), or the recorded
|
|
5612
|
+
// daemon is verifiably gone/reparented (e.g. it died before we armed).
|
|
5613
|
+
if ((!explicit && watched === 1) || isOrphan()) { orphaned(); return () => {}; }
|
|
5614
|
+
const timer = setInterval(() => { if (isOrphan()) { clearInterval(timer); orphaned(); } }, intervalMs);
|
|
5615
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
5616
|
+
return () => { try { clearInterval(timer); } catch { /* ignore */ } };
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5552
5619
|
// --- Daemon ----------------------------------------------------------------
|
|
5553
5620
|
|
|
5554
5621
|
/**
|
|
@@ -5622,8 +5689,12 @@ async function runSupervisorDaemon() {
|
|
|
5622
5689
|
// supervisor id, so the same profile launched twice is distinct end-to-end.
|
|
5623
5690
|
// NANO_SUPERVISOR_ACTIVITY_FILE tells the child where to report per-job
|
|
5624
5691
|
// activity for `supervisor status` (idle vs the job key it is servicing).
|
|
5692
|
+
// NANO_SUPERVISOR_DAEMON_PID hands the child our pid so its parent-death
|
|
5693
|
+
// watchdog can self-reap if we die ungracefully (SIGKILL/crash) and can't
|
|
5694
|
+
// run the child-draining shutdown — even if we die mid-startup, before the
|
|
5695
|
+
// child reparents to init.
|
|
5625
5696
|
const child = spawn(exec, [entry, 'nano', 'work', w.profile, '--name', w.id, ...w.args], {
|
|
5626
|
-
env: { ...process.env, NANO_SUPERVISOR_ACTIVITY_FILE: activityFile },
|
|
5697
|
+
env: { ...process.env, NANO_SUPERVISOR_ACTIVITY_FILE: activityFile, NANO_SUPERVISOR_DAEMON_PID: String(process.pid) },
|
|
5627
5698
|
stdio: ['ignore', fd, fd],
|
|
5628
5699
|
});
|
|
5629
5700
|
if (typeof fd === 'number') { try { closeSync(fd); } catch { /* dup'd into child */ } }
|
|
@@ -5889,6 +5960,11 @@ async function runSupervisorDaemon() {
|
|
|
5889
5960
|
|
|
5890
5961
|
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
5891
5962
|
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
5963
|
+
// SIGHUP (controlling terminal/session gone) must also drain children rather
|
|
5964
|
+
// than let Node's default terminate the daemon and orphan the fleet. SIGKILL
|
|
5965
|
+
// and hard crashes still can't run this — the per-worker parent-death
|
|
5966
|
+
// watchdog is the backstop for those.
|
|
5967
|
+
process.once('SIGHUP', () => shutdown('SIGHUP'));
|
|
5892
5968
|
dlog(`supervisor daemon up (pid ${process.pid}) — control ${socketPath}`);
|
|
5893
5969
|
persist();
|
|
5894
5970
|
|
|
@@ -7998,6 +8074,7 @@ export {
|
|
|
7998
8074
|
supervisorJobCell,
|
|
7999
8075
|
supervisorWorkerActivityFile,
|
|
8000
8076
|
WORK_FORWARD_FLAGS,
|
|
8077
|
+
installParentDeathWatchdog,
|
|
8001
8078
|
runSupervisorDaemon,
|
|
8002
8079
|
startSupervisorDaemon,
|
|
8003
8080
|
supervisorRequest,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.35.
|
|
3
|
+
"version": "1.35.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
|
|
6
6
|
"main": "c8ctl-plugin.js",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
59
|
"node-pty": "^1.0.0",
|
|
60
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.35.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.35.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.35.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.35.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.35.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.35.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.35.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.35.2",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.35.2",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.35.2",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.35.2",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.35.2",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.35.2",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.35.2"
|
|
67
67
|
}
|
|
68
68
|
}
|