@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.
- package/dist/attach-to-running-jvm-process.d.ts +139 -0
- package/dist/attach-to-running-jvm-process.d.ts.map +1 -0
- package/dist/attach-to-running-jvm-process.js +338 -0
- package/dist/attach-to-running-jvm-process.js.map +1 -0
- package/dist/attach-to-running-node-process.d.ts +138 -0
- package/dist/attach-to-running-node-process.d.ts.map +1 -0
- package/dist/attach-to-running-node-process.js +315 -0
- package/dist/attach-to-running-node-process.js.map +1 -0
- package/dist/attach-to-running-process.d.ts +70 -0
- package/dist/attach-to-running-process.d.ts.map +1 -0
- package/dist/attach-to-running-process.js +90 -0
- package/dist/attach-to-running-process.js.map +1 -0
- package/dist/cdp-client.d.ts +37 -0
- package/dist/cdp-client.d.ts.map +1 -0
- package/dist/cdp-client.js +111 -0
- package/dist/cdp-client.js.map +1 -0
- package/dist/cgroup-partial-restriction.d.ts +127 -0
- package/dist/cgroup-partial-restriction.d.ts.map +1 -0
- package/dist/cgroup-partial-restriction.js +236 -0
- package/dist/cgroup-partial-restriction.js.map +1 -0
- package/dist/collector-version.d.ts +12 -0
- package/dist/collector-version.d.ts.map +1 -0
- package/dist/collector-version.js +14 -0
- package/dist/collector-version.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumented-execution.d.ts +111 -0
- package/dist/instrumented-execution.d.ts.map +1 -0
- package/dist/instrumented-execution.js +363 -0
- package/dist/instrumented-execution.js.map +1 -0
- package/dist/jvm-agent/build.d.ts +72 -0
- package/dist/jvm-agent/build.d.ts.map +1 -0
- package/dist/jvm-agent/build.js +156 -0
- package/dist/jvm-agent/build.js.map +1 -0
- package/dist/polling-output-source.d.ts +28 -0
- package/dist/polling-output-source.d.ts.map +1 -0
- package/dist/polling-output-source.js +73 -0
- package/dist/polling-output-source.js.map +1 -0
- package/dist/profile-backend-observation.d.ts +94 -0
- package/dist/profile-backend-observation.d.ts.map +1 -0
- package/dist/profile-backend-observation.js +136 -0
- package/dist/profile-backend-observation.js.map +1 -0
- package/dist/respawn-and-supervise.d.ts +119 -0
- package/dist/respawn-and-supervise.d.ts.map +1 -0
- package/dist/respawn-and-supervise.js +212 -0
- package/dist/respawn-and-supervise.js.map +1 -0
- 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
|