@crouter/api 0.3.386 → 0.3.388
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/api/__tests__/integration/client.test.js +97 -0
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +40 -21
- package/dist/core/asset-root.d.ts +7 -0
- package/dist/core/asset-root.js +18 -0
- package/dist/core/canvas/boot-id.d.ts +6 -0
- package/dist/core/canvas/boot-id.js +26 -0
- package/dist/core/canvas/paths.d.ts +72 -0
- package/dist/core/canvas/paths.js +163 -0
- package/dist/core/canvas/pid.d.ts +391 -0
- package/dist/core/canvas/pid.js +948 -0
- package/dist/core/command-plugins/bundle.d.ts +149 -0
- package/dist/core/command-plugins/bundle.js +588 -0
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +51 -0
- package/dist/core/config.d.ts +233 -0
- package/dist/core/config.js +1120 -0
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/errors.d.ts +38 -0
- package/dist/core/errors.js +90 -0
- package/dist/core/events/emit.d.ts +6 -0
- package/dist/core/events/emit.js +42 -0
- package/dist/core/events/envelope.d.ts +2 -0
- package/dist/core/events/envelope.js +84 -0
- package/dist/core/events/errors.d.ts +4 -0
- package/dist/core/events/errors.js +69 -0
- package/dist/core/events/operation-id.d.ts +4 -0
- package/dist/core/events/operation-id.js +24 -0
- package/dist/core/events/serialize.d.ts +4 -0
- package/dist/core/events/serialize.js +199 -0
- package/dist/core/events/source.d.ts +16 -0
- package/dist/core/events/source.js +31 -0
- package/dist/core/events/types.d.ts +68 -0
- package/dist/core/events/types.js +11 -0
- package/dist/core/exclusive-lock.d.ts +34 -0
- package/dist/core/exclusive-lock.js +197 -0
- package/dist/core/fs-utils.d.ts +44 -0
- package/dist/core/fs-utils.js +208 -0
- package/dist/core/help.d.ts +309 -0
- package/dist/core/help.js +406 -0
- package/dist/core/human/page-catalog.d.ts +57 -0
- package/dist/core/human/page-catalog.js +172 -0
- package/dist/core/installed-plugins.d.ts +2 -0
- package/dist/core/installed-plugins.js +79 -0
- package/dist/core/io.d.ts +122 -0
- package/dist/core/io.js +373 -0
- package/dist/core/keybindings/attach-control.d.ts +49 -0
- package/dist/core/keybindings/attach-control.js +42 -0
- package/dist/core/keybindings/catalog.d.ts +18 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/types.d.ts +42 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/layout.d.ts +26 -0
- package/dist/core/layout.js +94 -0
- package/dist/core/locked-file.d.ts +27 -0
- package/dist/core/locked-file.js +118 -0
- package/dist/core/log.d.ts +9 -0
- package/dist/core/log.js +89 -0
- package/dist/core/manifest.d.ts +5 -0
- package/dist/core/manifest.js +15 -0
- package/dist/core/plugin-env.d.ts +8 -0
- package/dist/core/plugin-env.js +31 -0
- package/dist/core/plugin-extensions.d.ts +29 -0
- package/dist/core/plugin-extensions.js +191 -0
- package/dist/core/plugin-swap-lock.d.ts +9 -0
- package/dist/core/plugin-swap-lock.js +31 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +26 -0
- package/dist/core/profiles/env-store.d.ts +22 -0
- package/dist/core/profiles/env-store.js +163 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +120 -0
- package/dist/core/profiles/manifest.js +529 -0
- package/dist/core/rate-limit-scope.d.ts +25 -0
- package/dist/core/rate-limit-scope.js +64 -0
- package/dist/core/render.d.ts +12 -0
- package/dist/core/render.js +138 -0
- package/dist/core/resolver.d.ts +14 -0
- package/dist/core/resolver.js +111 -0
- package/dist/core/runtime/branded-host.d.ts +25 -0
- package/dist/core/runtime/branded-host.js +264 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
- package/dist/core/runtime/broker/daemon-ops.js +177 -0
- package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
- package/dist/core/runtime/broker/signal-stream.js +149 -0
- package/dist/core/scope.d.ts +32 -0
- package/dist/core/scope.js +184 -0
- package/dist/core/scoped-state/db.d.ts +17 -0
- package/dist/core/scoped-state/db.js +247 -0
- package/dist/core/scoped-state/migrate.d.ts +8 -0
- package/dist/core/scoped-state/migrate.js +187 -0
- package/dist/core/scoped-state/paths.d.ts +9 -0
- package/dist/core/scoped-state/paths.js +27 -0
- package/dist/core/scoped-state/profiles.d.ts +27 -0
- package/dist/core/scoped-state/profiles.js +93 -0
- package/dist/core/scoped-state/providers.d.ts +24 -0
- package/dist/core/scoped-state/providers.js +19 -0
- package/dist/core/scoped-state/schema.d.ts +6 -0
- package/dist/core/scoped-state/schema.js +43 -0
- package/dist/core/scoped-state/settings.d.ts +28 -0
- package/dist/core/scoped-state/settings.js +83 -0
- package/dist/core/spaces/open-beneath.d.ts +71 -0
- package/dist/core/spaces/open-beneath.js +581 -0
- package/dist/core/sqlite-statements.d.ts +4 -0
- package/dist/core/sqlite-statements.js +17 -0
- package/dist/core/subscription-state.d.ts +121 -0
- package/dist/core/subscription-state.js +287 -0
- package/dist/core/user-settings.d.ts +377 -0
- package/dist/core/user-settings.js +458 -0
- package/dist/daemon/broker-signals/bus.d.ts +30 -0
- package/dist/daemon/broker-signals/bus.js +87 -0
- package/dist/daemon/manage.d.ts +176 -0
- package/dist/daemon/manage.js +664 -0
- package/dist/daemon/pidfile.d.ts +8 -0
- package/dist/daemon/pidfile.js +37 -0
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/native/linux.d.ts +29 -0
- package/dist/native/linux.js +20 -0
- package/dist/shared/env.d.ts +116 -0
- package/dist/shared/env.js +271 -0
- package/dist/shared/inbox-entry-body.d.ts +22 -0
- package/dist/shared/inbox-entry-body.js +116 -0
- package/dist/shared/working-activity.d.ts +9 -0
- package/dist/shared/working-activity.js +27 -0
- package/dist/types.d.ts +562 -0
- package/dist/types.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
type PsProcessState = 'zombie' | 'gone' | 'live' | 'unknown';
|
|
2
|
+
/** True if a process with `pid` is currently alive. `kill(pid, 0)` provides the
|
|
3
|
+
* initial existence check; EPERM means the foreign process exists. For our
|
|
4
|
+
* processes, the later `ps` read is authoritative because it also distinguishes
|
|
5
|
+
* zombies and processes reaped between the two probes. Only an unavailable
|
|
6
|
+
* `ps` probe fails open to alive. A null/undefined pid reads dead. */
|
|
7
|
+
export declare function isPidAlive(pid: number | null | undefined): boolean;
|
|
8
|
+
/** Existence-only liveness for READ-ONLY display projections: the signal-0
|
|
9
|
+
* probe alone, no `ps`. A zombie reads alive here, which a dashboard column
|
|
10
|
+
* tolerates and which no lifecycle decision may rely on — revive, teardown,
|
|
11
|
+
* and every other authority stays on `isPidAlive`. Exists because the
|
|
12
|
+
* dashboard snapshot runs this per live row per request; a `ps` spawn each
|
|
13
|
+
* time was a third of daemon CPU on a large canvas. */
|
|
14
|
+
export declare function isPidPresent(pid: number | null | undefined): boolean;
|
|
15
|
+
/** THE single precision-aware identity compare, used EVERYWHERE two
|
|
16
|
+
* `composeIdentity` fingerprints are compared (this module's own
|
|
17
|
+
* `recordedPidLiveness`, `captureTeardownSnapshot`'s reuse check, and
|
|
18
|
+
* `killProcessTreePids`'s reuse check) — centralized so no call site can
|
|
19
|
+
* drift from this rule.
|
|
20
|
+
*
|
|
21
|
+
* Two hazards this closes, both in the false-DEAD direction (the dangerous
|
|
22
|
+
* one — a false DEAD lets `reviveNode`'s double-launch guard relaunch a
|
|
23
|
+
* SECOND broker onto a still-live session):
|
|
24
|
+
*
|
|
25
|
+
* 1. `composeIdentity` appends the `#<ticks>` Linux discriminator only when
|
|
26
|
+
* `/proc/<pid>/stat` happens to be readable at capture time — so the SAME
|
|
27
|
+
* live process can be recorded plain `lstart` on one capture and
|
|
28
|
+
* `lstart#ticks` on another (a transient `/proc` read failure, or
|
|
29
|
+
* capturing on a platform/container without procfs). A naive `===` reads
|
|
30
|
+
* that as a reuse (different string) and reports `dead` for a broker that
|
|
31
|
+
* never died.
|
|
32
|
+
* 2. `lstart` is NOT stable for a live process on every host. It is derived
|
|
33
|
+
* from `/proc/stat` btime (wall-clock boot epoch) + starttime jiffies, so
|
|
34
|
+
* a clock correction / suspend-resume shifts btime and re-anchors the
|
|
35
|
+
* SAME live process's `lstart` by seconds between two captures. Observed
|
|
36
|
+
* on Blaxel unikraft guests: pid+ticks identical across probes, `lstart`
|
|
37
|
+
* drifting +81s — a base-first compare then reads the live home-node
|
|
38
|
+
* broker as reused/dead every cycle, driving an uncapped revive loop.
|
|
39
|
+
*
|
|
40
|
+
* A third hazard the base itself once carried: `ticks` (jiffies-since-boot)
|
|
41
|
+
* RESET on every boot and can repeat across boots, so ticks-alone is only
|
|
42
|
+
* safe WITHIN one boot. `composeIdentity` therefore boot-SCOPES the base on
|
|
43
|
+
* Linux-with-procfs — the base IS the kernel `boot_id` UUID — so a
|
|
44
|
+
* new-format identity is `<bootId>#<ticks>` and equality means same boot AND
|
|
45
|
+
* same process. This closes the cross-boot false match at the root, without
|
|
46
|
+
* plumbing boot reconciliation ahead of every caller.
|
|
47
|
+
*
|
|
48
|
+
* Rule: split both on `#`.
|
|
49
|
+
* - BOTH sides carry `#ticks` AND both bases are `boot_id` UUIDs
|
|
50
|
+
* (new-format vs new-format): match iff base AND ticks are equal. A
|
|
51
|
+
* different boot_id with equal ticks is NOT a match (the cross-boot
|
|
52
|
+
* collision).
|
|
53
|
+
* - BOTH sides carry `#ticks` but a base is NOT a boot_id UUID on EITHER
|
|
54
|
+
* side — a LEGACY `lstart#ticks` row recorded before this repo boot-scoped
|
|
55
|
+
* the base, or a Linux host that has procfs ticks but no readable boot_id:
|
|
56
|
+
* ticks DECIDE it alone (the prior landed semantics). This is a NARROW
|
|
57
|
+
* format-migration path so an in-place upgrade never false-kills a live
|
|
58
|
+
* broker whose row still holds `lstart#ticks`; it is NOT a general
|
|
59
|
+
* fallback — new-format vs new-format always demands base+ticks above.
|
|
60
|
+
* - EITHER side lacks a valid `#ticks` suffix (macOS / no procfs / transient
|
|
61
|
+
* `/proc` miss / malformed suffix): fall back to the coarse base —
|
|
62
|
+
* matching iff the bases agree (fail-open to same-process, matching this
|
|
63
|
+
* module's universal discipline: only a POSITIVE mismatch counts as reuse,
|
|
64
|
+
* never an absence of evidence). */
|
|
65
|
+
export declare function identitiesMatch(a: string, b: string): boolean;
|
|
66
|
+
/** Three-valued liveness verdict for a RECORDED node pid, identity-guarded
|
|
67
|
+
* against PID REUSE using the launch-time baseline (`pi_pid_identity`,
|
|
68
|
+
* captured by `recordPid`). On a heavily-forking host — esp. Linux, where
|
|
69
|
+
* low pids recycle fast — a broker's recorded pid can be reused by an
|
|
70
|
+
* unrelated process (or the `ps` probe confirming it can itself
|
|
71
|
+
* intermittently fail) within milliseconds of the broker dying; collapsing
|
|
72
|
+
* "confirmed alive", "confirmed dead/reused", and "couldn't confirm either
|
|
73
|
+
* way" into one boolean forces every caller to fail open to ALIVE on the
|
|
74
|
+
* third case — which, upstream in the daemon's grace-clock bookkeeping, is
|
|
75
|
+
* worse than misreporting one tick: a single such read WIPES the daemon's
|
|
76
|
+
* memory that the node had already been observed dead, restarting the whole
|
|
77
|
+
* grace window from scratch (the confirmed Round-2 crash-revive wedge, in
|
|
78
|
+
* the pre-fleet daemon's liveness bookkeeping). Exposing the third state lets callers
|
|
79
|
+
* decide for themselves whether "can't confirm" should act like alive or
|
|
80
|
+
* dead for their own direction of risk.
|
|
81
|
+
*
|
|
82
|
+
* Without a snapshot this preserves the historical per-pid probes. With a
|
|
83
|
+
* snapshot it classifies process presence, zombie state, and identity from
|
|
84
|
+
* that one authoritative table read:
|
|
85
|
+
* - absent or zombie pid → `'dead'`. Load-bearing: a SIGKILLed detached
|
|
86
|
+
* broker awaiting reap must still be revived rather than read as live.
|
|
87
|
+
* - `expectedIdentity == null` (legacy row / launch capture failed) →
|
|
88
|
+
* `'alive'` (preserve pre-identity behavior — no baseline to check
|
|
89
|
+
* against).
|
|
90
|
+
* - `ps` probe itself failed (`capturePidIdentities` → `null`) →
|
|
91
|
+
* `'indeterminate'`. THIS is the branch the Round-2 fix changes: it used
|
|
92
|
+
* to fail open to `true` (alive) here, which is the confirmed root cause
|
|
93
|
+
* of the wedge.
|
|
94
|
+
* - probe succeeded but has no row for this pid (alive per signal-0 a
|
|
95
|
+
* moment ago, gone now) → `'dead'`.
|
|
96
|
+
* - probe succeeded and the identity is present → `'alive'` iff
|
|
97
|
+
* `identitiesMatch` says so, else `'dead'` (positively reused). */
|
|
98
|
+
export type RecordedPidLiveness = 'alive' | 'dead' | 'indeterminate';
|
|
99
|
+
export declare function recordedPidLiveness(pid: number | null | undefined, expectedIdentity: string | null | undefined, snapshot?: PsLivenessSnapshot | null): RecordedPidLiveness;
|
|
100
|
+
/** SIGTERM a process GROUP by pid (the negative-pid convention) — best-effort,
|
|
101
|
+
* swallowing ESRCH (already gone). Used by cron cancellation, timeout, and
|
|
102
|
+
* stale-lease recovery. Lives beside isPidAlive: canvas/ is
|
|
103
|
+
* the lowest shared layer every process-liveness/-teardown primitive sits at.
|
|
104
|
+
* Guards `pid` before signaling: never hit pid 0
|
|
105
|
+
* (which means "my own group") or a negative/non-integer value. */
|
|
106
|
+
export declare function killProcessGroup(pid: number, signal?: NodeJS.Signals): void;
|
|
107
|
+
/** The kernel `boot_id` (`/proc/sys/kernel/random/boot_id`) is stable for the
|
|
108
|
+
* whole lifetime of this process — it only changes on a kernel boot, which
|
|
109
|
+
* necessarily kills this process — so once a real value is read it is cached
|
|
110
|
+
* for good, rather than re-read for every row of a whole-process-table `ps`
|
|
111
|
+
* scan. Only SUCCESSFUL reads are cached forever: a `null` read is negative
|
|
112
|
+
* for `BOOT_ID_NEGATIVE_TTL_MS`, after which the next call retries — so a
|
|
113
|
+
* transient `/proc` miss on a boot_id-capable host still recovers (never
|
|
114
|
+
* permanently downgraded to legacy `lstart#ticks`), while a genuinely
|
|
115
|
+
* no-boot_id host is not hammered with a synchronous throwing read on every
|
|
116
|
+
* identity composition. `now` is injectable for tests. */
|
|
117
|
+
export declare function makeBootIdCache(read: () => string | null, now?: () => number, negativeTtlMs?: number): () => string | null;
|
|
118
|
+
/** Is `identity` a LEGACY (pre-boot-scoped) baseline — a `<lstart>#<ticks>` row
|
|
119
|
+
* whose base is NOT a boot_id UUID but which DOES carry ticks? These are the
|
|
120
|
+
* rows the one-time startup migration re-records once this platform can
|
|
121
|
+
* compose boot-scoped identities (see `migrateLegacyPidIdentities`). */
|
|
122
|
+
export declare function isLegacyTicksIdentity(identity: string): boolean;
|
|
123
|
+
/** Is `identity` a NEW-format boot-scoped baseline — `<bootId>#<ticks>` with a
|
|
124
|
+
* real boot_id UUID base AND ticks? The startup migration only re-records a
|
|
125
|
+
* legacy row when the pid's CURRENT identity comes out in this shape (i.e. the
|
|
126
|
+
* platform now composes new-format), never otherwise. */
|
|
127
|
+
export declare function isBootScopedIdentity(identity: string): boolean;
|
|
128
|
+
/** Compose the stable, BOOT-SCOPED process-identity fingerprint used
|
|
129
|
+
* everywhere in this module. ALWAYS used to build an identity string, never a
|
|
130
|
+
* raw field directly, so every identity captured anywhere (launch-time
|
|
131
|
+
* `recordPid`, the teardown snapshot, and the escalation-window re-check) is
|
|
132
|
+
* comparable apples-to-apples.
|
|
133
|
+
*
|
|
134
|
+
* Platform capability, not layered fallbacks — the base is the finest STABLE
|
|
135
|
+
* per-boot anchor available:
|
|
136
|
+
* - Linux-with-procfs (the production guest shape): `<bootId>#<ticks>` — the
|
|
137
|
+
* per-boot `boot_id` UUID plus the per-process jiffies-since-boot. Ticks
|
|
138
|
+
* reset per boot and can repeat, so scoping the base to the boot is what
|
|
139
|
+
* makes an equal-ticks compare safe ACROSS boots (see `identitiesMatch`).
|
|
140
|
+
* - Linux-with-procfs but no readable `boot_id` (if that combination
|
|
141
|
+
* exists): `<lstart>#<ticks>` — the prior landed behavior.
|
|
142
|
+
* - No procfs ticks (macOS / containers without procfs): `<lstart>` alone —
|
|
143
|
+
* unchanged coarse behavior. The base is only ever a `boot_id` when a
|
|
144
|
+
* `ticks` discriminator is ALSO present; a bare `boot_id` (shared by every
|
|
145
|
+
* process on the boot) would be a useless per-process identity. */
|
|
146
|
+
export declare function composeIdentity(pid: number, lstart: string, deps?: {
|
|
147
|
+
ticks?: string | null;
|
|
148
|
+
bootId?: string | null;
|
|
149
|
+
}): string;
|
|
150
|
+
/** One authoritative process-table sample. Supervision, broker census,
|
|
151
|
+
* teardown tree walks, identity guards, and zombie detection can all classify
|
|
152
|
+
* from this one read instead of spawning a `ps` probe per pid. `null` means the
|
|
153
|
+
* probe itself failed; callers preserve that as unknown rather than absence. */
|
|
154
|
+
export interface PsLivenessProcess {
|
|
155
|
+
zombie: boolean;
|
|
156
|
+
identity: string;
|
|
157
|
+
command: string;
|
|
158
|
+
}
|
|
159
|
+
export interface PsLivenessSnapshot {
|
|
160
|
+
processes: ReadonlyMap<number, PsLivenessProcess>;
|
|
161
|
+
childrenOf: ReadonlyMap<number, number[]>;
|
|
162
|
+
}
|
|
163
|
+
export declare function captureLivenessSnapshot(): PsLivenessSnapshot | null;
|
|
164
|
+
/** The same sample without blocking the event loop, for callers running inside
|
|
165
|
+
* crtrd's detached work: one `ps` read classifies every pid they care about,
|
|
166
|
+
* where a per-pid `spawnSync` probe would stall broker supervision and the API
|
|
167
|
+
* socket for the duration of each probe. `null` means the probe failed, which
|
|
168
|
+
* every caller must preserve as unknown rather than absence. */
|
|
169
|
+
export declare function captureLivenessSnapshotAsync(): Promise<PsLivenessSnapshot | null>;
|
|
170
|
+
/** Parsed `/proc/<pid>/stat`: `comm` is parenthesized and may itself contain
|
|
171
|
+
* spaces/parens, so fields are counted after the LAST `)`. */
|
|
172
|
+
interface ProcStat {
|
|
173
|
+
comm: string;
|
|
174
|
+
state: string;
|
|
175
|
+
ppid: number;
|
|
176
|
+
starttime: string;
|
|
177
|
+
}
|
|
178
|
+
export declare function parseProcStat(raw: string): ProcStat | null;
|
|
179
|
+
/** `/proc/<pid>/cmdline` (NUL-separated argv) as the single space-joined line
|
|
180
|
+
* `ps -o command=` prints; kernel threads and zombies, whose cmdline is
|
|
181
|
+
* empty, show as `[comm]` exactly as `ps` renders them. */
|
|
182
|
+
export declare function procCommandLine(rawCmdline: string, comm: string): string;
|
|
183
|
+
/** Where a procfs read comes from: the real `/proc` in production, a fake tree
|
|
184
|
+
* plus a fixed boot id in tests. */
|
|
185
|
+
interface ProcSource {
|
|
186
|
+
root: string;
|
|
187
|
+
bootId: () => string | null;
|
|
188
|
+
/** The uid this process runs as; defaults to `process.getuid()`. Tests inject
|
|
189
|
+
* one so a fake tree (owned by the test user) can hold a "foreign" entry. */
|
|
190
|
+
selfUid?: () => number | null;
|
|
191
|
+
}
|
|
192
|
+
declare function procProcessState(pid: number, src?: ProcSource): PsProcessState;
|
|
193
|
+
declare function procIdentities(pids: readonly number[], src?: ProcSource): Map<number, string> | null;
|
|
194
|
+
declare function procCommand(pid: number, src?: ProcSource): string | null;
|
|
195
|
+
/** One whole-table sample from procfs. Skipped as absent, as in a `ps` sample:
|
|
196
|
+
* a pid that vanishes between the listing and its read, and a pid whose stat
|
|
197
|
+
* is refused (EACCES/EPERM — `hidepid=1`) when its `/proc/<pid>` directory is
|
|
198
|
+
* owned by another uid (this user cannot see it, and `ps` would not list it).
|
|
199
|
+
* Every other read failure — including a refusal on a process we own or whose
|
|
200
|
+
* owner we cannot establish — fails the whole sample, which every caller
|
|
201
|
+
* treats as unknown. */
|
|
202
|
+
export declare function readProcfsSnapshot(src?: ProcSource): PsLivenessSnapshot | null;
|
|
203
|
+
/** The same sample without blocking the event loop (crtrd's detached work). */
|
|
204
|
+
export declare function readProcfsSnapshotAsync(src?: ProcSource): Promise<PsLivenessSnapshot | null>;
|
|
205
|
+
/** Every live process's pid, parent pid and full command line, for callers that
|
|
206
|
+
* only match command text or walk parentage (stray-daemon sweep, viewer-pane
|
|
207
|
+
* detection). `null` when no sample could be taken. Same backend rule as the
|
|
208
|
+
* rest of this module: `/proc` where usable, `ps` elsewhere. */
|
|
209
|
+
export declare function listProcessTable(): {
|
|
210
|
+
pid: number;
|
|
211
|
+
ppid: number;
|
|
212
|
+
command: string;
|
|
213
|
+
}[] | null;
|
|
214
|
+
/** Test seam: exercise the `/proc` backend against a fake tree. */
|
|
215
|
+
export declare const procfsBackendForTest: {
|
|
216
|
+
procIdentities: typeof procIdentities;
|
|
217
|
+
procProcessState: typeof procProcessState;
|
|
218
|
+
procCommand: typeof procCommand;
|
|
219
|
+
readProcfsSnapshot: typeof readProcfsSnapshot;
|
|
220
|
+
readProcfsSnapshotAsync: typeof readProcfsSnapshotAsync;
|
|
221
|
+
};
|
|
222
|
+
/** Every transitive descendant of `rootPid`, discovered by walking `ps`'s
|
|
223
|
+
* pid/ppid table (BFS) — NOT process-GROUP membership. The pi SDK's bash tool
|
|
224
|
+
* spawns its shell child with
|
|
225
|
+
* `detached: true` (verified in `@earendil-works/pi-agent-core`'s
|
|
226
|
+
* `harness/env/nodejs.js`), which calls `setsid()` and puts that child in a
|
|
227
|
+
* brand-new process GROUP of its own — so `kill(-rootPid)` never reaches it.
|
|
228
|
+
* `setsid()` changes only SID/PGID, NEVER ppid, so the child (and its own
|
|
229
|
+
* descendants) is still discoverable by walking parentage from `rootPid`, as
|
|
230
|
+
* long as `rootPid`'s OS process is still alive when we walk (a fully-dead
|
|
231
|
+
* parent is reparented away by the kernel immediately on exit, severing this
|
|
232
|
+
* link — the wedged-but-alive broker this fix targets never hits that). Best-
|
|
233
|
+
* effort: returns `[]` (never throws) on any `ps` failure, exactly like
|
|
234
|
+
* `killProcessGroup`'s ESRCH-swallowing. */
|
|
235
|
+
export declare function descendantPids(rootPid: number): number[];
|
|
236
|
+
/** Capture a portable process-identity fingerprint — `ps`'s `lstart` column,
|
|
237
|
+
* the process's full wall-clock start time — for each of `pids`, in ONE
|
|
238
|
+
* batched `ps -p <list>` call. `lstart` is a standard format keyword on BOTH
|
|
239
|
+
* BSD ps (macOS) and procps-ng (Linux), so this needs no `/proc` parsing or
|
|
240
|
+
* other Linux-only machinery. Used to guard against PID REUSE during the
|
|
241
|
+
* multi-second teardown escalation ladder: a
|
|
242
|
+
* captured pid can exit and the OS can recycle it for an unrelated process
|
|
243
|
+
* before the SIGKILL rung fires, and a bare `kill(pid, 0)` / `kill(pid, sig)`
|
|
244
|
+
* cannot tell the difference.
|
|
245
|
+
*
|
|
246
|
+
* Returns `null` — NOT an empty map — when the `ps` PROBE ITSELF failed (it
|
|
247
|
+
* could not be spawned, or was killed by the timeout): a probe failure means
|
|
248
|
+
* we have NO information, which is a fundamentally different outcome from a
|
|
249
|
+
* probe that SUCCEEDED and simply found no row for a given pid (that pid is
|
|
250
|
+
* genuinely gone). Collapsing these two into the same empty map is unsafe:
|
|
251
|
+
* `killProcessTreePids` cannot tell "pid confirmed gone" from "we don't know"
|
|
252
|
+
* without this
|
|
253
|
+
* distinction, and misreading a failed probe as positive proof of
|
|
254
|
+
* absence/reuse either signals a reused stranger or — worse — silently skips
|
|
255
|
+
* reaping a real live orphan. A successful call may still return a map with
|
|
256
|
+
* no entry for some/all of `pids` (those pids have no current row — genuinely
|
|
257
|
+
* gone); that is the valid empty-or-partial case and is NOT a failure.
|
|
258
|
+
*
|
|
259
|
+
* EXIT STATUS IS NOT THE FAILURE SIGNAL. `ps` exits 1 — on BOTH BSD ps and
|
|
260
|
+
* procps-ng — when it matched no process at all, which is precisely the
|
|
261
|
+
* "probe succeeded, that pid is gone" case, and exits 1 with PARTIAL stdout
|
|
262
|
+
* when only some of a batch's pids still exist. Reading a non-zero exit as a
|
|
263
|
+
* probe failure therefore turned every confirmed-dead pid into
|
|
264
|
+
* `'indeterminate'` (`recordedPidLiveness`), so a just-killed broker read as
|
|
265
|
+
* "can't confirm" — never as dead. Callers that fail open on indeterminate
|
|
266
|
+
* (`ensureAttach`'s revive decision, the daemon's exit policy) then treat a
|
|
267
|
+
* dead broker as live forever. Discriminate on whether the PROCESS RAN
|
|
268
|
+
* (`r.error`/`r.signal`) and whether it complained (`stderr`), never on what
|
|
269
|
+
* it exited with: a real argument/usage failure writes a diagnostic to stderr
|
|
270
|
+
* (`ps: process id too large`), while "matched nothing" is silent. */
|
|
271
|
+
export declare function capturePidIdentities(pids: readonly number[]): Map<number, string> | null;
|
|
272
|
+
/** Read one pid's current full command line for a destructive caller that
|
|
273
|
+
* must freshly re-confirm process ownership immediately before signaling.
|
|
274
|
+
* Failure and absence both return null, so uncertainty never authorizes a
|
|
275
|
+
* signal. */
|
|
276
|
+
export declare function capturePidCommand(pid: number): string | null;
|
|
277
|
+
/** The full pre-signal snapshot `teardown()` needs, taken ONCE, from ONE `ps`
|
|
278
|
+
* table: the process TREE rooted at `rootPid`
|
|
279
|
+
* (itself + every transitive descendant) alongside each member's identity
|
|
280
|
+
* fingerprint — PLUS, load-bearing, a launch-time-identity REUSE check on
|
|
281
|
+
* `rootPid` itself before the tree is even walked.
|
|
282
|
+
*
|
|
283
|
+
* The hazard this closes: the PID-reuse guard elsewhere in this module (see
|
|
284
|
+
* `killProcessTreePids`) only ever compared identities captured across the
|
|
285
|
+
* teardown ESCALATION window (a few seconds) — it had no idea whether
|
|
286
|
+
* `rootPid` (the broker pid recorded on the node row at LAUNCH time, possibly
|
|
287
|
+
* hours or days earlier) had ALREADY been recycled for an unrelated process
|
|
288
|
+
* before `teardown()` was ever called. In that case the naive snapshot would
|
|
289
|
+
* capture the STRANGER's current identity as the "expected" baseline and
|
|
290
|
+
* cheerfully SIGTERM/SIGKILL it and whatever the stranger's OWN process tree
|
|
291
|
+
* turns out to be — exactly the wrong-victim risk finding 1 targets.
|
|
292
|
+
*
|
|
293
|
+
* `expectedIdentity` is the LAUNCH-time identity recorded on the node row
|
|
294
|
+
* (`recordPid`'s `capturePidIdentities([pid])` snapshot, taken the instant the
|
|
295
|
+
* broker was spawned/bound) — `null` for a node booted before this field
|
|
296
|
+
* existed, or if that original capture itself failed. When `expectedIdentity`
|
|
297
|
+
* is present AND the CURRENT probe for `rootPid` succeeds AND finds a DIFFERENT
|
|
298
|
+
* identity, `rootPid` is provably a different OS process than the broker this
|
|
299
|
+
* node ever launched: `reused: true`, `tree: []`, `identities: null` — the
|
|
300
|
+
* caller must treat this exactly like "nothing to tear down", and must NOT
|
|
301
|
+
* walk `descendantPids` from this poisoned root (those "descendants" would be
|
|
302
|
+
* the STRANGER's children, not this node's). Every other case (no baseline,
|
|
303
|
+
* the pid is simply gone, the probe itself failed) falls through to the
|
|
304
|
+
* normal walk — fail-open, matching every other guard in this module: a
|
|
305
|
+
* MISMATCH is the only thing that skips, never an absence of evidence. */
|
|
306
|
+
export interface TeardownSnapshot {
|
|
307
|
+
/** `[rootPid, ...descendants]`, or `[]` when `reused` is true (nothing safe
|
|
308
|
+
* to signal) or `rootPid` itself is invalid. */
|
|
309
|
+
tree: number[];
|
|
310
|
+
/** Identity fingerprint per pid in `tree`, from the SAME `ps` table read as
|
|
311
|
+
* the tree walk — or `null` when that probe failed outright (fail-open: the
|
|
312
|
+
* caller's downstream `killProcessTreePids` signals unguarded in that case,
|
|
313
|
+
* same as an omitted `identities` arg). Always `null` when `reused` is true
|
|
314
|
+
* (there is no tree to guard). */
|
|
315
|
+
identities: Map<number, string> | null;
|
|
316
|
+
/** True iff `rootPid`'s CURRENT identity is POSITIVELY known to differ from
|
|
317
|
+
* `expectedIdentity` — the pid was already reused by an unrelated process
|
|
318
|
+
* before this snapshot was taken. Callers must skip ALL signaling when true. */
|
|
319
|
+
reused: boolean;
|
|
320
|
+
}
|
|
321
|
+
export declare function captureTeardownSnapshot(rootPid: number, expectedIdentity: string | null, excludePid?: number): TeardownSnapshot;
|
|
322
|
+
/** Best-effort signal against every pid in an ALREADY-CAPTURED tree snapshot
|
|
323
|
+
* (typically `[rootPid, ...descendantPids(rootPid)]`, taken ONCE by the
|
|
324
|
+
* caller before sending any signal — see the doc on `isProcessTreeAlive` for
|
|
325
|
+
* why re-deriving via `ps` after signaling starts is unsafe). Signals EACH
|
|
326
|
+
* pid's OWN process group (a detached SDK-spawned descendant is its own
|
|
327
|
+
* group leader — the pi bash tool's shell child)
|
|
328
|
+
* PLUS a plain `kill(pid)` on each (belt-and-suspenders for a pid that is
|
|
329
|
+
* NOT its own group leader). Every signal is best-effort — `killProcessGroup`
|
|
330
|
+
* swallows ESRCH and the plain `kill` here does too — so firing this against
|
|
331
|
+
* an already-fully-dead snapshot is a harmless no-op.
|
|
332
|
+
*
|
|
333
|
+
* `identities`, when supplied, is a
|
|
334
|
+
* `capturePidIdentities` snapshot taken alongside the pid list at the SAME
|
|
335
|
+
* moment — `null` if that INITIAL capture's `ps` probe itself failed (see
|
|
336
|
+
* `capturePidIdentities`'s doc). Before signaling EITHER the group or the
|
|
337
|
+
* plain kill for a pid, this re-derives CURRENT identities in one fresh
|
|
338
|
+
* batched call and compares — but ONLY skips on POSITIVE proof of reuse; any
|
|
339
|
+
* failure or absence of evidence falls through to the normal signal path
|
|
340
|
+
* (fail-open, matching this module's unguarded behavior):
|
|
341
|
+
*
|
|
342
|
+
* - baseline identity present AND the current probe SUCCEEDED AND the
|
|
343
|
+
* current identity for this pid is present AND DIFFERENT — the OS has
|
|
344
|
+
* recycled this pid for an unrelated process since the snapshot — SKIP
|
|
345
|
+
* (the sole skip condition; this is the actual reused-pid hazard finding
|
|
346
|
+
* 3 targets).
|
|
347
|
+
* - baseline identity present but the pid is now simply GONE (current
|
|
348
|
+
* probe succeeded, no row for this pid) — it died naturally in the
|
|
349
|
+
* meantime, not reused — SIGNAL as normal (harmless no-op on the plain
|
|
350
|
+
* kill; load-bearing for the group kill, which can still reach a
|
|
351
|
+
* surviving process that shares this now-dead pid's process GROUP).
|
|
352
|
+
* - baseline had NO identity at all for this pid (the pid was ALREADY dead
|
|
353
|
+
* at snapshot time — e.g. a broker that crashed before `teardown()` ever
|
|
354
|
+
* ran) — SIGNAL as normal regardless of the current probe: this is
|
|
355
|
+
* exactly how `killProcessGroup` on an already-dead root still reaches a
|
|
356
|
+
* live process sharing its process group (the pgid persists after the
|
|
357
|
+
* group leader exits; see `host-teardown-process-group.test.ts`'s "dead
|
|
358
|
+
* broker" case).
|
|
359
|
+
* - the INITIAL capture's probe failed outright (`identities === null`) —
|
|
360
|
+
* we have NO baseline to compare against for ANY pid — SIGNAL as normal
|
|
361
|
+
* for the whole list; the CURRENT probe is not even attempted (nothing
|
|
362
|
+
* to compare it to).
|
|
363
|
+
* - the baseline succeeded but the CURRENT probe (taken here) fails — we
|
|
364
|
+
* have NO current information — SIGNAL as normal for the whole list; a
|
|
365
|
+
* failed probe must never be read as "gone" (would risk reaching a
|
|
366
|
+
* reused stranger, the residual risk this guard cannot close) NOR as
|
|
367
|
+
* "reused" (would wrongly disable the reap for every real live pid).
|
|
368
|
+
*
|
|
369
|
+
* Callers that omit `identities` entirely (pre-existing call sites) keep the
|
|
370
|
+
* prior unguarded behavior. */
|
|
371
|
+
export declare function killProcessTreePids(pids: readonly number[], signal?: NodeJS.Signals, identities?: ReadonlyMap<number, string> | null): void;
|
|
372
|
+
/** Is `rootPid`'s process TREE — itself or any CURRENT descendant — still
|
|
373
|
+
* alive, re-deriving the descendant set fresh via `ps` on every call? Safe
|
|
374
|
+
* ONLY before any signal has been sent to this tree. Once a signal kills the
|
|
375
|
+
* root (it's typically its own process-group leader — see `launch()` in
|
|
376
|
+
* host.ts), the kernel reparents any surviving child away from `rootPid`
|
|
377
|
+
* IMMEDIATELY (standard orphan handling, confirmed empirically: a killed
|
|
378
|
+
* parent's child shows `ppid=1` within the same tick, not lazily) —
|
|
379
|
+
* silently "losing" that child from a fresh `descendantPids` walk even
|
|
380
|
+
* though it is still very much alive. Use this ONLY to watch a tree you have
|
|
381
|
+
* not touched yet (e.g. waiting out a graceful-shutdown grace window); once
|
|
382
|
+
* you start signaling, capture the pid list ONCE and check it with
|
|
383
|
+
* `isAnyPidAlive` instead — see `killProcessTreePids`. */
|
|
384
|
+
export declare function isProcessTreeAlive(rootPid: number): boolean;
|
|
385
|
+
/** Is any pid in an ALREADY-CAPTURED pid list (see `killProcessTreePids`)
|
|
386
|
+
* still alive? Pure signal-0 check against the fixed list — NEVER re-walks
|
|
387
|
+
* `ps` — the correct liveness check once you've started signaling a tree
|
|
388
|
+
* (see `isProcessTreeAlive`'s doc for why re-deriving after that point would
|
|
389
|
+
* silently drop a still-alive orphan). */
|
|
390
|
+
export declare function isAnyPidAlive(pids: readonly number[]): boolean;
|
|
391
|
+
export {};
|