@tlockney/cdp-kernel 0.3.0 → 0.5.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/README.md +60 -404
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +4 -2
- package/dist/bun-target.js +35 -8
- package/dist/cdp.d.ts +6 -1
- package/dist/cdp.js +25 -10
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +46 -8
- package/dist/children.d.ts +7 -0
- package/dist/children.js +32 -0
- package/dist/debugger.d.ts +42 -0
- package/dist/debugger.js +545 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/history.d.ts +18 -0
- package/dist/history.js +45 -0
- package/dist/kernel.js +211 -32
- package/dist/main.js +3 -1
- package/dist/mod.d.ts +5 -3
- package/dist/mod.js +5 -3
- package/dist/murmur2.d.ts +2 -0
- package/dist/murmur2.js +41 -0
- package/dist/package.d.ts +16 -2
- package/dist/package.js +26 -3
- package/dist/sourcemap.d.ts +22 -0
- package/dist/sourcemap.js +86 -0
- package/dist/target.d.ts +39 -3
- package/dist/target.js +281 -30
- package/dist/text.d.ts +11 -1
- package/dist/text.js +72 -2
- package/dist/transform.d.ts +19 -0
- package/dist/transform.js +74 -13
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +9 -1
- package/dist/workerd-target.js +108 -9
- package/dist/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +7 -1
- package/dist/zmtp.js +30 -1
- package/package.json +10 -4
- package/runtime/bootstrap.mjs +138 -12
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +2 -2
- package/runtime/comm.mjs +11 -1
- package/runtime/deno-compat.d.mts +2 -0
- package/runtime/deno-compat.mjs +17 -2
- package/runtime/display.d.mts +2 -0
- package/runtime/display.mjs +17 -2
- package/runtime/evaluator.d.mts +4 -0
- package/runtime/evaluator.mjs +8 -0
- package/runtime/introspect.d.mts +13 -0
- package/runtime/introspect.mjs +120 -0
- package/runtime/node-hooks.d.mts +61 -1
- package/runtime/node-hooks.mjs +354 -21
- package/runtime/workerd-worker.mjs +82 -6
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Line mapping between a cell as written and the script that runs, for the
|
|
2
|
+
// debugger (#18): a breakpoint set on a cell's line must land on the line
|
|
3
|
+
// esbuild's output put it on, and a paused frame must report the line the
|
|
4
|
+
// user wrote. Only lines: Jupyter's breakpoints are per line.
|
|
5
|
+
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
6
|
+
const DIGIT = new Map([...BASE64].map((c, i) => [c, i]));
|
|
7
|
+
/** One segment's VLQ fields: generated column, source, line, column, name. */
|
|
8
|
+
export function decodeVlq(segment) {
|
|
9
|
+
const values = [];
|
|
10
|
+
let value = 0;
|
|
11
|
+
let shift = 0;
|
|
12
|
+
for (const char of segment) {
|
|
13
|
+
const digit = DIGIT.get(char);
|
|
14
|
+
if (digit === undefined)
|
|
15
|
+
throw new Error(`bad base64 VLQ digit ${JSON.stringify(char)}`);
|
|
16
|
+
value += (digit & 31) << shift;
|
|
17
|
+
if (digit & 32) {
|
|
18
|
+
shift += 5;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
values.push(value & 1 ? -(value >>> 1) : value >>> 1);
|
|
22
|
+
value = 0;
|
|
23
|
+
shift = 0;
|
|
24
|
+
}
|
|
25
|
+
return values;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* For each generated line (0-based), the first original line (0-based) a
|
|
29
|
+
* segment on it maps to, or -1. Source-map fields are relative to the
|
|
30
|
+
* previous segment's; only the original line is tracked.
|
|
31
|
+
*/
|
|
32
|
+
export function generatedToOriginalLines(mappings) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
let original = 0;
|
|
35
|
+
for (const line of mappings.split(";")) {
|
|
36
|
+
let first = -1;
|
|
37
|
+
for (const segment of line.split(",")) {
|
|
38
|
+
if (!segment)
|
|
39
|
+
continue;
|
|
40
|
+
const fields = decodeVlq(segment);
|
|
41
|
+
if (fields.length >= 4) {
|
|
42
|
+
original += fields[2];
|
|
43
|
+
if (first === -1)
|
|
44
|
+
first = original;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
lines.push(first);
|
|
48
|
+
}
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
/** Both directions between the lines of a cell and of its script (0-based). */
|
|
52
|
+
export class LineMap {
|
|
53
|
+
#toOriginal;
|
|
54
|
+
#toGenerated = new Map();
|
|
55
|
+
/** `offset`: lines the script adds before esbuild's output (the prelude). */
|
|
56
|
+
constructor(mappings, offset = 0) {
|
|
57
|
+
const fromEsbuild = generatedToOriginalLines(mappings);
|
|
58
|
+
this.#toOriginal = [...Array(offset).fill(-1), ...fromEsbuild];
|
|
59
|
+
this.#toOriginal.forEach((original, generated) => {
|
|
60
|
+
if (original !== -1 && !this.#toGenerated.has(original))
|
|
61
|
+
this.#toGenerated.set(original, generated);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** The cell line a script line came from: its own mapping, or the nearest one above. */
|
|
65
|
+
toOriginal(generated) {
|
|
66
|
+
for (let line = Math.min(generated, this.#toOriginal.length - 1); line >= 0; line--) {
|
|
67
|
+
if (this.#toOriginal[line] !== -1)
|
|
68
|
+
return this.#toOriginal[line];
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The script line for a cell line, or for the next cell line that has
|
|
74
|
+
* code (a breakpoint on a blank or comment line moves down, as debuggers
|
|
75
|
+
* do), or null past the end.
|
|
76
|
+
*/
|
|
77
|
+
toGenerated(original) {
|
|
78
|
+
const last = Math.max(-1, ...this.#toGenerated.keys());
|
|
79
|
+
for (let line = original; line <= last; line++) {
|
|
80
|
+
const generated = this.#toGenerated.get(line);
|
|
81
|
+
if (generated !== undefined)
|
|
82
|
+
return generated;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/target.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
|
|
2
2
|
import { type CellTransformOptions, transformCell } from "./transform.ts";
|
|
3
|
+
import type { DebugChannel } from "./debugger.ts";
|
|
3
4
|
/** Targets driven over CDP. */
|
|
4
5
|
export type CdpRuntime = "node" | "deno";
|
|
5
|
-
/** Bun and
|
|
6
|
+
/** Bun, workerd and celld have their own backends: src/bun-target.ts, src/workerd-target.ts, src/celld-target.ts. */
|
|
6
7
|
export type RuntimeName = CdpRuntime | "bun" | "workerd" | "celld";
|
|
7
8
|
/** What the kernel needs from a target runtime. */
|
|
8
9
|
export interface KernelTarget {
|
|
@@ -37,7 +38,12 @@ export interface KernelTarget {
|
|
|
37
38
|
}>;
|
|
38
39
|
propertyNames(expression: string): Promise<string[]>;
|
|
39
40
|
lexicalNames(): Promise<string[]>;
|
|
40
|
-
|
|
41
|
+
/** inspect_request (#10): text describing a dotted path's value, without running anything. */
|
|
42
|
+
inspect(path: string, detail: number): Promise<string | null>;
|
|
43
|
+
/** The Debugger domain, on targets driven over CDP (#18). */
|
|
44
|
+
readonly debug?: DebugChannel;
|
|
45
|
+
/** Answers the target's pending prompt; `interrupted` makes it throw instead (#121). */
|
|
46
|
+
replyInput(value: string | null, interrupted?: boolean): void;
|
|
41
47
|
kill(): void;
|
|
42
48
|
}
|
|
43
49
|
export interface LaunchOptions {
|
|
@@ -51,6 +57,13 @@ export declare function launchCommand(runtime: CdpRuntime, bootstrap: string, {
|
|
|
51
57
|
cmd: string;
|
|
52
58
|
args: string[];
|
|
53
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* The esbuild binary this host's esbuild runs, for a Node target to compile
|
|
62
|
+
* imported TypeScript Node can't strip (#73); null when it can't be found.
|
|
63
|
+
* Resolved from esbuild's own location, so it's found in a node_modules
|
|
64
|
+
* install and in Deno's npm cache alike.
|
|
65
|
+
*/
|
|
66
|
+
export declare function esbuildBinary(resolve?: (specifier: string) => string, platform?: string, arch?: string): string | null;
|
|
54
67
|
/** Messages the bootstrap sends through the CDP binding. */
|
|
55
68
|
export type TargetMessage = {
|
|
56
69
|
kind: "iopub";
|
|
@@ -65,6 +78,8 @@ export type TargetMessage = {
|
|
|
65
78
|
password: boolean;
|
|
66
79
|
barrier: number;
|
|
67
80
|
};
|
|
81
|
+
/** The parsed value, or undefined for text that isn't JSON. */
|
|
82
|
+
export declare function parseJson(text: string): unknown;
|
|
68
83
|
export declare function isTargetMessage(value: unknown): value is TargetMessage;
|
|
69
84
|
/**
|
|
70
85
|
* Splits one output pipe into text and barrier markers, and releases them
|
|
@@ -83,6 +98,19 @@ export declare class PipeDemux {
|
|
|
83
98
|
/** Release everything (the process died). */
|
|
84
99
|
close(): void;
|
|
85
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Removes the runtime's own stderr lines (see INSPECTOR_CHATTER) and passes
|
|
103
|
+
* everything else on. A chatter line can arrive split across chunks, so a
|
|
104
|
+
* trailing partial line that could still turn out to be one is held back
|
|
105
|
+
* until its newline arrives; any other partial line passes straight through.
|
|
106
|
+
*/
|
|
107
|
+
export declare class StderrFilter {
|
|
108
|
+
#private;
|
|
109
|
+
constructor(runtime: CdpRuntime, emit: (text: string) => void);
|
|
110
|
+
push(chunk: string): void;
|
|
111
|
+
/** Releases a held partial line (the process died). */
|
|
112
|
+
flush(): void;
|
|
113
|
+
}
|
|
86
114
|
/** Promises keyed by barrier number, resolvable before or after first use. */
|
|
87
115
|
export declare class Gates {
|
|
88
116
|
#private;
|
|
@@ -113,7 +141,10 @@ export declare class Target implements KernelTarget {
|
|
|
113
141
|
executable?: string;
|
|
114
142
|
denoCompat?: boolean;
|
|
115
143
|
denoConfig?: string;
|
|
144
|
+
startTimeoutMs?: number;
|
|
116
145
|
}): Promise<Target>;
|
|
146
|
+
/** The Debugger domain on this target's session, for src/debugger.ts. */
|
|
147
|
+
get debug(): DebugChannel;
|
|
117
148
|
/** Resolves once all output and messages sent so far have been delivered. */
|
|
118
149
|
flush(): Promise<void>;
|
|
119
150
|
/** Runs transformed cell code. Rejects with Interrupted after interrupt(). */
|
|
@@ -126,6 +157,10 @@ export declare class Target implements KernelTarget {
|
|
|
126
157
|
* evaluate is abandoned here and the session stays usable.
|
|
127
158
|
*/
|
|
128
159
|
interrupt(): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* For synchronous functions. With awaitPromise the inspector would wrap
|
|
162
|
+
* even a plain return value in a promise it holds only weakly (#55).
|
|
163
|
+
*/
|
|
129
164
|
callFunctionOn(functionDeclaration: string, args: CdpParams[]): Promise<CdpParams>;
|
|
130
165
|
/** The broadcastResult step of Deno's execute flow (jupyter/mod.rs). */
|
|
131
166
|
broadcastResult(executionCount: number, result: RemoteObject): Promise<void>;
|
|
@@ -149,7 +184,8 @@ export declare class Target implements KernelTarget {
|
|
|
149
184
|
stack: string;
|
|
150
185
|
}>;
|
|
151
186
|
propertyNames(expression: string): Promise<string[]>;
|
|
187
|
+
inspect(path: string, detail: number): Promise<string | null>;
|
|
152
188
|
lexicalNames(): Promise<string[]>;
|
|
153
|
-
replyInput(value: string | null): void;
|
|
189
|
+
replyInput(value: string | null, interrupted?: boolean): void;
|
|
154
190
|
kill(): void;
|
|
155
191
|
}
|
package/dist/target.js
CHANGED
|
@@ -4,17 +4,33 @@
|
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
7
8
|
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
import { asEvaluateResult, CdpAbandoned, CdpSession } from "./cdp.js";
|
|
9
11
|
import { SHIM, transformCell } from "./transform.js";
|
|
10
12
|
import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
|
|
13
|
+
import { killOnExit } from "./children.js";
|
|
11
14
|
const BINDING = "__cdpk_send";
|
|
15
|
+
/** How long a target may take from spawn to a usable session. */
|
|
16
|
+
const START_TIMEOUT_MS = 60_000;
|
|
12
17
|
const MARKER_RE = /\x1b\[cdpk:(\d+)(m?)\]\x1b/;
|
|
13
18
|
const MAX_MARKER_LEN = 24;
|
|
14
19
|
// Lines the runtimes print to stderr on their own: inspector notices, and
|
|
15
20
|
// Deno's npm/jsr "Download https://..." progress (which `deno jupyter`
|
|
16
|
-
// sends to the server log rather than the notebook).
|
|
17
|
-
|
|
21
|
+
// sends to the server log rather than the notebook). Each entry is the
|
|
22
|
+
// line's start once colour codes are removed.
|
|
23
|
+
const INSPECTOR_CHATTER = [
|
|
24
|
+
"Debugger listening on ",
|
|
25
|
+
"Debugger attached.",
|
|
26
|
+
"For help, see: https://nodejs.org/",
|
|
27
|
+
"Visit chrome://inspect",
|
|
28
|
+
"Waiting for the debugger to disconnect",
|
|
29
|
+
];
|
|
30
|
+
const DENO_CHATTER = [...INSPECTOR_CHATTER, "Download http://", "Download https://"];
|
|
31
|
+
const ANSI_SGR = /\x1b\[[0-9;]*m/g;
|
|
32
|
+
/** A colour code cut off at the end of a chunk. */
|
|
33
|
+
const PARTIAL_SGR = /\x1b(?:\[[0-9;]*)?$/;
|
|
18
34
|
/** `bootstrap` is the local path of runtime/bootstrap.mjs (see runtimeDir). */
|
|
19
35
|
export function launchCommand(runtime, bootstrap, { executable, denoConfig } = {}) {
|
|
20
36
|
// The CDP WebSocket has no authentication, so its URL must stay private:
|
|
@@ -41,6 +57,33 @@ export function launchCommand(runtime, bootstrap, { executable, denoConfig } = {
|
|
|
41
57
|
const node = hostRuntime() === "node" ? process.execPath : "node";
|
|
42
58
|
return { cmd: executable ?? node, args: [...inspect, bootstrap] };
|
|
43
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The esbuild binary this host's esbuild runs, for a Node target to compile
|
|
62
|
+
* imported TypeScript Node can't strip (#73); null when it can't be found.
|
|
63
|
+
* Resolved from esbuild's own location, so it's found in a node_modules
|
|
64
|
+
* install and in Deno's npm cache alike.
|
|
65
|
+
*/
|
|
66
|
+
export function esbuildBinary(resolve = import.meta.resolve, platform = process.platform, arch = process.arch) {
|
|
67
|
+
if (process.env.ESBUILD_BINARY_PATH)
|
|
68
|
+
return process.env.ESBUILD_BINARY_PATH;
|
|
69
|
+
const subpath = platform === "win32" ? "esbuild.exe" : "bin/esbuild";
|
|
70
|
+
try {
|
|
71
|
+
const main = fileURLToPath(resolve("esbuild"));
|
|
72
|
+
return createRequire(main).resolve(`@esbuild/${platform}-${arch}/${subpath}`);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The parsed value, or undefined for text that isn't JSON. */
|
|
79
|
+
export function parseJson(text) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
44
87
|
export function isTargetMessage(value) {
|
|
45
88
|
if (typeof value !== "object" || value === null)
|
|
46
89
|
return false;
|
|
@@ -139,6 +182,66 @@ export class PipeDemux {
|
|
|
139
182
|
void this.#pump();
|
|
140
183
|
}
|
|
141
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Removes the runtime's own stderr lines (see INSPECTOR_CHATTER) and passes
|
|
187
|
+
* everything else on. A chatter line can arrive split across chunks, so a
|
|
188
|
+
* trailing partial line that could still turn out to be one is held back
|
|
189
|
+
* until its newline arrives; any other partial line passes straight through.
|
|
190
|
+
*/
|
|
191
|
+
export class StderrFilter {
|
|
192
|
+
#starts;
|
|
193
|
+
#emit;
|
|
194
|
+
#held = "";
|
|
195
|
+
#atLineStart = true;
|
|
196
|
+
constructor(runtime, emit) {
|
|
197
|
+
this.#starts = runtime === "deno" ? DENO_CHATTER : INSPECTOR_CHATTER;
|
|
198
|
+
this.#emit = emit;
|
|
199
|
+
}
|
|
200
|
+
push(chunk) {
|
|
201
|
+
let text = this.#held + chunk;
|
|
202
|
+
this.#held = "";
|
|
203
|
+
let out = "";
|
|
204
|
+
if (!this.#atLineStart) {
|
|
205
|
+
// The rest of a line that already went out unfiltered.
|
|
206
|
+
const nl = text.indexOf("\n");
|
|
207
|
+
if (nl === -1) {
|
|
208
|
+
this.#emit(text);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
out = text.slice(0, nl + 1);
|
|
212
|
+
text = text.slice(nl + 1);
|
|
213
|
+
}
|
|
214
|
+
const lines = text.split("\n");
|
|
215
|
+
const last = lines.pop();
|
|
216
|
+
for (const line of lines)
|
|
217
|
+
if (!this.#isChatter(line, false))
|
|
218
|
+
out += line + "\n";
|
|
219
|
+
if (last && this.#isChatter(last, true))
|
|
220
|
+
this.#held = last;
|
|
221
|
+
else
|
|
222
|
+
out += last;
|
|
223
|
+
this.#atLineStart = last === "" || this.#held !== "";
|
|
224
|
+
if (out)
|
|
225
|
+
this.#emit(out);
|
|
226
|
+
}
|
|
227
|
+
/** Releases a held partial line (the process died). */
|
|
228
|
+
flush() {
|
|
229
|
+
if (this.#held)
|
|
230
|
+
this.#emit(this.#held);
|
|
231
|
+
this.#held = "";
|
|
232
|
+
this.#atLineStart = true;
|
|
233
|
+
}
|
|
234
|
+
/** Whether `line` is chatter or, when `partial`, could still become chatter. */
|
|
235
|
+
#isChatter(line, partial) {
|
|
236
|
+
let plain = line.replace(ANSI_SGR, "");
|
|
237
|
+
if (partial)
|
|
238
|
+
plain = plain.replace(PARTIAL_SGR, "");
|
|
239
|
+
// Any other escape may be a PipeDemux marker, which must never be dropped or held.
|
|
240
|
+
if (plain.includes("\x1b"))
|
|
241
|
+
return false;
|
|
242
|
+
return this.#starts.some((start) => plain.startsWith(start) || (partial && start.startsWith(plain)));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
142
245
|
/** Promises keyed by barrier number, resolvable before or after first use. */
|
|
143
246
|
export class Gates {
|
|
144
247
|
#map = new Map();
|
|
@@ -167,6 +270,12 @@ export class Gates {
|
|
|
167
270
|
export class Interrupted extends Error {
|
|
168
271
|
name = "Interrupted";
|
|
169
272
|
}
|
|
273
|
+
/** A catch handler: a call interrupt() abandoned becomes Interrupted. */
|
|
274
|
+
function interrupted(what) {
|
|
275
|
+
return (err) => {
|
|
276
|
+
throw err instanceof CdpAbandoned ? new Interrupted(`${what} interrupted`) : err;
|
|
277
|
+
};
|
|
278
|
+
}
|
|
170
279
|
export class Target {
|
|
171
280
|
runtime;
|
|
172
281
|
transform = transformCell;
|
|
@@ -179,10 +288,21 @@ export class Target {
|
|
|
179
288
|
#contextId;
|
|
180
289
|
#messages = Promise.resolve();
|
|
181
290
|
#interrupted = false;
|
|
291
|
+
#paused = false;
|
|
292
|
+
/** Set once the main context is gone: the runtime is exiting (#130). */
|
|
293
|
+
#exiting = false;
|
|
294
|
+
#exited;
|
|
295
|
+
#baselineGlobals = new Set();
|
|
182
296
|
versions;
|
|
183
297
|
constructor(runtime, child, shell, control, stdout, stderr, gates, contextId, versions) {
|
|
184
298
|
this.runtime = runtime;
|
|
185
299
|
this.#child = child;
|
|
300
|
+
this.#exited = new Promise((resolve) => {
|
|
301
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
302
|
+
resolve();
|
|
303
|
+
else
|
|
304
|
+
child.once("exit", () => resolve());
|
|
305
|
+
});
|
|
186
306
|
this.#shell = shell;
|
|
187
307
|
this.#control = control;
|
|
188
308
|
this.#stdout = stdout;
|
|
@@ -206,18 +326,41 @@ export class Target {
|
|
|
206
326
|
env.CDPK_DENO_COMPAT = "1";
|
|
207
327
|
if (runtime === "deno" && options.denoConfig !== undefined)
|
|
208
328
|
env.CDPK_DENO_PROJECT = "1";
|
|
329
|
+
const esbuild = runtime === "node" ? esbuildBinary() : null;
|
|
330
|
+
if (esbuild !== null)
|
|
331
|
+
env.CDPK_ESBUILD_BINARY = esbuild;
|
|
209
332
|
delete env.NO_COLOR;
|
|
210
333
|
const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
|
|
334
|
+
killOnExit(child);
|
|
335
|
+
// A start that fails or stalls must not leave the runtime running: its
|
|
336
|
+
// pipes and inspector sockets would keep this process alive (#52).
|
|
337
|
+
const sessions = [];
|
|
338
|
+
const timeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
|
|
339
|
+
let timer;
|
|
340
|
+
const stalled = new Promise((_, reject) => {
|
|
341
|
+
timer = setTimeout(() => reject(new Error(`${cmd} did not start within ${timeoutMs / 1000}s`)), timeoutMs);
|
|
342
|
+
});
|
|
343
|
+
try {
|
|
344
|
+
return await Promise.race([Target.#start(runtime, events, cmd, child, sessions), stalled]);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
for (const session of sessions)
|
|
348
|
+
session.close();
|
|
349
|
+
child.kill();
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
static async #start(runtime, events, cmd, child, sessions) {
|
|
211
357
|
child.stdout.setEncoding("utf8");
|
|
212
358
|
child.stderr.setEncoding("utf8");
|
|
213
359
|
const gates = new Gates();
|
|
214
360
|
const stdout = new PipeDemux((text) => events.stream("stdout", text), (n) => gates.wait(n));
|
|
215
361
|
const stderr = new PipeDemux((text) => events.stream("stderr", text), (n) => gates.wait(n));
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
if (text)
|
|
219
|
-
stderr.push(text);
|
|
220
|
-
};
|
|
362
|
+
const filter = new StderrFilter(runtime, (text) => stderr.push(text));
|
|
363
|
+
const pushStderr = (chunk) => filter.push(chunk);
|
|
221
364
|
// Until the inspector URL appears, stderr carries the runtime's banner.
|
|
222
365
|
const wsUrl = await new Promise((resolve, reject) => {
|
|
223
366
|
let banner = "";
|
|
@@ -238,12 +381,15 @@ export class Target {
|
|
|
238
381
|
child.stdout.on("data", (c) => stdout.push(c));
|
|
239
382
|
child.once("exit", (code) => {
|
|
240
383
|
gates.openAll();
|
|
384
|
+
filter.flush();
|
|
241
385
|
stdout.close();
|
|
242
386
|
stderr.close();
|
|
243
387
|
events.exit(code);
|
|
244
388
|
});
|
|
245
389
|
const shell = await CdpSession.connect(wsUrl);
|
|
390
|
+
sessions.push(shell);
|
|
246
391
|
const control = await CdpSession.connect(wsUrl);
|
|
392
|
+
sessions.push(control);
|
|
247
393
|
const contextReady = new Promise((resolve) => {
|
|
248
394
|
shell.on("Runtime.executionContextCreated", (params) => {
|
|
249
395
|
const context = params.context;
|
|
@@ -255,24 +401,54 @@ export class Target {
|
|
|
255
401
|
await shell.send("Runtime.addBinding", { name: BINDING });
|
|
256
402
|
const contextId = await contextReady;
|
|
257
403
|
const target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
|
|
404
|
+
// process.exit() or Deno.exit() destroys the context, then the runtime
|
|
405
|
+
// waits for its inspector clients to disconnect before it exits. Let go,
|
|
406
|
+
// so it exits and the kernel starts a fresh one (#130).
|
|
407
|
+
shell.on("Runtime.executionContextDestroyed", (params) => {
|
|
408
|
+
if (params.executionContextId === contextId)
|
|
409
|
+
target.#onExit();
|
|
410
|
+
});
|
|
258
411
|
shell.on("Runtime.bindingCalled", (params) => {
|
|
259
412
|
if (params.name !== BINDING || typeof params.payload !== "string")
|
|
260
413
|
return;
|
|
261
|
-
|
|
414
|
+
// __cdpk_send is an ordinary global, so a cell can call it with
|
|
415
|
+
// anything; that mustn't take the host down (#122).
|
|
416
|
+
const msg = parseJson(params.payload);
|
|
262
417
|
if (isTargetMessage(msg))
|
|
263
418
|
target.#deliver(msg, events);
|
|
264
419
|
});
|
|
265
|
-
// Wait for the bootstrap's top-level await to finish installing the
|
|
266
|
-
|
|
267
|
-
|
|
420
|
+
// Wait for the bootstrap's top-level await to finish installing the
|
|
421
|
+
// shim. There is no cap here: launch's start timeout bounds the wait and
|
|
422
|
+
// kills the child, so a slow bootstrap never yields a target without
|
|
423
|
+
// the shim (#134).
|
|
424
|
+
for (;;) {
|
|
425
|
+
const probe = await target.#evaluateSync(`typeof globalThis.${SHIM}?.tick === "function"`);
|
|
268
426
|
if (probe.result.value === true)
|
|
269
427
|
break;
|
|
270
428
|
await new Promise((r) => setTimeout(r, 25));
|
|
271
429
|
}
|
|
272
|
-
const versions = await target.#
|
|
430
|
+
const versions = await target.#evaluateSync(`JSON.stringify(typeof Deno !== "undefined" ? { runtime: Deno.version.deno, engine: "V8 " + Deno.version.v8 } : { runtime: process.versions.node, engine: "V8 " + process.versions.v8 })`);
|
|
273
431
|
Object.assign(target.versions, JSON.parse(String(versions.result.value)));
|
|
432
|
+
// The runtime's own enumerable globals, before any cell: the debugger's
|
|
433
|
+
// variable views show only what cells added (#18).
|
|
434
|
+
const globals = asEvaluateResult(await shell.send("Runtime.evaluate", { expression: "Object.keys(globalThis)", contextId, returnByValue: true }));
|
|
435
|
+
const names = globals.result.value;
|
|
436
|
+
if (Array.isArray(names))
|
|
437
|
+
for (const name of names)
|
|
438
|
+
target.#baselineGlobals.add(String(name));
|
|
439
|
+
shell.on("Debugger.paused", () => (target.#paused = true));
|
|
440
|
+
shell.on("Debugger.resumed", () => (target.#paused = false));
|
|
274
441
|
return target;
|
|
275
442
|
}
|
|
443
|
+
/** The Debugger domain on this target's session, for src/debugger.ts. */
|
|
444
|
+
get debug() {
|
|
445
|
+
return {
|
|
446
|
+
send: (method, params = {}) => this.#shell.send(method, params),
|
|
447
|
+
on: (method, listener) => this.#shell.on(method, listener),
|
|
448
|
+
contextId: this.#contextId,
|
|
449
|
+
baselineGlobals: this.#baselineGlobals,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
276
452
|
// Binding messages wait until both pipes have caught up to their marker,
|
|
277
453
|
// and are delivered strictly in order.
|
|
278
454
|
#deliver(msg, events) {
|
|
@@ -284,11 +460,23 @@ export class Target {
|
|
|
284
460
|
}
|
|
285
461
|
/** Resolves once all output and messages sent so far have been delivered. */
|
|
286
462
|
async flush() {
|
|
287
|
-
const n = await this.#
|
|
463
|
+
const n = await this.#evaluateSync(`${SHIM}.barrier()`).catch(interrupted("Execution"));
|
|
288
464
|
const barrier = Number(n.result.value);
|
|
289
465
|
await Promise.all([this.#stdout.waitFor(barrier), this.#stderr.waitFor(barrier)]);
|
|
290
466
|
await this.#messages;
|
|
291
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* For expressions that return a value without awaiting. With replMode or
|
|
470
|
+
* awaitPromise, the inspector holds the result promise only weakly until
|
|
471
|
+
* its then-callback runs, even when the expression returns a plain value:
|
|
472
|
+
* a GC in that window fails the call with "Promise was collected" (#50).
|
|
473
|
+
* A plain evaluate returns the value directly.
|
|
474
|
+
*/
|
|
475
|
+
#evaluateSync(expression) {
|
|
476
|
+
return this.#shell
|
|
477
|
+
.send("Runtime.evaluate", { expression, contextId: this.#contextId, returnByValue: true })
|
|
478
|
+
.then(asEvaluateResult);
|
|
479
|
+
}
|
|
292
480
|
#evaluateRaw(expression, session = this.#shell) {
|
|
293
481
|
return session
|
|
294
482
|
.send("Runtime.evaluate", { expression, contextId: this.#contextId, replMode: true, awaitPromise: true })
|
|
@@ -303,9 +491,27 @@ export class Target {
|
|
|
303
491
|
catch (err) {
|
|
304
492
|
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
305
493
|
throw new Interrupted("Execution interrupted");
|
|
494
|
+
if (this.#exiting) {
|
|
495
|
+
// The child's exit, which drops this target in the kernel, comes
|
|
496
|
+
// first, so the next cell starts a fresh one.
|
|
497
|
+
await this.#exited;
|
|
498
|
+
throw new Error(`${this.runtime} exited`);
|
|
499
|
+
}
|
|
306
500
|
throw err;
|
|
307
501
|
}
|
|
308
502
|
}
|
|
503
|
+
#onExit() {
|
|
504
|
+
if (this.#exiting)
|
|
505
|
+
return;
|
|
506
|
+
this.#exiting = true;
|
|
507
|
+
this.#shell.close();
|
|
508
|
+
this.#control.close();
|
|
509
|
+
// In case it still doesn't exit once both sessions are gone.
|
|
510
|
+
setTimeout(() => {
|
|
511
|
+
if (this.#child.exitCode === null && this.#child.signalCode === null)
|
|
512
|
+
this.#child.kill("SIGKILL");
|
|
513
|
+
}, 2000).unref();
|
|
514
|
+
}
|
|
309
515
|
/**
|
|
310
516
|
* Deno calls IsolateHandle::terminate_execution from another thread. The
|
|
311
517
|
* CDP equivalent is Runtime.terminateExecution, sent on a second session
|
|
@@ -315,14 +521,33 @@ export class Target {
|
|
|
315
521
|
*/
|
|
316
522
|
async interrupt() {
|
|
317
523
|
this.#interrupted = true;
|
|
524
|
+
// A paused isolate runs nothing to terminate: let it go first (#18).
|
|
525
|
+
if (this.#paused)
|
|
526
|
+
await this.#shell.send("Debugger.resume").catch(() => { });
|
|
318
527
|
await this.#control.send("Runtime.terminateExecution");
|
|
319
528
|
this.#shell.abandonPending();
|
|
320
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* For synchronous functions. With awaitPromise the inspector would wrap
|
|
532
|
+
* even a plain return value in a promise it holds only weakly (#55).
|
|
533
|
+
*/
|
|
321
534
|
callFunctionOn(functionDeclaration, args) {
|
|
322
535
|
return this.#shell.send("Runtime.callFunctionOn", {
|
|
323
536
|
functionDeclaration,
|
|
324
537
|
arguments: args,
|
|
325
538
|
executionContextId: this.#contextId,
|
|
539
|
+
returnByValue: true,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* For functions that return a promise. The target holds it until after
|
|
544
|
+
* the inspector's then-callback has run, so a GC can't collect it first.
|
|
545
|
+
*/
|
|
546
|
+
#callAsyncFunctionOn(functionDeclaration, args) {
|
|
547
|
+
return this.#shell.send("Runtime.callFunctionOn", {
|
|
548
|
+
functionDeclaration: `function (...args) { return globalThis.${SHIM}.hold((${functionDeclaration}).apply(this, args)); }`,
|
|
549
|
+
arguments: args,
|
|
550
|
+
executionContextId: this.#contextId,
|
|
326
551
|
awaitPromise: true,
|
|
327
552
|
returnByValue: true,
|
|
328
553
|
});
|
|
@@ -334,10 +559,12 @@ export class Target {
|
|
|
334
559
|
: result.unserializableValue !== undefined
|
|
335
560
|
? { unserializableValue: result.unserializableValue }
|
|
336
561
|
: { value: result.value };
|
|
337
|
-
|
|
562
|
+
// An async $display method can still be running when interrupt()
|
|
563
|
+
// abandons the call (#135).
|
|
564
|
+
await this.#callAsyncFunctionOn(`function (n, r) { return globalThis.${SHIM}.broadcastResult(n, r); }`, [
|
|
338
565
|
{ value: executionCount },
|
|
339
566
|
arg,
|
|
340
|
-
]);
|
|
567
|
+
]).catch(interrupted("Execution"));
|
|
341
568
|
}
|
|
342
569
|
/**
|
|
343
570
|
* Hands a frontend comm message to the target's comms. Resolves to
|
|
@@ -348,18 +575,12 @@ export class Target {
|
|
|
348
575
|
this.#interrupted = false;
|
|
349
576
|
let res;
|
|
350
577
|
try {
|
|
351
|
-
res = await this.#
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{ value: buffers.map((b) => Buffer.from(b).toString("base64")) },
|
|
358
|
-
],
|
|
359
|
-
executionContextId: this.#contextId,
|
|
360
|
-
awaitPromise: true,
|
|
361
|
-
returnByValue: true,
|
|
362
|
-
});
|
|
578
|
+
res = await this.#callAsyncFunctionOn(`function (t, c, m, b) { return globalThis.${SHIM}.commDispatch(t, c, m, b); }`, [
|
|
579
|
+
{ value: msgType },
|
|
580
|
+
{ value: content },
|
|
581
|
+
{ value: metadata },
|
|
582
|
+
{ value: buffers.map((b) => Buffer.from(b).toString("base64")) },
|
|
583
|
+
]);
|
|
363
584
|
}
|
|
364
585
|
catch (err) {
|
|
365
586
|
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
@@ -381,8 +602,21 @@ export class Target {
|
|
|
381
602
|
const value = res.result?.value;
|
|
382
603
|
return JSON.parse(typeof value === "string" ? value : '{"name":"Error","message":"","stack":""}');
|
|
383
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Evaluates without side effects: V8 throws instead of running anything
|
|
607
|
+
* that would change state (a call, a getter that writes), so completing
|
|
608
|
+
* or inspecting `obj.` never runs user code (#10). Not replMode, which
|
|
609
|
+
* returns a promise the inspector holds only weakly (#55); a plain
|
|
610
|
+
* evaluate sees the cells' let and const too.
|
|
611
|
+
*/
|
|
612
|
+
#evaluatePure(expression) {
|
|
613
|
+
return this.#shell
|
|
614
|
+
.send("Runtime.evaluate", { expression, contextId: this.#contextId, throwOnSideEffect: true })
|
|
615
|
+
.then(asEvaluateResult)
|
|
616
|
+
.catch(() => null);
|
|
617
|
+
}
|
|
384
618
|
async propertyNames(expression) {
|
|
385
|
-
const evaluated = await this.#
|
|
619
|
+
const evaluated = await this.#evaluatePure(`(${expression})`);
|
|
386
620
|
const objectId = evaluated?.result.objectId;
|
|
387
621
|
if (!objectId || evaluated?.exceptionDetails)
|
|
388
622
|
return [];
|
|
@@ -390,12 +624,29 @@ export class Target {
|
|
|
390
624
|
const list = props.result;
|
|
391
625
|
return (list ?? []).map((p) => p.name);
|
|
392
626
|
}
|
|
627
|
+
async inspect(path, detail) {
|
|
628
|
+
const found = await this.#evaluatePure(path);
|
|
629
|
+
if (!found || found.exceptionDetails)
|
|
630
|
+
return null;
|
|
631
|
+
const value = found.result;
|
|
632
|
+
const arg = value.objectId
|
|
633
|
+
? { objectId: value.objectId }
|
|
634
|
+
: value.unserializableValue !== undefined
|
|
635
|
+
? { unserializableValue: value.unserializableValue }
|
|
636
|
+
: { value: value.value };
|
|
637
|
+
const res = await this.callFunctionOn(`function (v, d) { return globalThis.${SHIM}.describeForInspect(v, d); }`, [
|
|
638
|
+
arg,
|
|
639
|
+
{ value: detail },
|
|
640
|
+
]);
|
|
641
|
+
const text = res.result?.value;
|
|
642
|
+
return typeof text === "string" ? text : null;
|
|
643
|
+
}
|
|
393
644
|
async lexicalNames() {
|
|
394
645
|
const res = await this.#shell.send("Runtime.globalLexicalScopeNames", { executionContextId: this.#contextId });
|
|
395
646
|
return res.names ?? [];
|
|
396
647
|
}
|
|
397
|
-
replyInput(value) {
|
|
398
|
-
this.#child.stdin.write(JSON.stringify({ value }) + "\n");
|
|
648
|
+
replyInput(value, interrupted = false) {
|
|
649
|
+
this.#child.stdin.write(JSON.stringify({ value, interrupted }) + "\n");
|
|
399
650
|
}
|
|
400
651
|
kill() {
|
|
401
652
|
this.#shell.close();
|
package/dist/text.d.ts
CHANGED
|
@@ -6,9 +6,19 @@ export type IsCompleteReply = {
|
|
|
6
6
|
} | {
|
|
7
7
|
status: "invalid";
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Bracket/quote/comment scanner deciding whether a cell is finished. Regex
|
|
11
|
+
* literals are recognised heuristically (see startsRegex), and a template's
|
|
12
|
+
* `${...}` is skipped like the rest of the template.
|
|
13
|
+
*/
|
|
10
14
|
export declare function checkIsComplete(code: string): IsCompleteReply;
|
|
11
15
|
/** The dotted expression ending at the cursor, e.g. "Math.fl". */
|
|
12
16
|
export declare function exprAtCursor(code: string, cursor: number): string;
|
|
17
|
+
/**
|
|
18
|
+
* The dotted name an inspect_request means (#10): the one the cursor is in
|
|
19
|
+
* or just after, or, right after "name(" while typing a call, that callee.
|
|
20
|
+
* Null when it isn't a plain path; nothing else is evaluated.
|
|
21
|
+
*/
|
|
22
|
+
export declare function inspectTarget(code: string, cursor: number): string | null;
|
|
13
23
|
export declare function codePointToUtf16Index(str: string, cpOffset: number): number;
|
|
14
24
|
export declare function utf16ToCodePointIndex(str: string, utf16Offset: number): number;
|