@camstack/system 1.2.311 → 1.2.312
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/addon-runner.js +6 -8
- package/dist/addon-runner.mjs +6 -8
- package/dist/builtins/native-metrics/native-metrics.addon.js +9 -101
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +3 -91
- package/dist/{child-cap-dispatch-BCJjUtXV.mjs → child-cap-dispatch-CHTiHbsv.mjs} +294 -10
- package/dist/{child-cap-dispatch-Be8e2cBP.js → child-cap-dispatch-Dyn_7ezH.js} +347 -9
- package/dist/index.js +87 -17
- package/dist/index.mjs +88 -19
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/moleculer/process-service.d.ts +9 -0
- package/dist/kernel/moleculer/runner-process-group.d.ts +92 -0
- package/dist/proc-stat-CbC55Kqi.mjs +92 -0
- package/dist/proc-stat-DL-WUOhl.js +127 -0
- package/dist/{resource-monitor-Vp0bPV3U.mjs → resource-monitor-B1v6E07R.mjs} +1 -1
- package/dist/{resource-monitor-LVE1BoLs.js → resource-monitor-DzNn_IZK.js} +5 -5
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
+
const require_proc_stat = require("./proc-stat-DL-WUOhl.js");
|
|
2
3
|
require("./hub-cap-forward-DmHNjbB0.js");
|
|
3
4
|
let node_crypto = require("node:crypto");
|
|
4
5
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
@@ -3210,7 +3211,7 @@ function createAddonPeerBytesFacility(args) {
|
|
|
3210
3211
|
* Failures THROW with an actionable message — a caller must never proceed as
|
|
3211
3212
|
* if dependencies were installed when they were not.
|
|
3212
3213
|
*/
|
|
3213
|
-
var execFileAsync$
|
|
3214
|
+
var execFileAsync$3 = (0, node_util.promisify)(node_child_process.execFile);
|
|
3214
3215
|
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
3215
3216
|
/** Where the bootstrap lands inside `cacheDir` (npm tgz carries a `package/` prefix). */
|
|
3216
3217
|
function bootstrappedCliPath(cacheDir) {
|
|
@@ -3218,7 +3219,7 @@ function bootstrappedCliPath(cacheDir) {
|
|
|
3218
3219
|
}
|
|
3219
3220
|
async function defaultProbeSystemNpm() {
|
|
3220
3221
|
try {
|
|
3221
|
-
await execFileAsync$
|
|
3222
|
+
await execFileAsync$3("npm", ["--version"], { timeout: 15e3 });
|
|
3222
3223
|
return true;
|
|
3223
3224
|
} catch {
|
|
3224
3225
|
return false;
|
|
@@ -3236,7 +3237,7 @@ async function defaultDownloadTarball(url, destTgz) {
|
|
|
3236
3237
|
await node_fs.promises.writeFile(destTgz, bytes);
|
|
3237
3238
|
}
|
|
3238
3239
|
async function defaultExtractTarball(tgzPath, destDir) {
|
|
3239
|
-
await execFileAsync$
|
|
3240
|
+
await execFileAsync$3("tar", [
|
|
3240
3241
|
"-xzf",
|
|
3241
3242
|
tgzPath,
|
|
3242
3243
|
"-C",
|
|
@@ -3307,7 +3308,7 @@ async function runNpm(args, options) {
|
|
|
3307
3308
|
registry: options.registry,
|
|
3308
3309
|
logger: options.logger
|
|
3309
3310
|
});
|
|
3310
|
-
return execFileAsync$
|
|
3311
|
+
return execFileAsync$3(invocation.command, [...invocation.argsPrefix, ...args], {
|
|
3311
3312
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
3312
3313
|
timeout: options.timeout ?? 3e5,
|
|
3313
3314
|
env: buildNpmChildEnv(options.env ?? process.env, process.execPath)
|
|
@@ -3388,7 +3389,7 @@ function resolveAddonClass(mod) {
|
|
|
3388
3389
|
}
|
|
3389
3390
|
//#endregion
|
|
3390
3391
|
//#region src/kernel/deps/manifest-native-deps.ts
|
|
3391
|
-
var execFileAsync$
|
|
3392
|
+
var execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
|
|
3392
3393
|
/**
|
|
3393
3394
|
* Fallback bootstrap-cache location when the caller doesn't thread one
|
|
3394
3395
|
* through (the forked addon-runner's spawn-time install). tmpdir survives the
|
|
@@ -3697,7 +3698,7 @@ async function rebuildNativeDeps(addonDir, packageNames, logger, npmCacheDir, re
|
|
|
3697
3698
|
return;
|
|
3698
3699
|
}
|
|
3699
3700
|
logger.warn("@electron/rebuild not available — falling back to npx", { meta: { addonDir } });
|
|
3700
|
-
await execFileAsync$
|
|
3701
|
+
await execFileAsync$2("npx", [
|
|
3701
3702
|
"--yes",
|
|
3702
3703
|
"electron-rebuild",
|
|
3703
3704
|
"-m",
|
|
@@ -12119,7 +12120,7 @@ function requestPrivilegedInstall(req, socketPath = pkgdSocketPath(), timeoutMs
|
|
|
12119
12120
|
* the failure it precedes ("smbclient: ENOENT") arrives minutes later in a
|
|
12120
12121
|
* different addon's logs.
|
|
12121
12122
|
*/
|
|
12122
|
-
var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
12123
|
+
var execFileAsync$1 = (0, node_util.promisify)(node_child_process.execFile);
|
|
12123
12124
|
/** How long a single package-manager invocation may take. */
|
|
12124
12125
|
var INSTALL_TIMEOUT_MS = 300 * 1e3;
|
|
12125
12126
|
/** How long the `command -v <binary>` presence probe may take. */
|
|
@@ -12281,7 +12282,7 @@ function planFor(manager, packageName) {
|
|
|
12281
12282
|
}
|
|
12282
12283
|
async function defaultHasBinary(binary) {
|
|
12283
12284
|
try {
|
|
12284
|
-
await execFileAsync("command", ["-v", binary], {
|
|
12285
|
+
await execFileAsync$1("command", ["-v", binary], {
|
|
12285
12286
|
timeout: PROBE_TIMEOUT_MS,
|
|
12286
12287
|
shell: process.platform === "win32" ? false : "/bin/sh"
|
|
12287
12288
|
});
|
|
@@ -12301,7 +12302,290 @@ async function defaultDetectManager() {
|
|
|
12301
12302
|
return null;
|
|
12302
12303
|
}
|
|
12303
12304
|
async function defaultRun(command, args) {
|
|
12304
|
-
await execFileAsync(command, [...args], { timeout: INSTALL_TIMEOUT_MS });
|
|
12305
|
+
await execFileAsync$1(command, [...args], { timeout: INSTALL_TIMEOUT_MS });
|
|
12306
|
+
}
|
|
12307
|
+
//#endregion
|
|
12308
|
+
//#region src/kernel/moleculer/runner-process-group.ts
|
|
12309
|
+
/**
|
|
12310
|
+
* A runner OWNS a process group, and the kernel reaps it on every runner exit (D668).
|
|
12311
|
+
*
|
|
12312
|
+
* ## Why
|
|
12313
|
+
*
|
|
12314
|
+
* An addon-runner spawns long-lived children: recorder ffmpeg writers, stream-broker
|
|
12315
|
+
* ffmpeg, llama-server, tailscaled, cloudflared, HAP ffmpeg. Their only kill was the
|
|
12316
|
+
* addon's own graceful `stop()`, which a runner that dies of SIGABRT (V8 OOM), SIGKILL
|
|
12317
|
+
* or a native crash never runs. ffmpeg ignores SIGPIPE and does not quit on stdin EOF,
|
|
12318
|
+
* so a dead reader does not stop it either. On 2026-09-28 at 10:21 CEST the hub's
|
|
12319
|
+
* recorder runner aborted; 28 ffmpeg writers survived as orphans and kept writing into
|
|
12320
|
+
* the SAME staging directories the respawned runner's 28 new writers used — 13 minutes
|
|
12321
|
+
* of dual writers on every camera, until they were killed by hand.
|
|
12322
|
+
*
|
|
12323
|
+
* Runners were spawned `detached: false`, i.e. inside hub-main's own group, so there
|
|
12324
|
+
* was no handle meaning "this runner and everything under it". Now every runner is
|
|
12325
|
+
* spawned `detached: true` on POSIX, which makes it a SESSION and group leader
|
|
12326
|
+
* (`setsid`): pgid === runner pid, and every descendant that does not itself call
|
|
12327
|
+
* `setsid`/`setpgid` inherits that group. A process group outlives its leader, so
|
|
12328
|
+
* `kill(-pgid)` still reaches the orphans after the runner is gone — which is exactly
|
|
12329
|
+
* the moment it matters.
|
|
12330
|
+
*
|
|
12331
|
+
* ## What `detached` does NOT change
|
|
12332
|
+
*
|
|
12333
|
+
* - The child stays REF'd: nothing calls `unref()`, so the parent still waits on it,
|
|
12334
|
+
* still gets its `exit`, still reads its stdout/stderr pipes.
|
|
12335
|
+
* - `process.ppid` is unchanged, so the runner can detect a lost parent before or
|
|
12336
|
+
* after READY and hard-stop its own group.
|
|
12337
|
+
* - UDS/stdio wiring is fd-based and independent of the session.
|
|
12338
|
+
*
|
|
12339
|
+
* What it DOES change: the runner is no longer in hub-main's group, so a signal sent to
|
|
12340
|
+
* hub-main's group (`kill -- -<pid>` in `server/backend/src/main.ts`, a terminal Ctrl+C)
|
|
12341
|
+
* no longer reaches runners by itself. `signalAllRunnerGroups` is the replacement, and
|
|
12342
|
+
* the process `exit` backstop below hard-stops any groups a hub/agent still owns.
|
|
12343
|
+
*
|
|
12344
|
+
* ## Out of scope, deliberately
|
|
12345
|
+
*
|
|
12346
|
+
* - Windows has no process groups in this sense; nothing here runs on `win32` and the
|
|
12347
|
+
* runner is spawned exactly as before.
|
|
12348
|
+
* - A descendant that calls `setsid`/`setpgid` itself (or is spawned `detached: true`
|
|
12349
|
+
* by an addon) leaves the group and is NOT reaped. No shipped addon does; one that
|
|
12350
|
+
* needs to must own its child's lifetime some other way.
|
|
12351
|
+
* - The kernel knows the runner id, not which device a child served. Per-device
|
|
12352
|
+
* attribution is the addon's (the recorder's own foreign-writer reap, D667).
|
|
12353
|
+
*/
|
|
12354
|
+
var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
|
|
12355
|
+
/** SIGTERM → SIGKILL grace for a runner's group, in ms. ffmpeg finalises a segment well inside it. */
|
|
12356
|
+
var RUNNER_GROUP_REAP_GRACE_MS = 2e3;
|
|
12357
|
+
/** How often the group is probed for emptiness during the grace. */
|
|
12358
|
+
var GROUP_PROBE_INTERVAL_MS = 100;
|
|
12359
|
+
/** Whether a runner is spawned as a process-group leader on this platform. */
|
|
12360
|
+
function runnerOwnsProcessGroup(platform = process.platform) {
|
|
12361
|
+
return platform !== "win32";
|
|
12362
|
+
}
|
|
12363
|
+
function errnoOf(err) {
|
|
12364
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
12365
|
+
const code = Reflect.get(err, "code");
|
|
12366
|
+
return typeof code === "string" ? code : void 0;
|
|
12367
|
+
}
|
|
12368
|
+
function signalGroup(ops, pgid, signal) {
|
|
12369
|
+
try {
|
|
12370
|
+
ops.kill(-pgid, signal);
|
|
12371
|
+
return "delivered";
|
|
12372
|
+
} catch (err) {
|
|
12373
|
+
if (errnoOf(err) === "ESRCH") return "empty";
|
|
12374
|
+
return { error: `${errnoOf(err) ?? "unknown"}: ${err instanceof Error ? err.message : String(err)}` };
|
|
12375
|
+
}
|
|
12376
|
+
}
|
|
12377
|
+
function signalPid(ops, pid) {
|
|
12378
|
+
try {
|
|
12379
|
+
ops.kill(pid, 0);
|
|
12380
|
+
return "delivered";
|
|
12381
|
+
} catch (err) {
|
|
12382
|
+
return errnoOf(err) === "ESRCH" ? "empty" : { error: errnoOf(err) ?? "unknown" };
|
|
12383
|
+
}
|
|
12384
|
+
}
|
|
12385
|
+
/**
|
|
12386
|
+
* A pgid this module may address. `kill(-1)` signals every process the caller may
|
|
12387
|
+
* signal and `kill(-0)` is the caller's OWN group — both are catastrophes, not reaps.
|
|
12388
|
+
*/
|
|
12389
|
+
function isAddressableGroup(pgid) {
|
|
12390
|
+
return Number.isInteger(pgid) && pgid > 1 && pgid !== process.pid;
|
|
12391
|
+
}
|
|
12392
|
+
/**
|
|
12393
|
+
* SIGTERM the whole group, wait up to `graceMs` for it to empty, SIGKILL what is left.
|
|
12394
|
+
*
|
|
12395
|
+
* pid reuse: POSIX never hands out a pid while a process group with that id exists, so
|
|
12396
|
+
* the SIGTERM always lands on our group. The SIGKILL is sent only when the group is
|
|
12397
|
+
* still populated at the end of the grace, which is the same group; the residual window
|
|
12398
|
+
* (group empties, the pid wraps around AND is reused as a new group leader, all inside
|
|
12399
|
+
* the grace) is not reachable in practice.
|
|
12400
|
+
*/
|
|
12401
|
+
async function reapProcessGroup(pgid, ops) {
|
|
12402
|
+
if (!isAddressableGroup(pgid)) return {
|
|
12403
|
+
pgid,
|
|
12404
|
+
membersAtExit: null,
|
|
12405
|
+
killed: null,
|
|
12406
|
+
alreadyEmpty: true,
|
|
12407
|
+
error: `refused pgid ${pgid}`
|
|
12408
|
+
};
|
|
12409
|
+
const membersAtExit = await ops.listMembers(pgid);
|
|
12410
|
+
const term = signalGroup(ops, pgid, "SIGTERM");
|
|
12411
|
+
if (term === "empty") return {
|
|
12412
|
+
pgid,
|
|
12413
|
+
membersAtExit,
|
|
12414
|
+
killed: [],
|
|
12415
|
+
alreadyEmpty: true,
|
|
12416
|
+
error: null
|
|
12417
|
+
};
|
|
12418
|
+
if (typeof term === "object") return {
|
|
12419
|
+
pgid,
|
|
12420
|
+
membersAtExit,
|
|
12421
|
+
killed: null,
|
|
12422
|
+
alreadyEmpty: false,
|
|
12423
|
+
error: term.error
|
|
12424
|
+
};
|
|
12425
|
+
for (let waited = 0; waited < ops.graceMs; waited += GROUP_PROBE_INTERVAL_MS) {
|
|
12426
|
+
await ops.sleep(Math.min(GROUP_PROBE_INTERVAL_MS, ops.graceMs - waited));
|
|
12427
|
+
if (signalGroup(ops, pgid, 0) === "empty") return {
|
|
12428
|
+
pgid,
|
|
12429
|
+
membersAtExit,
|
|
12430
|
+
killed: [],
|
|
12431
|
+
alreadyEmpty: false,
|
|
12432
|
+
error: null
|
|
12433
|
+
};
|
|
12434
|
+
}
|
|
12435
|
+
const survivors = membersAtExit === null ? null : membersAtExit.filter((m) => signalPid(ops, m.pid) === "delivered");
|
|
12436
|
+
const kill = signalGroup(ops, pgid, "SIGKILL");
|
|
12437
|
+
return {
|
|
12438
|
+
pgid,
|
|
12439
|
+
membersAtExit,
|
|
12440
|
+
killed: kill === "empty" ? [] : survivors,
|
|
12441
|
+
alreadyEmpty: false,
|
|
12442
|
+
error: typeof kill === "object" ? kill.error : null
|
|
12443
|
+
};
|
|
12444
|
+
}
|
|
12445
|
+
/** `ffmpeg×28 llama-server×1` — what a reap line says it removed. */
|
|
12446
|
+
function summarizeMembers(members) {
|
|
12447
|
+
const counts = /* @__PURE__ */ new Map();
|
|
12448
|
+
for (const m of members) counts.set(m.command, (counts.get(m.command) ?? 0) + 1);
|
|
12449
|
+
return [...counts.entries()].map(([command, n]) => `${command}×${n}`).join(" ");
|
|
12450
|
+
}
|
|
12451
|
+
async function listMembersFromProc(pgid, reader) {
|
|
12452
|
+
const pids = await reader.listPids();
|
|
12453
|
+
return (await Promise.all(pids.map(async (pid) => {
|
|
12454
|
+
try {
|
|
12455
|
+
return require_proc_stat.parseProcStat(await reader.readStat(pid));
|
|
12456
|
+
} catch {
|
|
12457
|
+
return null;
|
|
12458
|
+
}
|
|
12459
|
+
}))).flatMap((row) => row !== null && row.pgid === pgid && row.state !== "Z" ? [{
|
|
12460
|
+
pid: row.pid,
|
|
12461
|
+
command: row.comm
|
|
12462
|
+
}] : []);
|
|
12463
|
+
}
|
|
12464
|
+
var PS_GROUP_ROW_RE = /^(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/;
|
|
12465
|
+
/** Parse `ps -A -o pid=,pgid=,stat=,comm=` for one group, zombies excluded. */
|
|
12466
|
+
function parsePsGroupMembers(stdout, pgid) {
|
|
12467
|
+
const members = [];
|
|
12468
|
+
for (const line of stdout.split("\n")) {
|
|
12469
|
+
const match = line.trim().match(PS_GROUP_ROW_RE);
|
|
12470
|
+
if (!match) continue;
|
|
12471
|
+
const [, pidS, pgidS, stat = "", comm = ""] = match;
|
|
12472
|
+
if (Number(pgidS) !== pgid || stat.startsWith("Z")) continue;
|
|
12473
|
+
const command = comm.trim();
|
|
12474
|
+
members.push({
|
|
12475
|
+
pid: Number(pidS),
|
|
12476
|
+
command: command.slice(command.lastIndexOf("/") + 1)
|
|
12477
|
+
});
|
|
12478
|
+
}
|
|
12479
|
+
return members;
|
|
12480
|
+
}
|
|
12481
|
+
async function listMembersFromPs(pgid) {
|
|
12482
|
+
const { stdout } = await execFileAsync("ps", [
|
|
12483
|
+
"-A",
|
|
12484
|
+
"-o",
|
|
12485
|
+
"pid=,pgid=,stat=,comm="
|
|
12486
|
+
], {
|
|
12487
|
+
timeout: 3e3,
|
|
12488
|
+
maxBuffer: 8 * 1024 * 1024
|
|
12489
|
+
});
|
|
12490
|
+
return parsePsGroupMembers(stdout, pgid);
|
|
12491
|
+
}
|
|
12492
|
+
/** Live members of `pgid`: `/proc` on Linux, `ps` elsewhere; `null` when neither answers. */
|
|
12493
|
+
async function listProcessGroupMembers(pgid, platform = process.platform) {
|
|
12494
|
+
try {
|
|
12495
|
+
return platform === "linux" ? await listMembersFromProc(pgid, require_proc_stat.nodeProcReader) : await listMembersFromPs(pgid);
|
|
12496
|
+
} catch {
|
|
12497
|
+
return null;
|
|
12498
|
+
}
|
|
12499
|
+
}
|
|
12500
|
+
var defaultProcessGroupOps = {
|
|
12501
|
+
kill: (pid, signal) => {
|
|
12502
|
+
process.kill(pid, signal);
|
|
12503
|
+
},
|
|
12504
|
+
listMembers: (pgid) => listProcessGroupMembers(pgid),
|
|
12505
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
12506
|
+
graceMs: RUNNER_GROUP_REAP_GRACE_MS
|
|
12507
|
+
};
|
|
12508
|
+
/**
|
|
12509
|
+
* Every runner group this process leads and has not finished reaping. Process-wide on
|
|
12510
|
+
* purpose: `process.on('exit')` and hub-main's signal handler must reach the groups of
|
|
12511
|
+
* every `$process` service in the process, and neither holds a service reference.
|
|
12512
|
+
*/
|
|
12513
|
+
var ownedGroups = /* @__PURE__ */ new Map();
|
|
12514
|
+
var exitBackstopInstalled = false;
|
|
12515
|
+
function installExitBackstop() {
|
|
12516
|
+
if (exitBackstopInstalled) return;
|
|
12517
|
+
exitBackstopInstalled = true;
|
|
12518
|
+
process.on("exit", () => {
|
|
12519
|
+
for (const [pgid, group] of ownedGroups) try {
|
|
12520
|
+
group.kill(-pgid, "SIGKILL");
|
|
12521
|
+
} catch {}
|
|
12522
|
+
});
|
|
12523
|
+
}
|
|
12524
|
+
function registerRunnerGroup(pgid, group) {
|
|
12525
|
+
if (!isAddressableGroup(pgid)) return;
|
|
12526
|
+
ownedGroups.set(pgid, group);
|
|
12527
|
+
installExitBackstop();
|
|
12528
|
+
}
|
|
12529
|
+
function releaseRunnerGroup(pgid) {
|
|
12530
|
+
ownedGroups.delete(pgid);
|
|
12531
|
+
}
|
|
12532
|
+
/**
|
|
12533
|
+
* Signal every runner group this process owns, synchronously. Hub-main's SIGTERM/SIGINT
|
|
12534
|
+
* handler calls it: before D668 the runners were in hub-main's own group and its
|
|
12535
|
+
* `kill -- -<pid>` reached them and their children; now each is its own group.
|
|
12536
|
+
* Returns how many groups were signalled.
|
|
12537
|
+
*/
|
|
12538
|
+
function signalAllRunnerGroups(signal) {
|
|
12539
|
+
let signalled = 0;
|
|
12540
|
+
for (const [pgid, group] of ownedGroups) try {
|
|
12541
|
+
group.kill(-pgid, signal);
|
|
12542
|
+
signalled += 1;
|
|
12543
|
+
} catch {}
|
|
12544
|
+
return signalled;
|
|
12545
|
+
}
|
|
12546
|
+
/**
|
|
12547
|
+
* Arm the runner's parent-death watchdog before addon code starts loading.
|
|
12548
|
+
*
|
|
12549
|
+
* A parent can die while an addon module or `initialize()` is still pending, so
|
|
12550
|
+
* installing this only after READY leaves exactly the boot window where a detached
|
|
12551
|
+
* runner can become an orphan. Any PPID change means the direct parent died and the
|
|
12552
|
+
* kernel reparented us; checking for a change also covers a non-PID-1 subreaper.
|
|
12553
|
+
*/
|
|
12554
|
+
function startRunnerParentDeathWatchdog(onParentDeath, options = {}) {
|
|
12555
|
+
const initialPpid = options.initialPpid ?? process.ppid;
|
|
12556
|
+
if (!Number.isInteger(initialPpid) || initialPpid <= 1) return null;
|
|
12557
|
+
const readPpid = options.readPpid ?? (() => process.ppid);
|
|
12558
|
+
const intervalMs = options.intervalMs ?? 5e3;
|
|
12559
|
+
let fired = false;
|
|
12560
|
+
const timer = setInterval(() => {
|
|
12561
|
+
if (fired || readPpid() === initialPpid) return;
|
|
12562
|
+
fired = true;
|
|
12563
|
+
clearInterval(timer);
|
|
12564
|
+
onParentDeath();
|
|
12565
|
+
}, intervalMs);
|
|
12566
|
+
return timer;
|
|
12567
|
+
}
|
|
12568
|
+
/**
|
|
12569
|
+
* Last resort for a detached runner whose direct parent disappeared without an
|
|
12570
|
+
* `exit` hook (SIGKILL, native crash). The runner's own pid is also its pgid, so
|
|
12571
|
+
* one SIGKILL removes the runner and every descendant in that group atomically.
|
|
12572
|
+
*
|
|
12573
|
+
* Calling `kill(-process.pid, ...)` from an ordinary, non-group-leading process
|
|
12574
|
+
* returns ESRCH: while this pid exists, no unrelated process group can have the
|
|
12575
|
+
* same id. That makes the failed call a safe compatibility fallback for an older
|
|
12576
|
+
* parent that did not spawn the runner detached.
|
|
12577
|
+
*/
|
|
12578
|
+
function killOwnRunnerGroupAfterParentLoss(platform = process.platform, kill = (target, signal) => {
|
|
12579
|
+
process.kill(target, signal);
|
|
12580
|
+
}) {
|
|
12581
|
+
const pid = process.pid;
|
|
12582
|
+
if (!runnerOwnsProcessGroup(platform) || !Number.isInteger(pid) || pid <= 1) return false;
|
|
12583
|
+
try {
|
|
12584
|
+
kill(-pid, "SIGKILL");
|
|
12585
|
+
return true;
|
|
12586
|
+
} catch {
|
|
12587
|
+
return false;
|
|
12588
|
+
}
|
|
12305
12589
|
}
|
|
12306
12590
|
//#endregion
|
|
12307
12591
|
//#region src/kernel/moleculer/child-cap-dispatch.ts
|
|
@@ -12942,6 +13226,12 @@ Object.defineProperty(exports, "createV8Reclaimer", {
|
|
|
12942
13226
|
return createV8Reclaimer;
|
|
12943
13227
|
}
|
|
12944
13228
|
});
|
|
13229
|
+
Object.defineProperty(exports, "defaultProcessGroupOps", {
|
|
13230
|
+
enumerable: true,
|
|
13231
|
+
get: function() {
|
|
13232
|
+
return defaultProcessGroupOps;
|
|
13233
|
+
}
|
|
13234
|
+
});
|
|
12945
13235
|
Object.defineProperty(exports, "describeRss", {
|
|
12946
13236
|
enumerable: true,
|
|
12947
13237
|
get: function() {
|
|
@@ -13122,6 +13412,12 @@ Object.defineProperty(exports, "isArrayBufferCensusReport", {
|
|
|
13122
13412
|
return isArrayBufferCensusReport;
|
|
13123
13413
|
}
|
|
13124
13414
|
});
|
|
13415
|
+
Object.defineProperty(exports, "killOwnRunnerGroupAfterParentLoss", {
|
|
13416
|
+
enumerable: true,
|
|
13417
|
+
get: function() {
|
|
13418
|
+
return killOwnRunnerGroupAfterParentLoss;
|
|
13419
|
+
}
|
|
13420
|
+
});
|
|
13125
13421
|
Object.defineProperty(exports, "localEndpointPath", {
|
|
13126
13422
|
enumerable: true,
|
|
13127
13423
|
get: function() {
|
|
@@ -13176,6 +13472,12 @@ Object.defineProperty(exports, "readSocketQueues", {
|
|
|
13176
13472
|
return readSocketQueues;
|
|
13177
13473
|
}
|
|
13178
13474
|
});
|
|
13475
|
+
Object.defineProperty(exports, "reapProcessGroup", {
|
|
13476
|
+
enumerable: true,
|
|
13477
|
+
get: function() {
|
|
13478
|
+
return reapProcessGroup;
|
|
13479
|
+
}
|
|
13480
|
+
});
|
|
13179
13481
|
Object.defineProperty(exports, "reclaimIntervalMs", {
|
|
13180
13482
|
enumerable: true,
|
|
13181
13483
|
get: function() {
|
|
@@ -13194,6 +13496,18 @@ Object.defineProperty(exports, "registerEventBusService", {
|
|
|
13194
13496
|
return registerEventBusService;
|
|
13195
13497
|
}
|
|
13196
13498
|
});
|
|
13499
|
+
Object.defineProperty(exports, "registerRunnerGroup", {
|
|
13500
|
+
enumerable: true,
|
|
13501
|
+
get: function() {
|
|
13502
|
+
return registerRunnerGroup;
|
|
13503
|
+
}
|
|
13504
|
+
});
|
|
13505
|
+
Object.defineProperty(exports, "releaseRunnerGroup", {
|
|
13506
|
+
enumerable: true,
|
|
13507
|
+
get: function() {
|
|
13508
|
+
return releaseRunnerGroup;
|
|
13509
|
+
}
|
|
13510
|
+
});
|
|
13197
13511
|
Object.defineProperty(exports, "resolveAddonClass", {
|
|
13198
13512
|
enumerable: true,
|
|
13199
13513
|
get: function() {
|
|
@@ -13218,6 +13532,12 @@ Object.defineProperty(exports, "runNpm", {
|
|
|
13218
13532
|
return runNpm;
|
|
13219
13533
|
}
|
|
13220
13534
|
});
|
|
13535
|
+
Object.defineProperty(exports, "runnerOwnsProcessGroup", {
|
|
13536
|
+
enumerable: true,
|
|
13537
|
+
get: function() {
|
|
13538
|
+
return runnerOwnsProcessGroup;
|
|
13539
|
+
}
|
|
13540
|
+
});
|
|
13221
13541
|
Object.defineProperty(exports, "sampleSocketDirection", {
|
|
13222
13542
|
enumerable: true,
|
|
13223
13543
|
get: function() {
|
|
@@ -13260,6 +13580,12 @@ Object.defineProperty(exports, "shouldReclaim", {
|
|
|
13260
13580
|
return shouldReclaim;
|
|
13261
13581
|
}
|
|
13262
13582
|
});
|
|
13583
|
+
Object.defineProperty(exports, "signalAllRunnerGroups", {
|
|
13584
|
+
enumerable: true,
|
|
13585
|
+
get: function() {
|
|
13586
|
+
return signalAllRunnerGroups;
|
|
13587
|
+
}
|
|
13588
|
+
});
|
|
13263
13589
|
Object.defineProperty(exports, "socketDirectionBytes", {
|
|
13264
13590
|
enumerable: true,
|
|
13265
13591
|
get: function() {
|
|
@@ -13284,6 +13610,12 @@ Object.defineProperty(exports, "startRunnerHeapWatch", {
|
|
|
13284
13610
|
return startRunnerHeapWatch;
|
|
13285
13611
|
}
|
|
13286
13612
|
});
|
|
13613
|
+
Object.defineProperty(exports, "startRunnerParentDeathWatchdog", {
|
|
13614
|
+
enumerable: true,
|
|
13615
|
+
get: function() {
|
|
13616
|
+
return startRunnerParentDeathWatchdog;
|
|
13617
|
+
}
|
|
13618
|
+
});
|
|
13287
13619
|
Object.defineProperty(exports, "strandedMb", {
|
|
13288
13620
|
enumerable: true,
|
|
13289
13621
|
get: function() {
|
|
@@ -13296,6 +13628,12 @@ Object.defineProperty(exports, "subscribePassthrough", {
|
|
|
13296
13628
|
return subscribePassthrough;
|
|
13297
13629
|
}
|
|
13298
13630
|
});
|
|
13631
|
+
Object.defineProperty(exports, "summarizeMembers", {
|
|
13632
|
+
enumerable: true,
|
|
13633
|
+
get: function() {
|
|
13634
|
+
return summarizeMembers;
|
|
13635
|
+
}
|
|
13636
|
+
});
|
|
13299
13637
|
Object.defineProperty(exports, "udsChildLogToWorkerEntry", {
|
|
13300
13638
|
enumerable: true,
|
|
13301
13639
|
get: function() {
|
package/dist/index.js
CHANGED
|
@@ -29,8 +29,8 @@ require("./builtins/system-config/index.js");
|
|
|
29
29
|
const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
|
|
30
30
|
const require_event_loop_stall_monitor = require("./event-loop-stall-monitor-ey4mbofz.js");
|
|
31
31
|
const require_tls$1 = require("./tls-D_wUeOTD.js");
|
|
32
|
-
const require_child_cap_dispatch = require("./child-cap-dispatch-
|
|
33
|
-
const require_resource_monitor = require("./resource-monitor-
|
|
32
|
+
const require_child_cap_dispatch = require("./child-cap-dispatch-Dyn_7ezH.js");
|
|
33
|
+
const require_resource_monitor = require("./resource-monitor-DzNn_IZK.js");
|
|
34
34
|
const require_custom_action_registry = require("./custom-action-registry-acBY3mHA.js");
|
|
35
35
|
let zod = require("zod");
|
|
36
36
|
let node_crypto = require("node:crypto");
|
|
@@ -95015,7 +95015,10 @@ var CRASH_WINDOW_MS = 6e4;
|
|
|
95015
95015
|
var MAX_CRASHES_IN_WINDOW = 5;
|
|
95016
95016
|
function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parentUdsPath) {
|
|
95017
95017
|
const spawnFn = deps?.spawnFn ?? node_child_process.spawn;
|
|
95018
|
+
const groupOps = deps?.processGroups ?? require_child_cap_dispatch.defaultProcessGroupOps;
|
|
95019
|
+
const runnersOwnGroups = require_child_cap_dispatch.runnerOwnsProcessGroup(deps?.platform ?? process.platform);
|
|
95018
95020
|
const processes = /* @__PURE__ */ new Map();
|
|
95021
|
+
const pendingGroupReaps = /* @__PURE__ */ new Map();
|
|
95019
95022
|
const runnerLocks = /* @__PURE__ */ new Map();
|
|
95020
95023
|
function withRunnerLock(runnerId, fn) {
|
|
95021
95024
|
const result = (runnerLocks.get(runnerId) ?? Promise.resolve()).then(fn, fn);
|
|
@@ -95055,6 +95058,49 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95055
95058
|
}, data));
|
|
95056
95059
|
}
|
|
95057
95060
|
/**
|
|
95061
|
+
* One line per reap that found something, or that failed. An empty group is the
|
|
95062
|
+
* normal case after a clean stop and stays quiet at debug. The kernel cannot say
|
|
95063
|
+
* which device a child served — the runner id is the finest attribution it has.
|
|
95064
|
+
*/
|
|
95065
|
+
function logGroupReap(runnerId, result) {
|
|
95066
|
+
const found = result.membersAtExit;
|
|
95067
|
+
const killed = result.killed ?? [];
|
|
95068
|
+
const line = `[${runnerId}] runner group reaped {pgid=${result.pgid}, members=${found === null ? "unreadable" : found.length}${found !== null && found.length > 0 ? ` (${require_child_cap_dispatch.summarizeMembers(found)})` : ""}, sigkilled=${killed.length}${killed.length > 0 ? ` (${require_child_cap_dispatch.summarizeMembers(killed)})` : ""}${result.error !== null ? `, error=${result.error}` : ""}}`;
|
|
95069
|
+
const logger = capturedBroker?.logger;
|
|
95070
|
+
if (result.error !== null || killed.length > 0) if (logger) logger.warn(line);
|
|
95071
|
+
else console.error(line);
|
|
95072
|
+
else if ((found?.length ?? 0) > 0 || found === null) logger?.info(line);
|
|
95073
|
+
else logger?.debug(line);
|
|
95074
|
+
}
|
|
95075
|
+
/**
|
|
95076
|
+
* Reap a runner's process group (D668): SIGTERM, a bounded grace, SIGKILL for what
|
|
95077
|
+
* remains. Memoized on the entry so the `exit` handler and a stop/restart that timed
|
|
95078
|
+
* out waiting for that exit share one reap, and registered in `pendingGroupReaps` so
|
|
95079
|
+
* the next spawn under the same id waits for it. Never rejects.
|
|
95080
|
+
*/
|
|
95081
|
+
function reapRunnerGroup(entry) {
|
|
95082
|
+
if (entry.groupReap !== null) return entry.groupReap;
|
|
95083
|
+
if (!entry.ownsGroup) {
|
|
95084
|
+
entry.groupReap = Promise.resolve();
|
|
95085
|
+
return entry.groupReap;
|
|
95086
|
+
}
|
|
95087
|
+
const reap = require_child_cap_dispatch.reapProcessGroup(entry.pid, groupOps).then((result) => logGroupReap(entry.name, result)).catch((err) => {
|
|
95088
|
+
capturedBroker?.logger.error(`[${entry.name}] runner group reap threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
95089
|
+
}).finally(() => {
|
|
95090
|
+
require_child_cap_dispatch.releaseRunnerGroup(entry.pid);
|
|
95091
|
+
if (pendingGroupReaps.get(entry.name) === tracked) pendingGroupReaps.delete(entry.name);
|
|
95092
|
+
});
|
|
95093
|
+
const previous = pendingGroupReaps.get(entry.name);
|
|
95094
|
+
const tracked = previous ? Promise.all([previous, reap]).then(() => void 0) : reap;
|
|
95095
|
+
pendingGroupReaps.set(entry.name, tracked);
|
|
95096
|
+
entry.groupReap = reap;
|
|
95097
|
+
return reap;
|
|
95098
|
+
}
|
|
95099
|
+
/** Resolves once every reap started for `runnerId` has finished. */
|
|
95100
|
+
async function awaitPredecessorReap(runnerId) {
|
|
95101
|
+
await pendingGroupReaps.get(runnerId);
|
|
95102
|
+
}
|
|
95103
|
+
/**
|
|
95058
95104
|
* Spawn ONE runner subprocess (`addon-runner.js`) that hosts the
|
|
95059
95105
|
* `addons` set sharing a single Moleculer broker. The normal case is
|
|
95060
95106
|
* a size-1 `addons` array — one addon, one process; the media group
|
|
@@ -95103,8 +95149,9 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95103
95149
|
"pipe",
|
|
95104
95150
|
"pipe"
|
|
95105
95151
|
],
|
|
95106
|
-
detached:
|
|
95152
|
+
detached: runnersOwnGroups
|
|
95107
95153
|
});
|
|
95154
|
+
const ownsGroup = runnersOwnGroups && typeof child.pid === "number" && child.pid > 1;
|
|
95108
95155
|
const entry = {
|
|
95109
95156
|
pid: child.pid,
|
|
95110
95157
|
nodeId,
|
|
@@ -95115,16 +95162,23 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95115
95162
|
startedAt: Date.now(),
|
|
95116
95163
|
restartCount: 0,
|
|
95117
95164
|
state: "starting",
|
|
95118
|
-
child
|
|
95165
|
+
child,
|
|
95166
|
+
ownsGroup,
|
|
95167
|
+
groupReap: null
|
|
95119
95168
|
};
|
|
95169
|
+
if (ownsGroup) require_child_cap_dispatch.registerRunnerGroup(entry.pid, {
|
|
95170
|
+
runnerId,
|
|
95171
|
+
kill: groupOps.kill
|
|
95172
|
+
});
|
|
95120
95173
|
child.stdout?.on("data", (data) => {
|
|
95121
|
-
if (data.toString().includes(`READY:${nodeId}`)) entry.state = "running";
|
|
95174
|
+
if (data.toString().includes(`READY:${nodeId}`) && entry.state === "starting") entry.state = "running";
|
|
95122
95175
|
});
|
|
95123
95176
|
child.stderr?.on("data", (data) => {
|
|
95124
95177
|
process.stderr.write(`[${runnerId}] ${data.toString()}`);
|
|
95125
95178
|
});
|
|
95126
95179
|
child.on("exit", (code, signal) => {
|
|
95127
95180
|
console.error(`[process-service EXIT] runner=${runnerId} code=${String(code)} signal=${String(signal)} state=${entry.state}`);
|
|
95181
|
+
const groupReap = reapRunnerGroup(entry);
|
|
95128
95182
|
if (entry.state === "stopped") return;
|
|
95129
95183
|
entry.state = "crashed";
|
|
95130
95184
|
capturedBroker?.logger.error(`[${runnerId}] Process exited (code=${code}, signal=${signal})`);
|
|
@@ -95163,6 +95217,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95163
95217
|
if (capturedBroker) forceMoleculerDisconnect(capturedBroker, entry.nodeId);
|
|
95164
95218
|
setTimeout(() => {
|
|
95165
95219
|
withRunnerLock(runnerId, async () => {
|
|
95220
|
+
await groupReap;
|
|
95166
95221
|
if (processes.has(runnerId)) return;
|
|
95167
95222
|
const restarted = spawnRunner(runnerId, addons, env);
|
|
95168
95223
|
restarted.restartCount = entry.restartCount + 1;
|
|
@@ -95175,10 +95230,12 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95175
95230
|
return {
|
|
95176
95231
|
name: "$process",
|
|
95177
95232
|
actions: {
|
|
95178
|
-
spawnRunner: { handler(ctx) {
|
|
95233
|
+
spawnRunner: { async handler(ctx) {
|
|
95179
95234
|
const { runnerId, addons, env } = ctx.params;
|
|
95180
95235
|
if (processes.has(runnerId)) throw new Error(`Runner "${runnerId}" already running`);
|
|
95181
95236
|
if (!Array.isArray(addons) || addons.length === 0) throw new Error(`spawnRunner requires non-empty addons array`);
|
|
95237
|
+
await awaitPredecessorReap(runnerId);
|
|
95238
|
+
if (processes.has(runnerId)) throw new Error(`Runner "${runnerId}" already running`);
|
|
95182
95239
|
invalidateHeapProfiles(addons);
|
|
95183
95240
|
const entry = spawnRunner(runnerId, addons, env);
|
|
95184
95241
|
return {
|
|
@@ -95215,6 +95272,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95215
95272
|
});
|
|
95216
95273
|
});
|
|
95217
95274
|
}
|
|
95275
|
+
await reapRunnerGroup(target);
|
|
95218
95276
|
processes.delete(name);
|
|
95219
95277
|
crashSupervisor.reset(name);
|
|
95220
95278
|
forceMoleculerDisconnect(ctx.broker, deadNodeId);
|
|
@@ -95251,6 +95309,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95251
95309
|
});
|
|
95252
95310
|
});
|
|
95253
95311
|
}
|
|
95312
|
+
await reapRunnerGroup(target);
|
|
95254
95313
|
processes.delete(name);
|
|
95255
95314
|
crashSupervisor.reset(name);
|
|
95256
95315
|
forceMoleculerDisconnect(ctx.broker, deadNodeId);
|
|
@@ -95310,6 +95369,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95310
95369
|
});
|
|
95311
95370
|
});
|
|
95312
95371
|
}
|
|
95372
|
+
await reapRunnerGroup(entry);
|
|
95313
95373
|
processes.delete(name);
|
|
95314
95374
|
crashSupervisor.reset(name);
|
|
95315
95375
|
invalidateHeapProfiles(runnerAddons);
|
|
@@ -95360,19 +95420,28 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
|
|
|
95360
95420
|
started() {
|
|
95361
95421
|
capturedBroker = this.broker;
|
|
95362
95422
|
},
|
|
95363
|
-
stopped() {
|
|
95364
|
-
const
|
|
95365
|
-
|
|
95423
|
+
async stopped() {
|
|
95424
|
+
const entries = [...processes.values()];
|
|
95425
|
+
await Promise.all(entries.map(async (entry) => {
|
|
95366
95426
|
entry.state = "stopped";
|
|
95367
|
-
entry.child.
|
|
95368
|
-
|
|
95369
|
-
|
|
95370
|
-
|
|
95371
|
-
|
|
95372
|
-
|
|
95373
|
-
|
|
95427
|
+
if (!(entry.child.exitCode !== null || entry.child.signalCode !== null)) {
|
|
95428
|
+
entry.child.kill("SIGTERM");
|
|
95429
|
+
await new Promise((resolve) => {
|
|
95430
|
+
const timer = setTimeout(() => {
|
|
95431
|
+
if (entry.child.exitCode === null && entry.child.signalCode === null) {
|
|
95432
|
+
capturedBroker?.logger.warn(`[${entry.name}] Did not exit on SIGTERM — sending SIGKILL`);
|
|
95433
|
+
entry.child.kill("SIGKILL");
|
|
95434
|
+
}
|
|
95435
|
+
resolve();
|
|
95436
|
+
}, 3e3);
|
|
95437
|
+
entry.child.once("exit", () => {
|
|
95438
|
+
clearTimeout(timer);
|
|
95439
|
+
resolve();
|
|
95440
|
+
});
|
|
95441
|
+
});
|
|
95374
95442
|
}
|
|
95375
|
-
|
|
95443
|
+
await reapRunnerGroup(entry);
|
|
95444
|
+
}));
|
|
95376
95445
|
processes.clear();
|
|
95377
95446
|
capturedBroker = null;
|
|
95378
95447
|
}
|
|
@@ -96259,6 +96328,7 @@ exports.setHubConnected = require_child_cap_dispatch.setHubConnected;
|
|
|
96259
96328
|
exports.setNodeEventInterest = require_child_cap_dispatch.setNodeEventInterest;
|
|
96260
96329
|
exports.setWrapperActive = require_builtins_device_manager_device_manager_addon.setWrapperActive;
|
|
96261
96330
|
exports.shouldReclaim = require_child_cap_dispatch.shouldReclaim;
|
|
96331
|
+
exports.signalAllRunnerGroups = require_child_cap_dispatch.signalAllRunnerGroups;
|
|
96262
96332
|
exports.socketDirectionBytes = require_child_cap_dispatch.socketDirectionBytes;
|
|
96263
96333
|
exports.socketDirectionMessages = require_child_cap_dispatch.socketDirectionMessages;
|
|
96264
96334
|
exports.startEventLoopStallMonitor = require_event_loop_stall_monitor.startEventLoopStallMonitor;
|