@descryy/runtime-orchestrator 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 (49) hide show
  1. package/dist/attach-to-running-jvm-process.d.ts +139 -0
  2. package/dist/attach-to-running-jvm-process.d.ts.map +1 -0
  3. package/dist/attach-to-running-jvm-process.js +338 -0
  4. package/dist/attach-to-running-jvm-process.js.map +1 -0
  5. package/dist/attach-to-running-node-process.d.ts +138 -0
  6. package/dist/attach-to-running-node-process.d.ts.map +1 -0
  7. package/dist/attach-to-running-node-process.js +315 -0
  8. package/dist/attach-to-running-node-process.js.map +1 -0
  9. package/dist/attach-to-running-process.d.ts +70 -0
  10. package/dist/attach-to-running-process.d.ts.map +1 -0
  11. package/dist/attach-to-running-process.js +90 -0
  12. package/dist/attach-to-running-process.js.map +1 -0
  13. package/dist/cdp-client.d.ts +37 -0
  14. package/dist/cdp-client.d.ts.map +1 -0
  15. package/dist/cdp-client.js +111 -0
  16. package/dist/cdp-client.js.map +1 -0
  17. package/dist/cgroup-partial-restriction.d.ts +127 -0
  18. package/dist/cgroup-partial-restriction.d.ts.map +1 -0
  19. package/dist/cgroup-partial-restriction.js +236 -0
  20. package/dist/cgroup-partial-restriction.js.map +1 -0
  21. package/dist/collector-version.d.ts +12 -0
  22. package/dist/collector-version.d.ts.map +1 -0
  23. package/dist/collector-version.js +14 -0
  24. package/dist/collector-version.js.map +1 -0
  25. package/dist/index.d.ts +19 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +10 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/instrumented-execution.d.ts +111 -0
  30. package/dist/instrumented-execution.d.ts.map +1 -0
  31. package/dist/instrumented-execution.js +363 -0
  32. package/dist/instrumented-execution.js.map +1 -0
  33. package/dist/jvm-agent/build.d.ts +72 -0
  34. package/dist/jvm-agent/build.d.ts.map +1 -0
  35. package/dist/jvm-agent/build.js +156 -0
  36. package/dist/jvm-agent/build.js.map +1 -0
  37. package/dist/polling-output-source.d.ts +28 -0
  38. package/dist/polling-output-source.d.ts.map +1 -0
  39. package/dist/polling-output-source.js +73 -0
  40. package/dist/polling-output-source.js.map +1 -0
  41. package/dist/profile-backend-observation.d.ts +94 -0
  42. package/dist/profile-backend-observation.d.ts.map +1 -0
  43. package/dist/profile-backend-observation.js +136 -0
  44. package/dist/profile-backend-observation.js.map +1 -0
  45. package/dist/respawn-and-supervise.d.ts +119 -0
  46. package/dist/respawn-and-supervise.d.ts.map +1 -0
  47. package/dist/respawn-and-supervise.js +212 -0
  48. package/dist/respawn-and-supervise.js.map +1 -0
  49. package/package.json +45 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Retroactive attach to an already-running **Node** process, via its own
3
+ * inspector protocol — the specific, buildable slice
4
+ * `documents/decisions-inbox/RT-attach-to-running-process-scope.md`
5
+ * deliberately did not build. That entry's own "deliberately not built"
6
+ * list names it directly: "Node's `SIGUSR1` inspector activation + a CDP
7
+ * WebSocket client — real, but Node-only and a new protocol surface, not
8
+ * built this pass." This is that pass, for Node only — the other two items
9
+ * on that list (JVM Attach API, ptrace) remain out of scope, unchanged.
10
+ *
11
+ * **What makes this genuinely retroactive, unlike `attachToRunningProcess`
12
+ * (the file-based sibling in this same package).** That one needs the
13
+ * target to have already been told, at its own launch time, to write
14
+ * output somewhere reachable (`> server.log 2>&1`) — narrower than "attach
15
+ * to anything already running." This one needs nothing declared at launch:
16
+ * `SIGUSR1` (POSIX only; Node's own signal handler, not something this
17
+ * process installs) turns on the target's inspector **after the fact**, on
18
+ * a process that was never started with `--inspect` and never expected to
19
+ * be debugged. From that moment its debugger discovery endpoint is
20
+ * reachable at `http://127.0.0.1:<port>/json`, by the target itself, not by
21
+ * anything this collector primed in advance.
22
+ *
23
+ * **The one real limit, disclosed rather than hidden.** `SIGUSR1` does not
24
+ * report back which port the inspector actually bound — Node prints
25
+ * `Debugger listening on ws://host:port/<uuid>` to the target's own
26
+ * *stderr*, which is exactly the channel this module's own doc (and RT-024
27
+ * before it) says cannot be read for a process we did not spawn. So
28
+ * discovery is: try the default port (9229, Node's own default and the one
29
+ * `SIGUSR1` binds to when nothing else is already listening there), and if
30
+ * a caller already knows the target was configured for a different port
31
+ * (`--inspect-port`, `NODE_OPTIONS`), they supply it. **If the port is
32
+ * genuinely unknown and is not 9229, this module cannot discover it** — not
33
+ * attempted, not guessed at by scanning a port range, which would risk
34
+ * attaching to the wrong process's inspector entirely.
35
+ *
36
+ * **CDP client: written here, not reused, because nothing reusable
37
+ * exists.** `packages/browser`'s console collector speaks CDP only through
38
+ * Playwright's own browser-automation abstraction (`page.on("console")`),
39
+ * which assumes a `Target`/`Page`-domain browser session — a Node inspector
40
+ * session has neither domain, being a single top-level `Runtime` session
41
+ * instead. `cdp-client.ts` in this package is the actual reusable piece:
42
+ * protocol-family-generic, so a future browser-side raw-CDP need could
43
+ * reuse it too, instead of a third client getting written.
44
+ *
45
+ * **Measured: a freshly-spawned target has a brief window, sometimes under
46
+ * 100ms, before Node installs its own `SIGUSR1` handler** — a signal sent
47
+ * inside that window hits POSIX's default action for an unhandled
48
+ * `SIGUSR1` (terminate) instead of toggling the inspector, killing the
49
+ * target rather than attaching to it. Irrelevant to this module's actual
50
+ * purpose — attaching to something genuinely already running, which by
51
+ * definition has been alive far longer than that window — but real, and
52
+ * worth naming for a caller tempted to attach immediately after spawning
53
+ * something themselves.
54
+ *
55
+ * **Measured side effect: attaching changes the target's crash behaviour.**
56
+ * Once a CDP client is connected, V8 no longer terminates the process
57
+ * immediately on an uncaught exception — it prints "Waiting for the
58
+ * debugger to disconnect..." to stderr and blocks, exactly as it would
59
+ * under a launched `--inspect-brk` session, so a human debugger gets to
60
+ * inspect the paused, crashed state before the process actually exits.
61
+ * Measured directly (a naive test that waited for the child's own `exit`
62
+ * event after attach hung indefinitely) rather than assumed from docs. This
63
+ * is standard CDP/V8 behaviour, not a defect this collector introduces, and
64
+ * not silently worked around: the process resumes its exit exactly when
65
+ * `stop()` closes the CDP session, which is also when the exception's own
66
+ * evidence has already been captured, so nothing about the timing costs a
67
+ * caller the evidence. A caller relying on "did the target exit yet?" as a
68
+ * signal must call `stop()` first.
69
+ *
70
+ * **What is captured, and under which vocabulary.** `Runtime.consoleAPICalled`
71
+ * → `CONSOLE_MESSAGE`, `Runtime.exceptionThrown` → `EXCEPTION` — the same
72
+ * two `RuntimeEventType`s `browser-console-collector.ts` already claims for
73
+ * the analogous browser-page concepts, because they are the same concepts
74
+ * (console output; an uncaught exception), observed on a different kind of
75
+ * process. `source: "backend-process"`, not a new source value — this is a
76
+ * backend process being observed, and `EvidenceSource` already names what
77
+ * was observed rather than which mechanism did it (`inbound-proxy.ts`'s own
78
+ * precedent, restated there for exactly this reason). `capabilities()`
79
+ * reports `backendLogAccess`, not `consoleObservation` — `consoleObservation`
80
+ * is `browser-console-collector`'s claim on a *browser page's* console,
81
+ * kept apart from a backend process's console the same way `LogCollector`
82
+ * already keeps them apart for its own text-based capture.
83
+ */
84
+ import { connectCdp } from "./cdp-client.js";
85
+ import { isProcessAlive } from "./attach-to-running-process.js";
86
+ import { COLLECTOR_VERSION } from "./collector-version.js";
87
+ /**
88
+ * Sends `SIGUSR1` to `pid`, then polls `http://<host>:<port>/json` until the
89
+ * target's inspector answers or `timeoutMs` elapses. Resolves with the
90
+ * `webSocketDebuggerUrl` of the first entry — an already-running Node
91
+ * process presents exactly one inspectable target for its main thread,
92
+ * unlike a browser's `/json`, which lists one entry per open tab.
93
+ *
94
+ * Rejects, naming the exact blocker, rather than hanging or guessing: a pid
95
+ * that was never alive, a POSIX-only signal on a non-POSIX platform, or a
96
+ * port that never answers (most often because the real inspector bound
97
+ * somewhere other than `port`, which this function has no way to learn —
98
+ * see the module doc).
99
+ */
100
+ export async function discoverNodeInspectorUrl(pid, options = {}) {
101
+ if (!isProcessAlive(pid)) {
102
+ throw new Error(`discoverNodeInspectorUrl: pid ${String(pid)} is not running — cannot attach to a target that does not exist.`);
103
+ }
104
+ if (process.platform === "win32") {
105
+ throw new Error("discoverNodeInspectorUrl: SIGUSR1 inspector activation is POSIX-only; not available on win32.");
106
+ }
107
+ const host = options.host ?? "127.0.0.1";
108
+ const port = options.port ?? 9229;
109
+ const timeoutMs = options.timeoutMs ?? 5000;
110
+ const discoveryUrl = `http://${host}:${String(port)}/json`;
111
+ process.kill(pid, "SIGUSR1");
112
+ const deadline = Date.now() + timeoutMs;
113
+ let lastError = null;
114
+ while (Date.now() < deadline) {
115
+ if (!isProcessAlive(pid)) {
116
+ throw new Error(`discoverNodeInspectorUrl: pid ${String(pid)} exited while waiting for its inspector to come up.`);
117
+ }
118
+ try {
119
+ const response = await fetch(discoveryUrl);
120
+ if (response.ok) {
121
+ const targets = (await response.json());
122
+ const target = targets.find((candidate) => candidate.webSocketDebuggerUrl !== undefined);
123
+ if (target?.webSocketDebuggerUrl !== undefined)
124
+ return target.webSocketDebuggerUrl;
125
+ }
126
+ }
127
+ catch (error) {
128
+ lastError = error;
129
+ }
130
+ await new Promise((resolve) => setTimeout(resolve, 50));
131
+ }
132
+ const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
133
+ throw new Error(`discoverNodeInspectorUrl: no inspector answered at ${discoveryUrl} within ${String(timeoutMs)}ms after SIGUSR1${detail}. ` +
134
+ "Most likely the target's inspector bound to a different port than the one checked here (already in use by another " +
135
+ "process, or the target was itself configured with --inspect-port/NODE_OPTIONS) — this module cannot discover a " +
136
+ "non-default port from outside; a caller who knows the real port must supply discoveryPort.");
137
+ }
138
+ /** A `file://` URL is decoded to a plain filesystem path — every other adapter in this repo reports a path, never a URL, and CDP is the only source in this file that starts with one. Anything else (`node:internal/timers`, a bare module specifier) is passed through verbatim, the same "kept, not filtered" treatment `adapter-rust`'s std/core frames get. */
139
+ function pathFromCdpUrl(url) {
140
+ if (url === undefined || url === "")
141
+ return null;
142
+ if (!url.startsWith("file://"))
143
+ return url;
144
+ try {
145
+ return new URL(url).pathname;
146
+ }
147
+ catch {
148
+ return url;
149
+ }
150
+ }
151
+ /** CDP reports 0-based line/column; every other location in this codebase is 1-based (source lines as an editor shows them). Converted here, once, rather than left for a reader to notice CDP is the one odd source. */
152
+ function locationFromCallFrame(frame) {
153
+ return {
154
+ file: pathFromCdpUrl(frame.url),
155
+ line: typeof frame.lineNumber === "number" ? frame.lineNumber + 1 : null,
156
+ column: typeof frame.columnNumber === "number" ? frame.columnNumber + 1 : null,
157
+ functionName: frame.functionName !== undefined && frame.functionName !== "" ? frame.functionName : null,
158
+ // V8's own reported state, captured live off the running process — the
159
+ // same class a Python traceback or Ruby backtrace gets, never a side
160
+ // artifact this collector resolved through.
161
+ reliability: "self-contained",
162
+ resolvedVia: null,
163
+ };
164
+ }
165
+ function formatConsoleArgs(args) {
166
+ return args
167
+ .map((arg) => {
168
+ if (arg.value !== undefined)
169
+ return typeof arg.value === "string" ? arg.value : JSON.stringify(arg.value);
170
+ if (arg.description !== undefined)
171
+ return arg.description;
172
+ return `<${arg.type ?? "unknown"}>`;
173
+ })
174
+ .join(" ");
175
+ }
176
+ function notThisCollectorsJob(what) {
177
+ return { availability: "unavailable", reason: `attachToRunningNodeProcess observes a Node process's Runtime domain (console, exceptions) only, not ${what}.` };
178
+ }
179
+ const CAPABILITIES = {
180
+ domObservation: notThisCollectorsJob("DOM inspection"),
181
+ // Reserved for a browser page's console (browser-console-collector.ts's
182
+ // own claim); this observes a backend process's, reported under
183
+ // backendLogAccess instead, matching LogCollector's own split.
184
+ consoleObservation: notThisCollectorsJob("a browser page's console"),
185
+ networkObservation: notThisCollectorsJob("network traffic"),
186
+ backendLogAccess: { availability: "available", reason: null },
187
+ distributedTrace: notThisCollectorsJob("trace-id/request-id correlation — CDP's Runtime domain carries no such fields to extract"),
188
+ // Self-contained, from V8's own reported call-frame state, the same
189
+ // standing Go's/Rust's/Ruby's own rows already have — never a side
190
+ // artifact this collector resolved through. A target running compiled or
191
+ // bundled output would still report positions in that output verbatim,
192
+ // the same honest limit every self-contained-reliability adapter in this
193
+ // repo carries, not something specific to Node inspector observation.
194
+ sourceMapping: { availability: "available", reason: null },
195
+ stackCapture: { availability: "available", reason: null },
196
+ processLifecycle: notThisCollectorsJob("process lifecycle — spawn/ready/exit are ProcessCollector's to observe"),
197
+ databaseObservation: notThisCollectorsJob("database queries"),
198
+ externalServiceObservation: notThisCollectorsJob("outbound external requests"),
199
+ };
200
+ /**
201
+ * Attach to an already-running Node process via `SIGUSR1` + CDP, and start
202
+ * capturing `CONSOLE_MESSAGE`/`EXCEPTION` evidence from that moment forward.
203
+ *
204
+ * **Retroactive, not historical.** Anything the target logged or threw
205
+ * *before* this attaches is gone — `SIGUSR1` turns the inspector on, it
206
+ * does not hand over a backlog. `attachToRunningProcess` (the file-based
207
+ * sibling) is the one that can recover pre-attach lines, and only because a
208
+ * file on disk already held them; nothing analogous exists for CDP, which
209
+ * is a live event stream with no buffer of its own.
210
+ *
211
+ * Discovery and connection happen inside `start()`, not here — matching
212
+ * every other `Collector` in this repo (`createBrowserConsoleCollector`,
213
+ * `createTestRunnerCollector`): a routine failure to attach resolves as
214
+ * `{ available: false, reason }`, never a thrown exception a caller has to
215
+ * remember to catch.
216
+ */
217
+ export function attachToRunningNodeProcess(options) {
218
+ let session = null;
219
+ return {
220
+ collectorId: `attach-node-inspector:${options.processId}`,
221
+ async start(context) {
222
+ let webSocketDebuggerUrl;
223
+ try {
224
+ webSocketDebuggerUrl = await discoverNodeInspectorUrl(options.pid, {
225
+ ...(options.discoveryHost !== undefined ? { host: options.discoveryHost } : {}),
226
+ ...(options.discoveryPort !== undefined ? { port: options.discoveryPort } : {}),
227
+ ...(options.discoveryTimeoutMs !== undefined ? { timeoutMs: options.discoveryTimeoutMs } : {}),
228
+ });
229
+ }
230
+ catch (error) {
231
+ return { available: false, reason: error instanceof Error ? error.message : String(error) };
232
+ }
233
+ try {
234
+ session = await connectCdp(webSocketDebuggerUrl);
235
+ }
236
+ catch (error) {
237
+ return {
238
+ available: false,
239
+ reason: `attachToRunningNodeProcess: found the inspector but could not open a CDP connection to it: ${error instanceof Error ? error.message : String(error)}`,
240
+ };
241
+ }
242
+ await session.send("Runtime.enable");
243
+ session.on("Runtime.consoleAPICalled", (params) => {
244
+ const p = params;
245
+ context.emit({
246
+ timestamp: new Date().toISOString(),
247
+ source: "backend-process",
248
+ service: options.service ?? null,
249
+ process: options.processId,
250
+ eventType: "CONSOLE_MESSAGE",
251
+ payload: { level: p.type ?? "log", text: formatConsoleArgs(p.args ?? []) },
252
+ traceId: null,
253
+ requestId: null,
254
+ correlationId: null,
255
+ graphNodeId: null,
256
+ sourceLocation: null,
257
+ stackTrace: null,
258
+ confidence: 1,
259
+ redactionStatus: "pending-redaction",
260
+ collectorVersion: COLLECTOR_VERSION,
261
+ });
262
+ });
263
+ session.on("Runtime.exceptionThrown", (params) => {
264
+ const p = params;
265
+ const details = p.exceptionDetails ?? {};
266
+ const description = details.exception?.description;
267
+ const message = description !== undefined
268
+ ? (description.split("\n")[0] ?? description)
269
+ : details.exception?.value !== undefined
270
+ ? String(details.exception.value)
271
+ : (details.text ?? "Uncaught exception");
272
+ const callFrames = details.stackTrace?.callFrames ?? [];
273
+ const frames = callFrames.map((frame) => ({ location: locationFromCallFrame(frame), raw: `${frame.functionName ?? "<anonymous>"} (${frame.url ?? ""}:${String(frame.lineNumber)}:${String(frame.columnNumber)})` }));
274
+ // No frames from CDP means the throw site itself (url/lineNumber
275
+ // directly on exceptionDetails) is the only location available —
276
+ // still real, so it becomes a synthetic one-frame trace rather than
277
+ // being discarded.
278
+ const stackTrace = frames.length > 0
279
+ ? { fidelity: "synchronous", frames, primaryFrameIndex: 0 }
280
+ : {
281
+ fidelity: "synchronous",
282
+ frames: [{ location: locationFromCallFrame({ url: details.url, lineNumber: details.lineNumber, columnNumber: details.columnNumber }), raw: description ?? message }],
283
+ primaryFrameIndex: 0,
284
+ };
285
+ context.emit({
286
+ timestamp: new Date().toISOString(),
287
+ source: "backend-process",
288
+ service: options.service ?? null,
289
+ process: options.processId,
290
+ eventType: "EXCEPTION",
291
+ payload: { message, stack: description ?? null },
292
+ traceId: null,
293
+ requestId: null,
294
+ correlationId: null,
295
+ graphNodeId: null,
296
+ sourceLocation: stackTrace.frames[stackTrace.primaryFrameIndex]?.location ?? null,
297
+ stackTrace,
298
+ confidence: 1,
299
+ redactionStatus: "pending-redaction",
300
+ collectorVersion: COLLECTOR_VERSION,
301
+ });
302
+ });
303
+ return { available: true };
304
+ },
305
+ stop() {
306
+ session?.close();
307
+ session = null;
308
+ return Promise.resolve();
309
+ },
310
+ capabilities() {
311
+ return CAPABILITIES;
312
+ },
313
+ };
314
+ }
315
+ //# sourceMappingURL=attach-to-running-node-process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-to-running-node-process.js","sourceRoot":"","sources":["../src/attach-to-running-node-process.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Attach observation to a process Descry did not spawn (RUNTIME-CHECKLIST.md
3
+ * §39 item 1's open half — "execute yes; attach: not implemented").
4
+ *
5
+ * **What this honestly covers, and what it does not.** Every collector in
6
+ * this repo reads a `ProcessOutputSource` (`process-output.ts`), and the
7
+ * only live implementation (`createPollingProcessOutputSource`) is already
8
+ * source-agnostic — it needs a growing `readOutput(): string` and a boolean
9
+ * `isFinished()`, nothing that presumes we spawned the process. `execute`
10
+ * satisfies those from `ManagedProcess`'s in-memory pipe buffer
11
+ * (`process-manager.ts`), which exists only because `child_process.spawn`
12
+ * wired the pipes *at spawn time* — Node (and POSIX generally) gives no way
13
+ * to retroactively attach to another process's stdout/stderr fds after the
14
+ * fact. So true retroactive attach to an *arbitrary* already-running
15
+ * process — one that logs to a TTY, or that never wrote its output
16
+ * anywhere Descry can now reach — is not implemented here and is not a
17
+ * bounded slice: it would need a per-language debugger/inspector protocol
18
+ * (e.g. Node's own `SIGUSR1` inspector activation, JVM's Attach API, a
19
+ * `/proc` ptrace shim on Linux only), each its own multi-week surface,
20
+ * scoped in `documents/decisions-inbox/RT-attach-to-running-process-scope.md`.
21
+ *
22
+ * What IS real and implemented: attaching to a process that is already
23
+ * running and was already directed to write its output to a file Descry
24
+ * can read — e.g. a dev server the user started with
25
+ * `npm run dev > server.log 2>&1 &`, or any long-lived service whose
26
+ * stdout/stderr is already redirected to disk. This is candidate (a), not
27
+ * (b): narrower than generic attach, but a real, testable capability —
28
+ * `attachToRunningProcess` verifies the target PID is alive, then reuses
29
+ * the exact same live-tail machinery `execute` uses, catching up on
30
+ * whatever the file already held *before* attach and then tailing new
31
+ * writes exactly like a spawned process's buffer.
32
+ *
33
+ * **Shares `execute`'s known identity gap (RT-024).** `isProcessAlive`
34
+ * confirms *a* process with this pid exists, not that it is the one the
35
+ * caller thinks it is — pids are reused by the OS. `execute`'s own
36
+ * readiness checks carry the identical caveat (see `readiness.ts`'s own
37
+ * doc comment) and it is disclosed there rather than solved; the same is
38
+ * true here, for the same reason: nothing in a bare pid or a bare file
39
+ * path proves provenance.
40
+ */
41
+ import type { ProcessOutputSource } from "@descryy/runtime-backend-observation";
42
+ export interface AttachToRunningProcessOptions {
43
+ readonly processId: string;
44
+ /** The pid of the already-running target. Verified alive before attaching, and re-checked on every poll to detect exit. */
45
+ readonly pid: number;
46
+ /** Path to a file the target process is already writing its stdout/stderr to. Read in full on every poll — same growing-buffer contract `ManagedProcess.readOutput()` has, not a delta. */
47
+ readonly logFilePath: string;
48
+ readonly pollIntervalMs?: number;
49
+ }
50
+ /**
51
+ * `process.kill(pid, 0)` sends no signal — it only asks the kernel whether
52
+ * delivery would be possible. `ESRCH` means no such pid; `EPERM` means a
53
+ * pid we don't own but that unambiguously exists, still alive for this
54
+ * purpose. Any other error is treated as "exists" rather than guessed away.
55
+ */
56
+ export declare function isProcessAlive(pid: number): boolean;
57
+ /**
58
+ * Attach to an already-running process via a log file it is already
59
+ * writing to. Throws immediately if the pid is not alive at call time —
60
+ * refusing rather than returning a source that would silently never emit
61
+ * anything, the same "loud, not quiet" contract `ProcessCollector` uses for
62
+ * its own ordering requirement.
63
+ *
64
+ * Lines already in `logFilePath` at attach time are yielded first (the
65
+ * "already running before we attached" evidence this is for), followed by
66
+ * whatever the target writes afterward — indistinguishable, once attached,
67
+ * from `execute`'s own live tail.
68
+ */
69
+ export declare function attachToRunningProcess(options: AttachToRunningProcessOptions): ProcessOutputSource;
70
+ //# sourceMappingURL=attach-to-running-process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-to-running-process.d.ts","sourceRoot":"","sources":["../src/attach-to-running-process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAIhF,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2HAA2H;IAC3H,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2LAA2L;IAC3L,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,6BAA6B,GAAG,mBAAmB,CAuBlG"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Attach observation to a process Descry did not spawn (RUNTIME-CHECKLIST.md
3
+ * §39 item 1's open half — "execute yes; attach: not implemented").
4
+ *
5
+ * **What this honestly covers, and what it does not.** Every collector in
6
+ * this repo reads a `ProcessOutputSource` (`process-output.ts`), and the
7
+ * only live implementation (`createPollingProcessOutputSource`) is already
8
+ * source-agnostic — it needs a growing `readOutput(): string` and a boolean
9
+ * `isFinished()`, nothing that presumes we spawned the process. `execute`
10
+ * satisfies those from `ManagedProcess`'s in-memory pipe buffer
11
+ * (`process-manager.ts`), which exists only because `child_process.spawn`
12
+ * wired the pipes *at spawn time* — Node (and POSIX generally) gives no way
13
+ * to retroactively attach to another process's stdout/stderr fds after the
14
+ * fact. So true retroactive attach to an *arbitrary* already-running
15
+ * process — one that logs to a TTY, or that never wrote its output
16
+ * anywhere Descry can now reach — is not implemented here and is not a
17
+ * bounded slice: it would need a per-language debugger/inspector protocol
18
+ * (e.g. Node's own `SIGUSR1` inspector activation, JVM's Attach API, a
19
+ * `/proc` ptrace shim on Linux only), each its own multi-week surface,
20
+ * scoped in `documents/decisions-inbox/RT-attach-to-running-process-scope.md`.
21
+ *
22
+ * What IS real and implemented: attaching to a process that is already
23
+ * running and was already directed to write its output to a file Descry
24
+ * can read — e.g. a dev server the user started with
25
+ * `npm run dev > server.log 2>&1 &`, or any long-lived service whose
26
+ * stdout/stderr is already redirected to disk. This is candidate (a), not
27
+ * (b): narrower than generic attach, but a real, testable capability —
28
+ * `attachToRunningProcess` verifies the target PID is alive, then reuses
29
+ * the exact same live-tail machinery `execute` uses, catching up on
30
+ * whatever the file already held *before* attach and then tailing new
31
+ * writes exactly like a spawned process's buffer.
32
+ *
33
+ * **Shares `execute`'s known identity gap (RT-024).** `isProcessAlive`
34
+ * confirms *a* process with this pid exists, not that it is the one the
35
+ * caller thinks it is — pids are reused by the OS. `execute`'s own
36
+ * readiness checks carry the identical caveat (see `readiness.ts`'s own
37
+ * doc comment) and it is disclosed there rather than solved; the same is
38
+ * true here, for the same reason: nothing in a bare pid or a bare file
39
+ * path proves provenance.
40
+ */
41
+ import { readFileSync } from "node:fs";
42
+ import { createPollingProcessOutputSource } from "./polling-output-source.js";
43
+ /**
44
+ * `process.kill(pid, 0)` sends no signal — it only asks the kernel whether
45
+ * delivery would be possible. `ESRCH` means no such pid; `EPERM` means a
46
+ * pid we don't own but that unambiguously exists, still alive for this
47
+ * purpose. Any other error is treated as "exists" rather than guessed away.
48
+ */
49
+ export function isProcessAlive(pid) {
50
+ try {
51
+ process.kill(pid, 0);
52
+ return true;
53
+ }
54
+ catch (error) {
55
+ return error.code === "EPERM";
56
+ }
57
+ }
58
+ /**
59
+ * Attach to an already-running process via a log file it is already
60
+ * writing to. Throws immediately if the pid is not alive at call time —
61
+ * refusing rather than returning a source that would silently never emit
62
+ * anything, the same "loud, not quiet" contract `ProcessCollector` uses for
63
+ * its own ordering requirement.
64
+ *
65
+ * Lines already in `logFilePath` at attach time are yielded first (the
66
+ * "already running before we attached" evidence this is for), followed by
67
+ * whatever the target writes afterward — indistinguishable, once attached,
68
+ * from `execute`'s own live tail.
69
+ */
70
+ export function attachToRunningProcess(options) {
71
+ if (!isProcessAlive(options.pid)) {
72
+ throw new Error(`attachToRunningProcess: pid ${options.pid} for "${options.processId}" is not running — attach requires ` +
73
+ `a target that already exists. Use ExecutionController.run() to spawn a new process instead.`);
74
+ }
75
+ return createPollingProcessOutputSource({
76
+ processId: options.processId,
77
+ readOutput: () => {
78
+ try {
79
+ return readFileSync(options.logFilePath, "utf8");
80
+ }
81
+ catch (error) {
82
+ const detail = error instanceof Error ? error.message : String(error);
83
+ throw new Error(`attachToRunningProcess: could not read log file "${options.logFilePath}" for pid ${options.pid}: ${detail}`);
84
+ }
85
+ },
86
+ isFinished: () => !isProcessAlive(options.pid),
87
+ ...(options.pollIntervalMs !== undefined ? { pollIntervalMs: options.pollIntervalMs } : {}),
88
+ });
89
+ }
90
+ //# sourceMappingURL=attach-to-running-process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-to-running-process.js","sourceRoot":"","sources":["../src/attach-to-running-process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIvC,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAW9E;;;;;GAKG;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;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAsC;IAC3E,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,+BAA+B,OAAO,CAAC,GAAG,SAAS,OAAO,CAAC,SAAS,qCAAqC;YACvG,6FAA6F,CAChG,CAAC;IACJ,CAAC;IAED,OAAO,gCAAgC,CAAC;QACtC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,UAAU,EAAE,GAAG,EAAE;YACf,IAAI,CAAC;gBACH,OAAO,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YACnD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACtE,MAAM,IAAI,KAAK,CACb,oDAAoD,OAAO,CAAC,WAAW,aAAa,OAAO,CAAC,GAAG,KAAK,MAAM,EAAE,CAC7G,CAAC;YACJ,CAAC;QACH,CAAC;QACD,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC;QAC9C,GAAG,CAAC,OAAO,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5F,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A minimal Chrome DevTools Protocol client over a raw `ws://` URL.
3
+ *
4
+ * **Why this exists rather than reusing something already in the repo.**
5
+ * `packages/browser` speaks CDP too, but only through Playwright's own
6
+ * `page.on("console"/"pageerror")` API (`browser-console-collector.ts`) —
7
+ * Playwright owns the actual WebSocket and the Target/Page domain framing
8
+ * underneath it, and never exposes a bare "connect to an arbitrary `ws://`
9
+ * endpoint and read raw CDP frames" surface. A Node inspector session has no
10
+ * `Target`/`Page` domains at all — it is a single top-level session speaking
11
+ * `Runtime`/`Debugger`/`Console` directly — so Playwright's browser
12
+ * abstraction does not fit it even if pressed into service. Checked before
13
+ * writing this: nothing else in the repo opens a WebSocket to a CDP
14
+ * endpoint directly. This is that client, kept protocol-family-generic (it
15
+ * knows nothing about "Node" or "browser") so a future CDP target of either
16
+ * kind can reuse it instead of a third one being written.
17
+ *
18
+ * CDP-over-WebSocket is JSON-RPC-shaped: a sent message carries `id` and
19
+ * gets exactly one `{id, result}` or `{id, error}` reply; an event has
20
+ * `method`/`params` and no `id`. This client does nothing more than that
21
+ * framing — domain-specific method names and event shapes belong to the
22
+ * caller, not here.
23
+ */
24
+ export interface CdpSession {
25
+ /** Sends a CDP command and resolves with its `result` once the matching `id` reply arrives. Rejects on a CDP-reported `error` or if the socket closes first. */
26
+ send<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
27
+ /** Subscribes to a CDP event by method name (e.g. `"Runtime.consoleAPICalled"`). Returns an unsubscribe function. */
28
+ on(method: string, handler: (params: Record<string, unknown>) => void): () => void;
29
+ /** Closes the underlying WebSocket. Idempotent. Rejects every still-pending `send()`. */
30
+ close(): void;
31
+ }
32
+ /**
33
+ * Resolves once the socket is open, or rejects if it errors/closes first —
34
+ * so a caller never has to race `open` against `error` itself.
35
+ */
36
+ export declare function connectCdp(webSocketUrl: string): Promise<CdpSession>;
37
+ //# sourceMappingURL=cdp-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdp-client.d.ts","sourceRoot":"","sources":["../src/cdp-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,UAAU;IACzB,gKAAgK;IAChK,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAChF,qHAAqH;IACrH,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnF,yFAAyF;IACzF,KAAK,IAAI,IAAI,CAAC;CACf;AAOD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmFpE"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * A minimal Chrome DevTools Protocol client over a raw `ws://` URL.
3
+ *
4
+ * **Why this exists rather than reusing something already in the repo.**
5
+ * `packages/browser` speaks CDP too, but only through Playwright's own
6
+ * `page.on("console"/"pageerror")` API (`browser-console-collector.ts`) —
7
+ * Playwright owns the actual WebSocket and the Target/Page domain framing
8
+ * underneath it, and never exposes a bare "connect to an arbitrary `ws://`
9
+ * endpoint and read raw CDP frames" surface. A Node inspector session has no
10
+ * `Target`/`Page` domains at all — it is a single top-level session speaking
11
+ * `Runtime`/`Debugger`/`Console` directly — so Playwright's browser
12
+ * abstraction does not fit it even if pressed into service. Checked before
13
+ * writing this: nothing else in the repo opens a WebSocket to a CDP
14
+ * endpoint directly. This is that client, kept protocol-family-generic (it
15
+ * knows nothing about "Node" or "browser") so a future CDP target of either
16
+ * kind can reuse it instead of a third one being written.
17
+ *
18
+ * CDP-over-WebSocket is JSON-RPC-shaped: a sent message carries `id` and
19
+ * gets exactly one `{id, result}` or `{id, error}` reply; an event has
20
+ * `method`/`params` and no `id`. This client does nothing more than that
21
+ * framing — domain-specific method names and event shapes belong to the
22
+ * caller, not here.
23
+ */
24
+ /**
25
+ * Resolves once the socket is open, or rejects if it errors/closes first —
26
+ * so a caller never has to race `open` against `error` itself.
27
+ */
28
+ export function connectCdp(webSocketUrl) {
29
+ return new Promise((resolveConnect, rejectConnect) => {
30
+ const socket = new WebSocket(webSocketUrl);
31
+ const pending = new Map();
32
+ const listeners = new Map();
33
+ let nextId = 1;
34
+ let settled = false;
35
+ function failAllPending(error) {
36
+ for (const { reject } of pending.values())
37
+ reject(error);
38
+ pending.clear();
39
+ }
40
+ socket.addEventListener("open", () => {
41
+ settled = true;
42
+ resolveConnect({
43
+ send(method, params = {}) {
44
+ const id = nextId++;
45
+ return new Promise((resolve, reject) => {
46
+ pending.set(id, { resolve: resolve, reject });
47
+ socket.send(JSON.stringify({ id, method, params }));
48
+ });
49
+ },
50
+ on(method, handler) {
51
+ let set = listeners.get(method);
52
+ if (set === undefined) {
53
+ set = new Set();
54
+ listeners.set(method, set);
55
+ }
56
+ set.add(handler);
57
+ return () => set.delete(handler);
58
+ },
59
+ close() {
60
+ socket.close();
61
+ },
62
+ });
63
+ });
64
+ socket.addEventListener("message", (event) => {
65
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
66
+ let msg;
67
+ try {
68
+ msg = JSON.parse(raw);
69
+ }
70
+ catch {
71
+ // A frame that is not valid JSON is not a valid CDP message from
72
+ // any target this protocol defines -- dropped rather than thrown,
73
+ // the same "unparseable stdout is ignorable noise" rule
74
+ // `test-runner`'s envelope gate applies to its own stream.
75
+ return;
76
+ }
77
+ if (msg.id !== undefined) {
78
+ const waiting = pending.get(msg.id);
79
+ if (waiting === undefined)
80
+ return;
81
+ pending.delete(msg.id);
82
+ if (msg.error !== undefined) {
83
+ waiting.reject(new Error(`CDP error for message ${String(msg.id)}: ${msg.error.message}`));
84
+ }
85
+ else {
86
+ waiting.resolve(msg.result);
87
+ }
88
+ return;
89
+ }
90
+ if (msg.method !== undefined) {
91
+ const set = listeners.get(msg.method);
92
+ if (set === undefined)
93
+ return;
94
+ for (const handler of set)
95
+ handler(msg.params ?? {});
96
+ }
97
+ });
98
+ socket.addEventListener("error", () => {
99
+ const error = new Error(`CDP WebSocket error connecting to ${webSocketUrl}`);
100
+ if (!settled) {
101
+ settled = true;
102
+ rejectConnect(error);
103
+ }
104
+ failAllPending(error);
105
+ });
106
+ socket.addEventListener("close", () => {
107
+ failAllPending(new Error(`CDP WebSocket to ${webSocketUrl} closed`));
108
+ });
109
+ });
110
+ }
111
+ //# sourceMappingURL=cdp-client.js.map