@descryy/runtime-controller 0.0.0

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.
Files changed (61) hide show
  1. package/dist/capability-registry.d.ts +24 -0
  2. package/dist/capability-registry.d.ts.map +1 -0
  3. package/dist/capability-registry.js +42 -0
  4. package/dist/capability-registry.js.map +1 -0
  5. package/dist/collector-version.d.ts +10 -0
  6. package/dist/collector-version.d.ts.map +1 -0
  7. package/dist/collector-version.js +12 -0
  8. package/dist/collector-version.js.map +1 -0
  9. package/dist/container-sandbox.d.ts +188 -0
  10. package/dist/container-sandbox.d.ts.map +1 -0
  11. package/dist/container-sandbox.js +233 -0
  12. package/dist/container-sandbox.js.map +1 -0
  13. package/dist/controller.d.ts +162 -0
  14. package/dist/controller.d.ts.map +1 -0
  15. package/dist/controller.js +433 -0
  16. package/dist/controller.js.map +1 -0
  17. package/dist/dependency-version-check.d.ts +90 -0
  18. package/dist/dependency-version-check.d.ts.map +1 -0
  19. package/dist/dependency-version-check.js +121 -0
  20. package/dist/dependency-version-check.js.map +1 -0
  21. package/dist/env.d.ts +16 -0
  22. package/dist/env.d.ts.map +1 -0
  23. package/dist/env.js +18 -0
  24. package/dist/env.js.map +1 -0
  25. package/dist/environment-metadata.d.ts +23 -0
  26. package/dist/environment-metadata.d.ts.map +1 -0
  27. package/dist/environment-metadata.js +80 -0
  28. package/dist/environment-metadata.js.map +1 -0
  29. package/dist/environment-version-check.d.ts +83 -0
  30. package/dist/environment-version-check.d.ts.map +1 -0
  31. package/dist/environment-version-check.js +218 -0
  32. package/dist/environment-version-check.js.map +1 -0
  33. package/dist/execution-safety.d.ts +154 -0
  34. package/dist/execution-safety.d.ts.map +1 -0
  35. package/dist/execution-safety.js +194 -0
  36. package/dist/execution-safety.js.map +1 -0
  37. package/dist/index.d.ts +35 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +14 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/orchestration.d.ts +70 -0
  42. package/dist/orchestration.d.ts.map +1 -0
  43. package/dist/orchestration.js +255 -0
  44. package/dist/orchestration.js.map +1 -0
  45. package/dist/process-collector.d.ts +34 -0
  46. package/dist/process-collector.d.ts.map +1 -0
  47. package/dist/process-collector.js +148 -0
  48. package/dist/process-collector.js.map +1 -0
  49. package/dist/process-manager.d.ts +133 -0
  50. package/dist/process-manager.d.ts.map +1 -0
  51. package/dist/process-manager.js +333 -0
  52. package/dist/process-manager.js.map +1 -0
  53. package/dist/readiness.d.ts +120 -0
  54. package/dist/readiness.d.ts.map +1 -0
  55. package/dist/readiness.js +179 -0
  56. package/dist/readiness.js.map +1 -0
  57. package/dist/sandbox.d.ts +139 -0
  58. package/dist/sandbox.d.ts.map +1 -0
  59. package/dist/sandbox.js +271 -0
  60. package/dist/sandbox.js.map +1 -0
  61. package/package.json +29 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Process manager (plan §5): command execution, working directory,
3
+ * environment variables, process-group tracking, stdout/stderr capture,
4
+ * exit-code capture, cleanup with a shutdown grace period, and orphan-
5
+ * process prevention.
6
+ *
7
+ * Orphan prevention and group-kill use POSIX process groups
8
+ * (`detached: true` + `process.kill(-pid, signal)`), matching this
9
+ * project's Linux development and CI targets. Not portable to Windows as
10
+ * written — out of scope for the V1 fixture-app slice (plan §34).
11
+ */
12
+ import type { FilesystemPolicy, NetworkPolicy, ProcessHandle, ResourceLimits, ServiceAttachConfiguration } from "@descryy/runtime-contracts";
13
+ /**
14
+ * Binds port 0 (OS picks a free one), reads it, releases it immediately.
15
+ * A real gap remains between "free when checked" and "still free when the
16
+ * real service binds it" (RT-024's own class of gap, one layer down) --
17
+ * unavoidable without binding the real service directly to the probe
18
+ * socket, which `spawnProcess`'s child-process model doesn't support, and
19
+ * which no ordinary application would cooperate with anyway.
20
+ *
21
+ * **Examined for a fix and deliberately not "fixed".** Holding the probe
22
+ * socket open for longer does not narrow the window, it widens it: the
23
+ * child cannot bind while we hold the port, and the dominant term is the
24
+ * child's own startup, not our release. Retrying on the child's bind error
25
+ * would mean parsing that error out of process output, which is
26
+ * per-language text and has no place in a language-neutral controller.
27
+ * What was done instead is to make the one symptom legible: `awaitReadiness`
28
+ * now names the endpoint it probed, so a collision reads as "nothing
29
+ * answered on this port" rather than as an unexplained application failure.
30
+ * In practice the window is a handful of milliseconds and readiness always
31
+ * runs afterwards, so a genuine collision fails loudly -- it is just no
32
+ * longer misattributed.
33
+ */
34
+ export declare function allocateEphemeralPort(): Promise<number>;
35
+ export interface SpawnProcessOptions {
36
+ readonly processId: string;
37
+ readonly command: string;
38
+ readonly args?: readonly string[];
39
+ readonly cwd?: string;
40
+ readonly env?: Readonly<Record<string, string | undefined>>;
41
+ /** Joins the resulting ProcessHandle back to ExecutionConfiguration.services (RT-023). Omit for a process with no corresponding named service. */
42
+ readonly serviceName?: string;
43
+ /** The real port this process is bound to, when the caller already knows it (e.g. an ephemeral port it allocated before spawning). Recorded on the handle, not otherwise used by this function. */
44
+ readonly port?: number;
45
+ /** §21's execution boundary (RT-070). Applied via `applyResourceLimits` -- see its own comment for the refuse-rather-than-silently-unconstrained contract. Re-applied on every (re)spawn, including `restart()`. */
46
+ readonly resourceLimits?: ResourceLimits;
47
+ /** §21's execution boundary, filesystem half. Applied via `applySandbox` -- see its own comment. Re-applied on every (re)spawn, including `restart()`. */
48
+ readonly filesystemPolicy?: FilesystemPolicy;
49
+ /** §21's execution boundary, network half. Applied via `applySandbox` -- see its own comment. Re-applied on every (re)spawn, including `restart()`. */
50
+ readonly networkPolicy?: NetworkPolicy;
51
+ /**
52
+ * Selects which real mechanism enforces `filesystemPolicy`/`networkPolicy`.
53
+ * Absent means `"bwrap"` -- exactly today's behavior, unchanged.
54
+ * `"container"` routes through `applyContainerSandbox` (`./container-sandbox.ts`)
55
+ * instead. See `ExecutionConfiguration.sandboxBackend`'s own comment.
56
+ *
57
+ * **Does not compose with `resourceLimits` in this pass**: the container
58
+ * backend applies directly to `command`/`args`, skipping
59
+ * `applyResourceLimits`'s `prlimit` wrap entirely (a minimal container
60
+ * image is not guaranteed to carry `prlimit`, and Docker's own
61
+ * `--memory`/`--cpus`/`--pids-limit` equivalent is not wired up here) --
62
+ * disclosed in `container-sandbox.ts`'s own module doc, not silently
63
+ * dropped.
64
+ */
65
+ readonly sandboxBackend?: "bwrap" | "container";
66
+ }
67
+ export interface ProcessExitInfo {
68
+ readonly exitCode: number | null;
69
+ readonly signal: NodeJS.Signals | null;
70
+ }
71
+ export interface ManagedProcess {
72
+ handle(): ProcessHandle;
73
+ /** Combined stdout+stderr captured so far, in arrival order — the backing store for log-pattern readiness checks. */
74
+ readOutput(): string;
75
+ waitForExit(): Promise<ProcessExitInfo>;
76
+ /** SIGTERM the process group, wait up to `gracePeriodMs`, then SIGKILL if still alive. Idempotent. */
77
+ kill(gracePeriodMs?: number): Promise<void>;
78
+ /**
79
+ * Kills the current process (if still alive — a no-op wait if it already
80
+ * exited on its own) and spawns a fresh one with the SAME `command`/
81
+ * `args`/`cwd`/`env`. `handle()`, `readOutput()` and `waitForExit()` all
82
+ * observe the new process afterward — this is the same `ManagedProcess`
83
+ * object, not a new one, so a caller holding the original reference does
84
+ * not need to re-fetch anything. Readiness is the caller's job, same as
85
+ * after the original `spawnProcess` — this resolves once the new process
86
+ * EXISTS, not once it is ready.
87
+ */
88
+ restart(gracePeriodMs?: number): Promise<void>;
89
+ }
90
+ export declare function spawnProcess(options: SpawnProcessOptions): ManagedProcess;
91
+ /**
92
+ * `process.kill(pid, 0)` sends no signal — it only asks the kernel whether
93
+ * delivery would be possible. `ESRCH` means no such pid; `EPERM` means a
94
+ * pid we don't own but that unambiguously exists, still alive for this
95
+ * purpose. Any other error is treated as "exists" rather than guessed away.
96
+ *
97
+ * A second, deliberate copy of `packages/orchestrator/src/attach-to-running-process.ts`'s
98
+ * function of the same name, not an import of it: `orchestrator` depends on
99
+ * `controller` (see `orchestrator/package.json`), so the reverse import
100
+ * would be circular. Tiny and pure, the same "duplicated because both
101
+ * copies are easy to notice drift" precedent `attach-to-running-jvm-process.ts`'s
102
+ * own `reconstructJvmSourcePath` already sets for exactly this reason.
103
+ */
104
+ export declare function isProcessAlive(pid: number): boolean;
105
+ export interface AttachProcessOptions {
106
+ readonly processId: string;
107
+ readonly attach: ServiceAttachConfiguration;
108
+ readonly serviceName?: string;
109
+ readonly port?: number;
110
+ }
111
+ /**
112
+ * `ManagedProcess` for a service `ExecutionController.run()` attaches to
113
+ * instead of spawning (`ServiceConfiguration.attach`). Reads
114
+ * `attach.logFilePath` fresh on every `readOutput()` call — the same
115
+ * growing-whole-buffer contract a spawned process's in-memory pipe buffer
116
+ * already has (`spawnProcess`'s own `outputBuffer`), so `createPollingProcessOutputSource`
117
+ * (`@descryy/runtime-orchestrator`) needs no attach-specific branch to
118
+ * consume either one.
119
+ *
120
+ * **`kill()` is a deliberate no-op, not a stub.** Descry did not spawn this
121
+ * process and `ATTACH_MODE_SCOPE_DISCLOSURE` (`@descryy/runtime-contracts`)
122
+ * is unconditional: attach mode "never executes code in it or applies
123
+ * changes to it." `ExecutionController`'s cleanup path kills every managed
124
+ * process on failure/timeout/stop — for an attached service that would mean
125
+ * terminating a process this runtime does not own, silently, the exact
126
+ * thing the disclosure promises never happens. Ending observation (no
127
+ * further polling) is the honest equivalent of "stop" here; the target
128
+ * keeps running, as it was before Descry ever attached. `restart()` throws
129
+ * for the same reason: there is no command this runtime could relaunch it
130
+ * with.
131
+ */
132
+ export declare function attachManagedProcess(options: AttachProcessOptions): ManagedProcess;
133
+ //# sourceMappingURL=process-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-manager.d.ts","sourceRoot":"","sources":["../src/process-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAK7I;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC,CAevD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,kJAAkJ;IAClJ,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,mMAAmM;IACnM,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oNAAoN;IACpN,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC,0JAA0J;IAC1J,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,uJAAuJ;IACvJ,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;CACjD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,IAAI,aAAa,CAAC;IACxB,qHAAqH;IACrH,UAAU,IAAI,MAAM,CAAC;IACrB,WAAW,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IACxC,sGAAsG;IACtG,IAAI,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;;;;;OASG;IACH,OAAO,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAID,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,cAAc,CAiKzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;IAC5C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,GAAG,cAAc,CA4ElF"}
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Process manager (plan §5): command execution, working directory,
3
+ * environment variables, process-group tracking, stdout/stderr capture,
4
+ * exit-code capture, cleanup with a shutdown grace period, and orphan-
5
+ * process prevention.
6
+ *
7
+ * Orphan prevention and group-kill use POSIX process groups
8
+ * (`detached: true` + `process.kill(-pid, signal)`), matching this
9
+ * project's Linux development and CI targets. Not portable to Windows as
10
+ * written — out of scope for the V1 fixture-app slice (plan §34).
11
+ */
12
+ import { execFile, spawn } from "node:child_process";
13
+ import { readFileSync } from "node:fs";
14
+ import { createServer } from "node:net";
15
+ import { promisify } from "node:util";
16
+ import { applyResourceLimits } from "./execution-safety.js";
17
+ import { applySandbox } from "./sandbox.js";
18
+ import { applyContainerSandbox } from "./container-sandbox.js";
19
+ /**
20
+ * Binds port 0 (OS picks a free one), reads it, releases it immediately.
21
+ * A real gap remains between "free when checked" and "still free when the
22
+ * real service binds it" (RT-024's own class of gap, one layer down) --
23
+ * unavoidable without binding the real service directly to the probe
24
+ * socket, which `spawnProcess`'s child-process model doesn't support, and
25
+ * which no ordinary application would cooperate with anyway.
26
+ *
27
+ * **Examined for a fix and deliberately not "fixed".** Holding the probe
28
+ * socket open for longer does not narrow the window, it widens it: the
29
+ * child cannot bind while we hold the port, and the dominant term is the
30
+ * child's own startup, not our release. Retrying on the child's bind error
31
+ * would mean parsing that error out of process output, which is
32
+ * per-language text and has no place in a language-neutral controller.
33
+ * What was done instead is to make the one symptom legible: `awaitReadiness`
34
+ * now names the endpoint it probed, so a collision reads as "nothing
35
+ * answered on this port" rather than as an unexplained application failure.
36
+ * In practice the window is a handful of milliseconds and readiness always
37
+ * runs afterwards, so a genuine collision fails loudly -- it is just no
38
+ * longer misattributed.
39
+ */
40
+ export function allocateEphemeralPort() {
41
+ return new Promise((resolve, reject) => {
42
+ const probe = createServer();
43
+ probe.once("error", reject);
44
+ probe.listen(0, () => {
45
+ const address = probe.address();
46
+ if (address === null || typeof address === "string") {
47
+ probe.close();
48
+ reject(new Error("could not determine an ephemeral port"));
49
+ return;
50
+ }
51
+ const { port } = address;
52
+ probe.close(() => resolve(port));
53
+ });
54
+ });
55
+ }
56
+ const DEFAULT_GRACE_PERIOD_MS = 5000;
57
+ export function spawnProcess(options) {
58
+ let startedAt = "";
59
+ let exitedAt = null;
60
+ let exitCode = null;
61
+ let signal = null;
62
+ let outputBuffer = "";
63
+ let killStarted = false;
64
+ let child;
65
+ let settled = false;
66
+ // Set only for the container backend (applyContainerSandbox's own
67
+ // returned name) -- see kill()'s own comment for why the container
68
+ // needs a real, separate stop mechanism from the bwrap path's group-kill.
69
+ let containerName = null;
70
+ let resolveExit;
71
+ let exitPromise;
72
+ // Everything a (re)spawn needs to reset, in one place -- `restart()` calls
73
+ // this again after the previous child has fully exited, and the two call
74
+ // sites (initial construction, restart) must not drift into resetting
75
+ // different subsets of this state.
76
+ function spawnChild() {
77
+ startedAt = new Date().toISOString();
78
+ exitedAt = null;
79
+ exitCode = null;
80
+ signal = null;
81
+ outputBuffer = "";
82
+ killStarted = false;
83
+ settled = false;
84
+ exitPromise = new Promise((resolve) => {
85
+ resolveExit = resolve;
86
+ });
87
+ // "container" skips applyResourceLimits entirely (see SpawnProcessOptions.
88
+ // sandboxBackend's own comment on why prlimit composition isn't attempted
89
+ // for this backend yet) -- every other case (the default, "bwrap")
90
+ // behaves exactly as before this field existed.
91
+ const wrapped = options.sandboxBackend === "container"
92
+ ? applyContainerSandbox({
93
+ interpreterCommand: options.command,
94
+ command: options.command,
95
+ args: options.args ?? [],
96
+ cwd: options.cwd ?? process.cwd(),
97
+ processEnv: { ...process.env, ...options.env },
98
+ ...(options.filesystemPolicy === undefined ? {} : { filesystemPolicy: options.filesystemPolicy }),
99
+ ...(options.networkPolicy === undefined ? {} : { networkPolicy: options.networkPolicy }),
100
+ })
101
+ : applySandbox({
102
+ interpreterCommand: options.command,
103
+ ...applyResourceLimits(options.command, options.args ?? [], options.resourceLimits),
104
+ cwd: options.cwd ?? process.cwd(),
105
+ ...(options.filesystemPolicy === undefined ? {} : { filesystemPolicy: options.filesystemPolicy }),
106
+ ...(options.networkPolicy === undefined ? {} : { networkPolicy: options.networkPolicy }),
107
+ });
108
+ containerName = wrapped.containerName ?? null;
109
+ child = spawn(wrapped.command, wrapped.args, {
110
+ cwd: options.cwd,
111
+ env: { ...process.env, ...options.env },
112
+ detached: true,
113
+ stdio: ["ignore", "pipe", "pipe"],
114
+ });
115
+ child.stdout?.on("data", (chunk) => {
116
+ outputBuffer += chunk.toString("utf8");
117
+ });
118
+ child.stderr?.on("data", (chunk) => {
119
+ outputBuffer += chunk.toString("utf8");
120
+ });
121
+ // Node does not call `spawn()`'s error path synchronously -- a bad
122
+ // command (ENOENT) surfaces later as an `'error'` event, never a thrown
123
+ // exception here, and per Node's own docs `'exit'` may never fire after
124
+ // an `'error'`. An EventEmitter's unhandled `'error'` event crashes the
125
+ // process, and with no `'exit'` guaranteed either, `exitPromise` would
126
+ // otherwise hang forever -- both `'exit'` and `'error'` settle the same
127
+ // promise exactly once, so a failed-to-spawn process reads as an
128
+ // ordinary never-became-ready process (no pid, no output, no readiness
129
+ // success) rather than crashing the caller or hanging its cleanup.
130
+ function settle(info) {
131
+ if (settled)
132
+ return;
133
+ settled = true;
134
+ exitedAt = new Date().toISOString();
135
+ exitCode = info.exitCode;
136
+ signal = info.signal;
137
+ resolveExit(info);
138
+ }
139
+ child.once("exit", (code, exitSignal) => settle({ exitCode: code, signal: exitSignal }));
140
+ child.once("error", () => settle({ exitCode: null, signal: null }));
141
+ }
142
+ spawnChild();
143
+ function handle() {
144
+ return {
145
+ processId: options.processId,
146
+ command: options.command,
147
+ pid: child.pid ?? null,
148
+ startedAt,
149
+ exitedAt,
150
+ exitCode,
151
+ signal,
152
+ serviceName: options.serviceName ?? null,
153
+ port: options.port ?? null,
154
+ };
155
+ }
156
+ async function kill(gracePeriodMs = DEFAULT_GRACE_PERIOD_MS) {
157
+ if (killStarted || exitedAt !== null) {
158
+ await exitPromise;
159
+ return;
160
+ }
161
+ killStarted = true;
162
+ const pid = child.pid;
163
+ if (pid === undefined) {
164
+ return;
165
+ }
166
+ if (containerName !== null) {
167
+ // Real finding from this backend's own escape tests, not assumed up
168
+ // front: the local `docker` CLI process is not the container -- it
169
+ // runs under `dockerd`, a separate process tree entirely. Signaling
170
+ // the CLI's process group (the mechanism below, correct for bwrap,
171
+ // which execs the sandboxed process directly) left a real orphaned
172
+ // container running in testing. `docker stop` targets the actual
173
+ // container by name and is the real stop mechanism for this backend;
174
+ // `--rm` (set by applyContainerSandbox) then removes it once stopped.
175
+ // Still falls through to the group-kill below afterward, as a
176
+ // backstop for the local CLI process itself.
177
+ try {
178
+ await execFileAsync("docker", ["stop", "--time", String(Math.max(1, Math.round(gracePeriodMs / 1000))), containerName]);
179
+ }
180
+ catch {
181
+ // Already stopped/removed (e.g. the process exited on its own
182
+ // first) -- not an error worth surfacing to the caller.
183
+ }
184
+ }
185
+ signalGroup(pid, "SIGTERM");
186
+ const exited = await Promise.race([exitPromise.then(() => true), sleep(gracePeriodMs).then(() => false)]);
187
+ if (!exited) {
188
+ signalGroup(pid, "SIGKILL");
189
+ await exitPromise;
190
+ }
191
+ }
192
+ async function restart(gracePeriodMs = DEFAULT_GRACE_PERIOD_MS) {
193
+ // Safe whether the process is still alive, already exited on its own,
194
+ // or already mid-kill -- `kill()` is idempotent in all three cases and
195
+ // this always waits for the OLD child to be fully gone before
196
+ // `spawnChild()` reassigns `child`/`exitPromise`/etc., so no event from
197
+ // the outgoing process can land on the incoming one's state.
198
+ await kill(gracePeriodMs);
199
+ spawnChild();
200
+ }
201
+ return {
202
+ handle,
203
+ readOutput: () => outputBuffer,
204
+ waitForExit: () => exitPromise,
205
+ kill,
206
+ restart,
207
+ };
208
+ }
209
+ /**
210
+ * `process.kill(pid, 0)` sends no signal — it only asks the kernel whether
211
+ * delivery would be possible. `ESRCH` means no such pid; `EPERM` means a
212
+ * pid we don't own but that unambiguously exists, still alive for this
213
+ * purpose. Any other error is treated as "exists" rather than guessed away.
214
+ *
215
+ * A second, deliberate copy of `packages/orchestrator/src/attach-to-running-process.ts`'s
216
+ * function of the same name, not an import of it: `orchestrator` depends on
217
+ * `controller` (see `orchestrator/package.json`), so the reverse import
218
+ * would be circular. Tiny and pure, the same "duplicated because both
219
+ * copies are easy to notice drift" precedent `attach-to-running-jvm-process.ts`'s
220
+ * own `reconstructJvmSourcePath` already sets for exactly this reason.
221
+ */
222
+ export function isProcessAlive(pid) {
223
+ try {
224
+ process.kill(pid, 0);
225
+ return true;
226
+ }
227
+ catch (error) {
228
+ return error.code === "EPERM";
229
+ }
230
+ }
231
+ const DEFAULT_ATTACH_POLL_INTERVAL_MS = 50;
232
+ /**
233
+ * `ManagedProcess` for a service `ExecutionController.run()` attaches to
234
+ * instead of spawning (`ServiceConfiguration.attach`). Reads
235
+ * `attach.logFilePath` fresh on every `readOutput()` call — the same
236
+ * growing-whole-buffer contract a spawned process's in-memory pipe buffer
237
+ * already has (`spawnProcess`'s own `outputBuffer`), so `createPollingProcessOutputSource`
238
+ * (`@descryy/runtime-orchestrator`) needs no attach-specific branch to
239
+ * consume either one.
240
+ *
241
+ * **`kill()` is a deliberate no-op, not a stub.** Descry did not spawn this
242
+ * process and `ATTACH_MODE_SCOPE_DISCLOSURE` (`@descryy/runtime-contracts`)
243
+ * is unconditional: attach mode "never executes code in it or applies
244
+ * changes to it." `ExecutionController`'s cleanup path kills every managed
245
+ * process on failure/timeout/stop — for an attached service that would mean
246
+ * terminating a process this runtime does not own, silently, the exact
247
+ * thing the disclosure promises never happens. Ending observation (no
248
+ * further polling) is the honest equivalent of "stop" here; the target
249
+ * keeps running, as it was before Descry ever attached. `restart()` throws
250
+ * for the same reason: there is no command this runtime could relaunch it
251
+ * with.
252
+ */
253
+ export function attachManagedProcess(options) {
254
+ const { pid, logFilePath } = options.attach;
255
+ if (!isProcessAlive(pid)) {
256
+ throw new Error(`attachManagedProcess: pid ${String(pid)} for "${options.processId}" is not running — attach requires ` +
257
+ `a target that already exists.`);
258
+ }
259
+ const startedAt = new Date().toISOString();
260
+ let exitedAt = null;
261
+ let exitPromise = null;
262
+ function handle() {
263
+ if (exitedAt === null && !isProcessAlive(pid)) {
264
+ exitedAt = new Date().toISOString();
265
+ }
266
+ return {
267
+ processId: options.processId,
268
+ // No command to report — this runtime never launched this process,
269
+ // so there is nothing here that isn't a guess. Disclosed rather than
270
+ // fabricated.
271
+ command: `<attached: pid ${String(pid)}, ${logFilePath}>`,
272
+ pid,
273
+ startedAt,
274
+ exitedAt,
275
+ // Neither is observable for a process we did not spawn and do not
276
+ // reap — see this function's own module doc on why `kill()` never
277
+ // signals it, which is exactly what would let us learn either.
278
+ exitCode: null,
279
+ signal: null,
280
+ serviceName: options.serviceName ?? null,
281
+ port: options.port ?? null,
282
+ attached: true,
283
+ };
284
+ }
285
+ function readOutput() {
286
+ try {
287
+ return readFileSync(logFilePath, "utf8");
288
+ }
289
+ catch (error) {
290
+ const detail = error instanceof Error ? error.message : String(error);
291
+ throw new Error(`attachManagedProcess: could not read log file "${logFilePath}" for pid ${String(pid)}: ${detail}`);
292
+ }
293
+ }
294
+ function waitForExit() {
295
+ exitPromise ??= new Promise((resolve) => {
296
+ const poll = () => {
297
+ if (!isProcessAlive(pid)) {
298
+ exitedAt ??= new Date().toISOString();
299
+ resolve({ exitCode: null, signal: null });
300
+ return;
301
+ }
302
+ setTimeout(poll, DEFAULT_ATTACH_POLL_INTERVAL_MS);
303
+ };
304
+ poll();
305
+ });
306
+ return exitPromise;
307
+ }
308
+ function kill() {
309
+ // Deliberate no-op — see this function's own module doc.
310
+ return Promise.resolve();
311
+ }
312
+ function restart() {
313
+ return Promise.reject(new Error(`attachManagedProcess: cannot restart an attached process (pid ${String(pid)}) — Descry did not spawn ` +
314
+ `it and has no command to relaunch it with.`));
315
+ }
316
+ return { handle, readOutput, waitForExit, kill, restart };
317
+ }
318
+ const execFileAsync = promisify(execFile);
319
+ function signalGroup(pid, signal) {
320
+ try {
321
+ // Negative pid targets the whole process group (the child is its
322
+ // leader, per `detached: true`), not just the immediate process --
323
+ // this is the orphan-prevention mechanism.
324
+ process.kill(-pid, signal);
325
+ }
326
+ catch {
327
+ // Group already gone -- nothing to signal, not an error.
328
+ }
329
+ }
330
+ function sleep(ms) {
331
+ return new Promise((resolve) => setTimeout(resolve, ms));
332
+ }
333
+ //# sourceMappingURL=process-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-manager.js","sourceRoot":"","sources":["../src/process-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,YAAY,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC5B,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE;YACnB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;YAChC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACpD,KAAK,CAAC,KAAK,EAAE,CAAC;gBACd,MAAM,CAAC,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC,CAAC;gBAC3D,OAAO;YACT,CAAC;YACD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;YACzB,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AA4DD,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAErC,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,IAAI,SAAS,GAAG,EAAE,CAAC;IACnB,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,MAAM,GAA0B,IAAI,CAAC;IACzC,IAAI,YAAY,GAAG,EAAE,CAAC;IACtB,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,IAAI,KAAmB,CAAC;IACxB,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,kEAAkE;IAClE,mEAAmE;IACnE,0EAA0E;IAC1E,IAAI,aAAa,GAAkB,IAAI,CAAC;IACxC,IAAI,WAA6C,CAAC;IAClD,IAAI,WAAqC,CAAC;IAE1C,2EAA2E;IAC3E,yEAAyE;IACzE,sEAAsE;IACtE,mCAAmC;IACnC,SAAS,UAAU;QACjB,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,QAAQ,GAAG,IAAI,CAAC;QAChB,QAAQ,GAAG,IAAI,CAAC;QAChB,MAAM,GAAG,IAAI,CAAC;QACd,YAAY,GAAG,EAAE,CAAC;QAClB,WAAW,GAAG,KAAK,CAAC;QACpB,OAAO,GAAG,KAAK,CAAC;QAChB,WAAW,GAAG,IAAI,OAAO,CAAkB,CAAC,OAAO,EAAE,EAAE;YACrD,WAAW,GAAG,OAAO,CAAC;QACxB,CAAC,CAAC,CAAC;QAEH,2EAA2E;QAC3E,0EAA0E;QAC1E,mEAAmE;QACnE,gDAAgD;QAChD,MAAM,OAAO,GACX,OAAO,CAAC,cAAc,KAAK,WAAW;YACpC,CAAC,CAAC,qBAAqB,CAAC;gBACpB,kBAAkB,EAAE,OAAO,CAAC,OAAO;gBACnC,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,EAAE;gBACxB,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;gBACjC,UAAU,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE;gBAC9C,GAAG,CAAC,OAAO,CAAC,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,EAAE,CAAC;gBACjG,GAAG,CAAC,OAAO,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;aACzF,CAAC;YACJ,CAAC,CAAC,YAAY,CAAC;gBACX,kBAAkB,EAAE,OAAO,CAAC,OAAO;gBACnC,GAAG,mBAAmB,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,IAAI,EAAE,EAAE,OAAO,CAAC,cAAc,CAAC;gBACnF,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;gBACjC,GAAG,CAAC,OAAO,CAAC,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,EAAE,CAAC;gBACjG,GAAG,CAAC,OAAO,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC;aACzF,CAAC,CAAC;QACT,aAAa,GAAI,OAA+C,CAAC,aAAa,IAAI,IAAI,CAAC;QACvF,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE;YAC3C,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE;YACvC,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;SAClC,CAAC,CAAC;QAEH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,YAAY,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,YAAY,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QAEH,mEAAmE;QACnE,wEAAwE;QACxE,wEAAwE;QACxE,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,iEAAiE;QACjE,uEAAuE;QACvE,mEAAmE;QACnE,SAAS,MAAM,CAAC,IAAqB;YACnC,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACpC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;YACzB,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YACrB,WAAW,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;QACzF,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,UAAU,EAAE,CAAC;IAEb,SAAS,MAAM;QACb,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,IAAI;YACtB,SAAS;YACT,QAAQ;YACR,QAAQ;YACR,MAAM;YACN,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;YACxC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI;SAC3B,CAAC;IACJ,CAAC;IAED,KAAK,UAAU,IAAI,CAAC,aAAa,GAAG,uBAAuB;QACzD,IAAI,WAAW,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACrC,MAAM,WAAW,CAAC;YAClB,OAAO;QACT,CAAC;QACD,WAAW,GAAG,IAAI,CAAC;QACnB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;QACtB,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QAED,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;YAC3B,oEAAoE;YACpE,mEAAmE;YACnE,oEAAoE;YACpE,mEAAmE;YACnE,mEAAmE;YACnE,iEAAiE;YACjE,qEAAqE;YACrE,sEAAsE;YACtE,8DAA8D;YAC9D,6CAA6C;YAC7C,IAAI,CAAC;gBACH,MAAM,aAAa,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;YAC1H,CAAC;YAAC,MAAM,CAAC;gBACP,8DAA8D;gBAC9D,wDAAwD;YAC1D,CAAC;QACH,CAAC;QAED,WAAW,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC1G,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,WAAW,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC5B,MAAM,WAAW,CAAC;QACpB,CAAC;IACH,CAAC;IAED,KAAK,UAAU,OAAO,CAAC,aAAa,GAAG,uBAAuB;QAC5D,sEAAsE;QACtE,uEAAuE;QACvE,8DAA8D;QAC9D,wEAAwE;QACxE,6DAA6D;QAC7D,MAAM,IAAI,CAAC,aAAa,CAAC,CAAC;QAC1B,UAAU,EAAE,CAAC;IACf,CAAC;IAED,OAAO;QACL,MAAM;QACN,UAAU,EAAE,GAAG,EAAE,CAAC,YAAY;QAC9B,WAAW,EAAE,GAAG,EAAE,CAAC,WAAW;QAC9B,IAAI;QACJ,OAAO;KACR,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;IAC3D,CAAC;AACH,CAAC;AASD,MAAM,+BAA+B,GAAG,EAAE,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA6B;IAChE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAC5C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,6BAA6B,MAAM,CAAC,GAAG,CAAC,SAAS,OAAO,CAAC,SAAS,qCAAqC;YACrG,+BAA+B,CAClC,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,WAAW,GAAoC,IAAI,CAAC;IAExD,SAAS,MAAM;QACb,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9C,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACtC,CAAC;QACD,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,mEAAmE;YACnE,qEAAqE;YACrE,cAAc;YACd,OAAO,EAAE,kBAAkB,MAAM,CAAC,GAAG,CAAC,KAAK,WAAW,GAAG;YACzD,GAAG;YACH,SAAS;YACT,QAAQ;YACR,kEAAkE;YAClE,kEAAkE;YAClE,+DAA+D;YAC/D,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,IAAI;YACZ,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI;YACxC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI;YAC1B,QAAQ,EAAE,IAAI;SACf,CAAC;IACJ,CAAC;IAED,SAAS,UAAU;QACjB,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,MAAM,IAAI,KAAK,CAAC,kDAAkD,WAAW,aAAa,MAAM,CAAC,GAAG,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;QACtH,CAAC;IACH,CAAC;IAED,SAAS,WAAW;QAClB,WAAW,KAAK,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YACtC,MAAM,IAAI,GAAG,GAAS,EAAE;gBACtB,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;oBACzB,QAAQ,KAAK,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;oBACtC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;oBAC1C,OAAO;gBACT,CAAC;gBACD,UAAU,CAAC,IAAI,EAAE,+BAA+B,CAAC,CAAC;YACpD,CAAC,CAAC;YACF,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;QACH,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,SAAS,IAAI;QACX,yDAAyD;QACzD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,SAAS,OAAO;QACd,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,KAAK,CACP,iEAAiE,MAAM,CAAC,GAAG,CAAC,2BAA2B;YACrG,4CAA4C,CAC/C,CACF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC5D,CAAC;AAED,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,SAAS,WAAW,CAAC,GAAW,EAAE,MAAsB;IACtD,IAAI,CAAC;QACH,iEAAiE;QACjE,mEAAmE;QACnE,2CAA2C;QAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,yDAAyD;IAC3D,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Readiness (plan §6): "Readiness must not equal 'process exists.'" —
3
+ * supported strategies are HTTP health check, TCP/port check, a known log
4
+ * pattern, a real subprocess command, or a caller-supplied hook (which
5
+ * covers the plan's "framework readiness hook" and "user-defined readiness"
6
+ * — two names for the same shape, a caller-provided async predicate,
7
+ * consolidated into one mechanism rather than two near-identical ones).
8
+ *
9
+ * **`command` is not `custom-hook` (RT-193).** An earlier version of this
10
+ * comment folded the plan's "command check" into `custom-hook` too, on the
11
+ * reasoning that both are "a caller-provided check." That collapsed a real
12
+ * distinction: `custom-hook` runs a caller's own in-process predicate,
13
+ * while `command` runs an actual subprocess and reads its actual exit
14
+ * code — the same "declared, not inferred" shape `ServiceConfiguration`'s
15
+ * own `command` field already uses, one layer over. A caller with a real
16
+ * CLI health check (`pg_isready`, a framework's own readiness script) had
17
+ * no way to express that without either wrapping it in a hand-written
18
+ * `custom-hook` predicate that shells out itself, or reimplementing the
19
+ * subprocess plumbing per caller — `command` is that plumbing, built once.
20
+ *
21
+ * The plan requires recording which mechanism succeeded — `ReadinessResult`
22
+ * makes that a field, not something inferred after the fact.
23
+ *
24
+ * **What `ready: true` does and does not claim (RT-024).** All four
25
+ * mechanisms answer one question — is something listening / responding
26
+ * where expected — and none answer whether it is the process *this*
27
+ * `spawnProcess` call produced. A stale listener from an unrelated, already-
28
+ * dead run satisfies `http`/`tcp-port` identically to a correct one; nothing
29
+ * here can tell them apart. `log-pattern` only carries the stronger,
30
+ * this-run-specific guarantee when its `read` is wired to *this* execution's
31
+ * own `ManagedProcess.readOutput()` — the type does not enforce that wiring,
32
+ * it is a caller discipline. `custom-hook` inherits whatever the caller's
33
+ * predicate actually checks.
34
+ *
35
+ * Callers that bind a fixed, guessable host:port are exposed to this gap;
36
+ * an ephemeral port closes it by construction (nothing else can already be
37
+ * bound there) rather than by detection, and is the default recommendation
38
+ * over a startup-nonce scheme, which only earns its extra machinery when a
39
+ * fixed port is genuinely unavoidable (a contract with an external tool).
40
+ * See `descry-runtime`'s `DECISIONS.md` RT-024 for how this was found —
41
+ * a real 30-second collector timeout that looked like a Playwright/DOM bug
42
+ * and was actually a correct wait against an orphaned process from a
43
+ * different, already-dead session.
44
+ *
45
+ * **A single probe attempt cannot outlive `timeoutMs` (RT-030).** `http`
46
+ * and `tcp-port` each bind their I/O to the *remaining* time until
47
+ * `deadline`, not a fresh per-attempt budget — a target that accepts a
48
+ * connection and never responds no longer lets one probe attempt run past
49
+ * the caller's own deadline. Measured, not theoretical: an unbound `fetch`
50
+ * here let `awaitReadiness` run past its own `timeoutMs` (>8s observed
51
+ * against a 3s budget). `command` honors the same discipline, the same
52
+ * way: `execFile`'s own `timeout` option is set to the remaining time until
53
+ * `deadline`, so a command that hangs instead of exiting is killed rather
54
+ * than left to run past the caller's own budget — it owns a real OS
55
+ * resource (a child process) exactly like the http/tcp-port sockets do, so
56
+ * it gets the same treatment. `custom-hook` is not wrapped the same way — a
57
+ * caller-supplied predicate owns its own timeout behavior, same as every
58
+ * other `Collector`-shaped callback contract in this repo (`collector.ts`'s
59
+ * own docs put "must resolve, not hang" on the implementer); imposing a
60
+ * surprise external cancellation on arbitrary caller code would be a
61
+ * different, larger decision than fixing the mechanisms that own a real OS
62
+ * resource this module can actually clean up.
63
+ */
64
+ export declare const READINESS_MECHANISMS: readonly ["http", "tcp-port", "log-pattern", "command", "custom-hook"];
65
+ export type ReadinessMechanism = (typeof READINESS_MECHANISMS)[number];
66
+ export interface HttpReadinessCheck {
67
+ readonly kind: "http";
68
+ readonly url: string;
69
+ readonly expectedStatus?: number;
70
+ }
71
+ export interface TcpPortReadinessCheck {
72
+ readonly kind: "tcp-port";
73
+ readonly host: string;
74
+ readonly port: number;
75
+ }
76
+ export interface LogPatternReadinessCheck {
77
+ readonly kind: "log-pattern";
78
+ /**
79
+ * Tested against `read()`'s full return value on every poll (RT-032's
80
+ * `awaitReadiness` loop). `read` is documented to wire to
81
+ * `ManagedProcess.readOutput()`, which returns the entire accumulated
82
+ * stdout+stderr buffer with **no size cap** — a caller-supplied pattern
83
+ * with an unbounded quantifier immediately before a literal that can be
84
+ * absent (the exact shape RT-069 found and fixed in
85
+ * `evidence-store/redaction.ts`'s `EMAIL_PATTERN`, and RT-070 also found
86
+ * and fixed in this package's own `PORT_PLACEHOLDER`) would run that
87
+ * catastrophic-backtracking cost against a growing buffer on every single
88
+ * poll. Nothing in this repo currently supplies such a pattern here, so
89
+ * this is a disclosed risk at the call site rather than a fixed one — the
90
+ * regex is the caller's, not this module's, to bound.
91
+ */
92
+ readonly pattern: RegExp;
93
+ /** Pulls currently-buffered output — wire this to a ManagedProcess's `readOutput`. */
94
+ readonly read: () => string;
95
+ }
96
+ export interface CommandReadinessCheck {
97
+ readonly kind: "command";
98
+ readonly command: string;
99
+ readonly args?: readonly string[];
100
+ /** Defaults to this process's own `cwd` (`execFile`'s own default) when omitted. */
101
+ readonly cwd?: string;
102
+ }
103
+ export interface CustomHookReadinessCheck {
104
+ readonly kind: "custom-hook";
105
+ readonly check: () => Promise<boolean>;
106
+ }
107
+ export type ReadinessCheck = HttpReadinessCheck | TcpPortReadinessCheck | LogPatternReadinessCheck | CommandReadinessCheck | CustomHookReadinessCheck;
108
+ export interface ReadinessResult {
109
+ readonly ready: boolean;
110
+ /** Which mechanism succeeded — null when none did within the timeout. */
111
+ readonly mechanism: ReadinessMechanism | null;
112
+ readonly elapsedMs: number;
113
+ readonly reason: string | null;
114
+ }
115
+ export interface AwaitReadinessOptions {
116
+ readonly timeoutMs: number;
117
+ readonly pollIntervalMs?: number;
118
+ }
119
+ export declare function awaitReadiness(checks: readonly ReadinessCheck[], options: AwaitReadinessOptions): Promise<ReadinessResult>;
120
+ //# sourceMappingURL=readiness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAKH,eAAO,MAAM,oBAAoB,wEAAyE,CAAC;AAC3G,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oFAAoF;IACpF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,qBAAqB,GACrB,wBAAwB,GACxB,qBAAqB,GACrB,wBAAwB,CAAC;AAE7B,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAwC1B"}