@tlockney/cdp-kernel 0.5.0 → 0.7.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 +27 -6
- package/dist/browser/channels.d.ts +27 -0
- package/dist/browser/channels.js +90 -0
- package/dist/browser/kernel.js +29674 -0
- package/dist/browser/kernel.js.map +7 -0
- package/dist/browser/mod.d.ts +20 -0
- package/dist/browser/mod.js +38 -0
- package/dist/browser/target.d.ts +71 -0
- package/dist/browser/target.js +206 -0
- package/dist/browser/worker.js +1000 -0
- package/dist/browser/worker.js.map +7 -0
- package/dist/cdp.d.ts +11 -0
- package/dist/cdp.js +6 -1
- package/dist/celld-target.js +9 -12
- package/dist/children.d.ts +25 -3
- package/dist/children.js +84 -2
- package/dist/debugger.d.ts +2 -0
- package/dist/debugger.js +12 -6
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/kernel-core.d.ts +69 -0
- package/dist/kernel-core.js +754 -0
- package/dist/kernel.d.ts +12 -1
- package/dist/kernel.js +132 -605
- package/dist/language-service-worker.d.ts +1 -0
- package/dist/language-service-worker.js +88 -0
- package/dist/language-service.d.ts +64 -0
- package/dist/language-service.js +128 -0
- package/dist/main.js +102 -8
- package/dist/message.d.ts +34 -0
- package/dist/message.js +14 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- package/dist/notebook-types.d.ts +62 -0
- package/dist/notebook-types.js +310 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/parent.d.ts +12 -0
- package/dist/parent.js +39 -0
- package/dist/port-holder.d.ts +13 -0
- package/dist/port-holder.js +105 -0
- package/dist/sourcemap.d.ts +7 -2
- package/dist/sourcemap.js +9 -3
- package/dist/strip-esbuild.d.ts +2 -0
- package/dist/strip-esbuild.js +44 -0
- package/dist/strip-sucrase.d.ts +2 -0
- package/dist/strip-sucrase.js +78 -0
- package/dist/target-api.d.ts +80 -0
- package/dist/target-api.js +22 -0
- package/dist/target.d.ts +26 -74
- package/dist/target.js +232 -58
- package/dist/traceback.d.ts +75 -0
- package/dist/traceback.js +200 -0
- package/dist/transform.d.ts +22 -1
- package/dist/transform.js +46 -43
- package/dist/wire.d.ts +4 -24
- package/dist/wire.js +3 -12
- package/dist/workerd-target.js +1 -1
- package/dist/workers-project.js +13 -2
- package/dist/zmq-channels.d.ts +14 -0
- package/dist/zmq-channels.js +59 -0
- package/dist/zmtp.d.ts +2 -0
- package/dist/zmtp.js +12 -0
- package/package.json +15 -5
- package/runtime/bootstrap.mjs +113 -33
- package/runtime/browser-worker.mjs +119 -0
- package/runtime/cdn.d.mts +2 -0
- package/runtime/cdn.mjs +17 -0
- package/runtime/display.mjs +202 -1
- package/runtime/format.d.mts +2 -0
- package/runtime/format.mjs +125 -0
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +6 -0
- package/runtime/node-hooks.mjs +11 -0
package/dist/target.js
CHANGED
|
@@ -7,11 +7,20 @@ import { spawn } from "node:child_process";
|
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { asEvaluateResult, CdpAbandoned, CdpSession } from "./cdp.js";
|
|
10
|
+
import { asEvaluateResult, CdpAbandoned, CdpError, CdpSession } from "./cdp.js";
|
|
11
11
|
import { SHIM, transformCell } from "./transform.js";
|
|
12
12
|
import { hostRuntime, isInsideNodeModules, runtimeDir } from "./package.js";
|
|
13
13
|
import { killOnExit } from "./children.js";
|
|
14
|
-
|
|
14
|
+
import { Interrupted, isTargetMessage, parseJson } from "./target-api.js";
|
|
15
|
+
export { Interrupted, isTargetMessage, parseJson };
|
|
16
|
+
/**
|
|
17
|
+
* The inspector keeps every object it returns a remote reference to alive
|
|
18
|
+
* until it is released, so a cell's result would never be collected (#286).
|
|
19
|
+
* Cells and lookups put theirs in these groups, which are released once the
|
|
20
|
+
* kernel is done with them.
|
|
21
|
+
*/
|
|
22
|
+
const CELL_GROUP = "cdpk-cell";
|
|
23
|
+
const LOOKUP_GROUP = "cdpk-lookup";
|
|
15
24
|
/** How long a target may take from spawn to a usable session. */
|
|
16
25
|
const START_TIMEOUT_MS = 60_000;
|
|
17
26
|
const MARKER_RE = /\x1b\[cdpk:(\d+)(m?)\]\x1b/;
|
|
@@ -27,7 +36,9 @@ const INSPECTOR_CHATTER = [
|
|
|
27
36
|
"Visit chrome://inspect",
|
|
28
37
|
"Waiting for the debugger to disconnect",
|
|
29
38
|
];
|
|
30
|
-
|
|
39
|
+
// Deno prints "Debugger session started." for each session the host opens
|
|
40
|
+
// (#281).
|
|
41
|
+
const DENO_CHATTER = [...INSPECTOR_CHATTER, "Debugger session started.", "Download http://", "Download https://"];
|
|
31
42
|
const ANSI_SGR = /\x1b\[[0-9;]*m/g;
|
|
32
43
|
/** A colour code cut off at the end of a chunk. */
|
|
33
44
|
const PARTIAL_SGR = /\x1b(?:\[[0-9;]*)?$/;
|
|
@@ -75,23 +86,11 @@ export function esbuildBinary(resolve = import.meta.resolve, platform = process.
|
|
|
75
86
|
return null;
|
|
76
87
|
}
|
|
77
88
|
}
|
|
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
|
-
}
|
|
87
|
-
export function isTargetMessage(value) {
|
|
88
|
-
if (typeof value !== "object" || value === null)
|
|
89
|
-
return false;
|
|
90
|
-
const kind = value.kind;
|
|
91
|
-
return (kind === "iopub" || kind === "input_request") &&
|
|
92
|
-
typeof value.barrier === "number";
|
|
93
|
-
}
|
|
94
89
|
const MARKER_PREFIX = "\x1b[cdpk:";
|
|
90
|
+
// A message record: the message's JSON, which never holds a raw ESC, between
|
|
91
|
+
// "\x1b[cdpk:<" and "\x1b]" (#271).
|
|
92
|
+
const RECORD_PREFIX = "\x1b[cdpk:<";
|
|
93
|
+
const RECORD_RE = /\x1b\[cdpk:<([^\x1b]*)\x1b\]/;
|
|
95
94
|
function isPartialMarker(tail) {
|
|
96
95
|
if (tail.length >= MAX_MARKER_LEN)
|
|
97
96
|
return false;
|
|
@@ -99,10 +98,13 @@ function isPartialMarker(tail) {
|
|
|
99
98
|
}
|
|
100
99
|
/**
|
|
101
100
|
* Splits one output pipe into text and barrier markers, and releases them
|
|
102
|
-
* in order. A gated marker (one that announces a
|
|
101
|
+
* in order. A gated marker (one that announces a message) pauses
|
|
103
102
|
* the pipe until `gate(n)` resolves, i.e. until that message has been
|
|
104
103
|
* delivered, so later output can't overtake it. A possible partial marker
|
|
105
104
|
* at the end of a chunk is held back until the next chunk decides it.
|
|
105
|
+
*
|
|
106
|
+
* With `onMessage`, the pipe also carries message records. A record stands
|
|
107
|
+
* where its gated marker would, and pauses the pipe the same way.
|
|
106
108
|
*/
|
|
107
109
|
export class PipeDemux {
|
|
108
110
|
#buffer = "";
|
|
@@ -111,28 +113,49 @@ export class PipeDemux {
|
|
|
111
113
|
#closed = false;
|
|
112
114
|
#reached = 0;
|
|
113
115
|
#waiters = [];
|
|
116
|
+
/** The gated marker the pipe is paused on, if any. */
|
|
117
|
+
#gatedOn = null;
|
|
114
118
|
onText;
|
|
115
119
|
gate;
|
|
116
|
-
|
|
120
|
+
onMessage;
|
|
121
|
+
constructor(onText, gate = () => Promise.resolve(), onMessage = null) {
|
|
117
122
|
this.onText = onText;
|
|
118
123
|
this.gate = gate;
|
|
124
|
+
this.onMessage = onMessage;
|
|
119
125
|
}
|
|
120
126
|
push(chunk) {
|
|
121
127
|
this.#buffer += chunk;
|
|
122
128
|
for (;;) {
|
|
123
129
|
const m = MARKER_RE.exec(this.#buffer);
|
|
124
|
-
|
|
130
|
+
const r = this.onMessage ? RECORD_RE.exec(this.#buffer) : null;
|
|
131
|
+
const first = m && r ? (m.index < r.index ? m : r) : m ?? r;
|
|
132
|
+
if (!first)
|
|
125
133
|
break;
|
|
126
|
-
if (
|
|
127
|
-
this.#items.push({ text: this.#buffer.slice(0,
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
if (first.index > 0)
|
|
135
|
+
this.#items.push({ text: this.#buffer.slice(0, first.index) });
|
|
136
|
+
if (first === m) {
|
|
137
|
+
this.#items.push({ barrier: Number(m[1]), gated: m[2] === "m" });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Anything else a cell prints in this shape is dropped (#122).
|
|
141
|
+
const msg = parseJson(first[1]);
|
|
142
|
+
if (isTargetMessage(msg))
|
|
143
|
+
this.#items.push({ message: msg });
|
|
144
|
+
}
|
|
145
|
+
this.#buffer = this.#buffer.slice(first.index + first[0].length);
|
|
130
146
|
}
|
|
131
147
|
// Hold back a tail that could still grow into a marker, including one
|
|
132
|
-
// cut inside the "\x1b[cdpk:" prefix itself
|
|
148
|
+
// cut inside the "\x1b[cdpk:" prefix itself, or into a record: one
|
|
149
|
+
// whose only ESC after its prefix is a final one.
|
|
133
150
|
const esc = this.#buffer.lastIndexOf("\x1b");
|
|
134
151
|
const tail = esc === -1 ? "" : this.#buffer.slice(esc);
|
|
135
|
-
|
|
152
|
+
let holdFrom = esc !== -1 && isPartialMarker(tail) ? esc : this.#buffer.length;
|
|
153
|
+
const record = this.onMessage ? this.#buffer.lastIndexOf(RECORD_PREFIX) : -1;
|
|
154
|
+
if (record !== -1) {
|
|
155
|
+
const next = this.#buffer.indexOf("\x1b", record + RECORD_PREFIX.length);
|
|
156
|
+
if (next === -1 || next === this.#buffer.length - 1)
|
|
157
|
+
holdFrom = Math.min(holdFrom, record);
|
|
158
|
+
}
|
|
136
159
|
if (holdFrom > 0) {
|
|
137
160
|
this.#items.push({ text: this.#buffer.slice(0, holdFrom) });
|
|
138
161
|
this.#buffer = this.#buffer.slice(holdFrom);
|
|
@@ -149,9 +172,22 @@ export class PipeDemux {
|
|
|
149
172
|
this.onText(item.text);
|
|
150
173
|
continue;
|
|
151
174
|
}
|
|
175
|
+
if ("message" in item) {
|
|
176
|
+
this.#reach(item.message.barrier);
|
|
177
|
+
this.onMessage?.(item.message);
|
|
178
|
+
if (!this.#closed) {
|
|
179
|
+
this.#gatedOn = item.message.barrier;
|
|
180
|
+
await this.gate(item.message.barrier);
|
|
181
|
+
this.#gatedOn = null;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
152
185
|
this.#reach(item.barrier);
|
|
153
|
-
if (item.gated && !this.#closed)
|
|
186
|
+
if (item.gated && !this.#closed) {
|
|
187
|
+
this.#gatedOn = item.barrier;
|
|
154
188
|
await this.gate(item.barrier);
|
|
189
|
+
this.#gatedOn = null;
|
|
190
|
+
}
|
|
155
191
|
}
|
|
156
192
|
}
|
|
157
193
|
finally {
|
|
@@ -172,6 +208,16 @@ export class PipeDemux {
|
|
|
172
208
|
return Promise.resolve();
|
|
173
209
|
return new Promise((resolve) => this.#waiters.push({ n, resolve }));
|
|
174
210
|
}
|
|
211
|
+
/** Where the pipe stands, for a state dump (#237). */
|
|
212
|
+
state() {
|
|
213
|
+
return {
|
|
214
|
+
reached: this.#reached,
|
|
215
|
+
waitingFor: this.#waiters.map((w) => w.n),
|
|
216
|
+
gatedOn: this.#gatedOn,
|
|
217
|
+
queued: this.#items.length,
|
|
218
|
+
held: this.#buffer.length,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
175
221
|
/** Release everything (the process died). */
|
|
176
222
|
close() {
|
|
177
223
|
this.#closed = true;
|
|
@@ -267,9 +313,6 @@ export class Gates {
|
|
|
267
313
|
this.#map.clear();
|
|
268
314
|
}
|
|
269
315
|
}
|
|
270
|
-
export class Interrupted extends Error {
|
|
271
|
-
name = "Interrupted";
|
|
272
|
-
}
|
|
273
316
|
/** A catch handler: a call interrupt() abandoned becomes Interrupted. */
|
|
274
317
|
function interrupted(what) {
|
|
275
318
|
return (err) => {
|
|
@@ -287,8 +330,13 @@ export class Target {
|
|
|
287
330
|
#gates;
|
|
288
331
|
#contextId;
|
|
289
332
|
#messages = Promise.resolve();
|
|
333
|
+
/** Messages received but not yet delivered. */
|
|
334
|
+
#undelivered = 0;
|
|
290
335
|
#interrupted = false;
|
|
291
336
|
#paused = false;
|
|
337
|
+
/** What the debugger asked to pause on (#307), and how many kernel calls hold it off. */
|
|
338
|
+
#pauseOnExceptions = "none";
|
|
339
|
+
#unpausable = 0;
|
|
292
340
|
/** Set once the main context is gone: the runtime is exiting (#130). */
|
|
293
341
|
#exiting = false;
|
|
294
342
|
#exited;
|
|
@@ -332,6 +380,9 @@ export class Target {
|
|
|
332
380
|
delete env.NO_COLOR;
|
|
333
381
|
const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe"], env });
|
|
334
382
|
killOnExit(child);
|
|
383
|
+
// Prompt replies and comm messages go to stdin; one written as the
|
|
384
|
+
// runtime exits fails with EPIPE, and the exit is handled on its own.
|
|
385
|
+
child.stdin.on("error", () => { });
|
|
335
386
|
// A start that fails or stalls must not leave the runtime running: its
|
|
336
387
|
// pipes and inspector sockets would keep this process alive (#52).
|
|
337
388
|
const sessions = [];
|
|
@@ -357,7 +408,12 @@ export class Target {
|
|
|
357
408
|
child.stdout.setEncoding("utf8");
|
|
358
409
|
child.stderr.setEncoding("utf8");
|
|
359
410
|
const gates = new Gates();
|
|
360
|
-
|
|
411
|
+
// Messages come only after attach(), so the target exists by then.
|
|
412
|
+
let target;
|
|
413
|
+
const stdout = new PipeDemux((text) => events.stream("stdout", text), (n) => gates.wait(n), (msg) => {
|
|
414
|
+
if (target)
|
|
415
|
+
target.#deliver(msg, events);
|
|
416
|
+
});
|
|
361
417
|
const stderr = new PipeDemux((text) => events.stream("stderr", text), (n) => gates.wait(n));
|
|
362
418
|
const filter = new StderrFilter(runtime, (text) => stderr.push(text));
|
|
363
419
|
const pushStderr = (chunk) => filter.push(chunk);
|
|
@@ -398,25 +454,22 @@ export class Target {
|
|
|
398
454
|
});
|
|
399
455
|
});
|
|
400
456
|
await shell.send("Runtime.enable");
|
|
401
|
-
await shell.send("Runtime.addBinding", { name: BINDING });
|
|
402
457
|
const contextId = await contextReady;
|
|
403
|
-
|
|
458
|
+
// The inspector socket leaves Nagle's algorithm on, so an event written
|
|
459
|
+
// just before an evaluate's reply holds that reply until the event is
|
|
460
|
+
// acknowledged, ~40 ms on Linux (#271). So the shell session, which runs
|
|
461
|
+
// the cells, takes no Runtime events (console calls among them), and
|
|
462
|
+
// messages come over stdout. The control session watches for exit.
|
|
463
|
+
await shell.send("Runtime.disable");
|
|
464
|
+
await control.send("Runtime.enable");
|
|
465
|
+
target = new Target(runtime, child, shell, control, stdout, stderr, gates, contextId, { runtime: "", engine: "" });
|
|
404
466
|
// process.exit() or Deno.exit() destroys the context, then the runtime
|
|
405
467
|
// waits for its inspector clients to disconnect before it exits. Let go,
|
|
406
468
|
// so it exits and the kernel starts a fresh one (#130).
|
|
407
|
-
|
|
408
|
-
if (params.executionContextId === contextId)
|
|
469
|
+
control.on("Runtime.executionContextDestroyed", (params) => {
|
|
470
|
+
if (params.executionContextId === contextId && target)
|
|
409
471
|
target.#onExit();
|
|
410
472
|
});
|
|
411
|
-
shell.on("Runtime.bindingCalled", (params) => {
|
|
412
|
-
if (params.name !== BINDING || typeof params.payload !== "string")
|
|
413
|
-
return;
|
|
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);
|
|
417
|
-
if (isTargetMessage(msg))
|
|
418
|
-
target.#deliver(msg, events);
|
|
419
|
-
});
|
|
420
473
|
// Wait for the bootstrap's top-level await to finish installing the
|
|
421
474
|
// shim. There is no cap here: launch's start timeout bounds the wait and
|
|
422
475
|
// kills the child, so a slow bootstrap never yields a target without
|
|
@@ -427,6 +480,7 @@ export class Target {
|
|
|
427
480
|
break;
|
|
428
481
|
await new Promise((r) => setTimeout(r, 25));
|
|
429
482
|
}
|
|
483
|
+
await target.#evaluateSync(`${SHIM}.attach()`);
|
|
430
484
|
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 })`);
|
|
431
485
|
Object.assign(target.versions, JSON.parse(String(versions.result.value)));
|
|
432
486
|
// The runtime's own enumerable globals, before any cell: the debugger's
|
|
@@ -443,21 +497,102 @@ export class Target {
|
|
|
443
497
|
/** The Debugger domain on this target's session, for src/debugger.ts. */
|
|
444
498
|
get debug() {
|
|
445
499
|
return {
|
|
446
|
-
send: (method, params = {}) =>
|
|
500
|
+
send: (method, params = {}) => {
|
|
501
|
+
if (method === "Debugger.setPauseOnExceptions") {
|
|
502
|
+
this.#pauseOnExceptions = String(params.state ?? "none");
|
|
503
|
+
// Applied when the kernel's own call in flight finishes.
|
|
504
|
+
if (this.#unpausable > 0)
|
|
505
|
+
return Promise.resolve({});
|
|
506
|
+
}
|
|
507
|
+
if (method === "Debugger.disable")
|
|
508
|
+
this.#pauseOnExceptions = "none";
|
|
509
|
+
return this.#shell.send(method, params);
|
|
510
|
+
},
|
|
447
511
|
on: (method, listener) => this.#shell.on(method, listener),
|
|
448
512
|
contextId: this.#contextId,
|
|
449
513
|
baselineGlobals: this.#baselineGlobals,
|
|
514
|
+
withoutPausing: (call) => this.#withoutPausing(call),
|
|
450
515
|
};
|
|
451
516
|
}
|
|
452
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Runs the kernel's own inspector call with pausing on exceptions off. A
|
|
519
|
+
* pause inside a call the inspector dispatches holds that call (Node), or
|
|
520
|
+
* resumes by itself and leaves Deno's event loop stopped for good, so no
|
|
521
|
+
* later cell runs (#307). A cell that throws meanwhile doesn't pause.
|
|
522
|
+
*/
|
|
523
|
+
async #withoutPausing(call) {
|
|
524
|
+
if (this.#pauseOnExceptions === "none" || this.#paused)
|
|
525
|
+
return call();
|
|
526
|
+
if (this.#unpausable++ === 0)
|
|
527
|
+
await this.#shell.send("Debugger.setPauseOnExceptions", { state: "none" });
|
|
528
|
+
try {
|
|
529
|
+
return await call();
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
if (--this.#unpausable === 0 && !this.#exiting) {
|
|
533
|
+
await this.#shell.send("Debugger.setPauseOnExceptions", { state: this.#pauseOnExceptions }).catch(() => { });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Messages wait until both pipes have caught up to their marker,
|
|
453
538
|
// and are delivered strictly in order.
|
|
454
539
|
#deliver(msg, events) {
|
|
540
|
+
this.#undelivered++;
|
|
455
541
|
this.#messages = this.#messages.then(async () => {
|
|
456
542
|
await Promise.all([this.#stdout.waitFor(msg.barrier), this.#stderr.waitFor(msg.barrier)]);
|
|
543
|
+
this.#undelivered--;
|
|
457
544
|
events.message(msg);
|
|
458
545
|
this.#gates.open(msg.barrier);
|
|
459
546
|
});
|
|
460
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* What the host is waiting on, whether the runtime still evaluates, runs
|
|
550
|
+
* immediates and runs timers, and where its JavaScript is if it is
|
|
551
|
+
* running any, for a kernel that has stopped answering (#237). The probes
|
|
552
|
+
* go over the control session, which no cell holds.
|
|
553
|
+
*/
|
|
554
|
+
async diagnose() {
|
|
555
|
+
const within = (promise, fallback) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(fallback), 2000).unref())]);
|
|
556
|
+
const probe = (expression, awaitPromise) => within(this.#control.send("Runtime.evaluate", { expression, contextId: this.#contextId, awaitPromise, returnByValue: true })
|
|
557
|
+
.then((res) => res.exceptionDetails ? `threw: ${JSON.stringify(res.exceptionDetails)}` : String(res.result?.value), (err) => `failed: ${err}`), "no answer in 2 s");
|
|
558
|
+
const [evaluates, immediates, timers] = await Promise.all([
|
|
559
|
+
probe("'yes'", false),
|
|
560
|
+
probe("new Promise((resolve) => setImmediate(() => resolve('yes')))", true),
|
|
561
|
+
probe("new Promise((resolve) => setTimeout(() => resolve('yes'), 0))", true),
|
|
562
|
+
]);
|
|
563
|
+
// A runtime whose event loop is stuck may be stuck in JavaScript: say
|
|
564
|
+
// where. (An idle one would take the pause in whatever runs next.)
|
|
565
|
+
let running = "event loop runs";
|
|
566
|
+
if (immediates !== "yes") {
|
|
567
|
+
try {
|
|
568
|
+
const paused = new Promise((resolve) => this.#control.on("Debugger.paused", resolve));
|
|
569
|
+
await this.#control.send("Debugger.enable");
|
|
570
|
+
await this.#control.send("Debugger.pause");
|
|
571
|
+
const event = await within(paused, null);
|
|
572
|
+
running = event === null ? "no" : event.callFrames
|
|
573
|
+
.slice(0, 10)
|
|
574
|
+
.map((f) => `${f.functionName || "(anonymous)"} ${f.url || "(no url)"}:${f.location.lineNumber + 1}`);
|
|
575
|
+
await this.#control.send("Debugger.resume").catch(() => { });
|
|
576
|
+
await this.#control.send("Debugger.disable");
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
running = `failed: ${err}`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
pid: this.#child.pid ?? null,
|
|
584
|
+
calls: { shell: this.#shell.pendingCalls(), control: this.#control.pendingCalls() },
|
|
585
|
+
stdout: this.#stdout.state(),
|
|
586
|
+
stderr: this.#stderr.state(),
|
|
587
|
+
undelivered: this.#undelivered,
|
|
588
|
+
paused: this.#paused,
|
|
589
|
+
exiting: this.#exiting,
|
|
590
|
+
evaluates,
|
|
591
|
+
immediates,
|
|
592
|
+
timers,
|
|
593
|
+
running,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
461
596
|
/** Resolves once all output and messages sent so far have been delivered. */
|
|
462
597
|
async flush() {
|
|
463
598
|
const n = await this.#evaluateSync(`${SHIM}.barrier()`).catch(interrupted("Execution"));
|
|
@@ -479,7 +614,13 @@ export class Target {
|
|
|
479
614
|
}
|
|
480
615
|
#evaluateRaw(expression, session = this.#shell) {
|
|
481
616
|
return session
|
|
482
|
-
.send("Runtime.evaluate", {
|
|
617
|
+
.send("Runtime.evaluate", {
|
|
618
|
+
expression,
|
|
619
|
+
contextId: this.#contextId,
|
|
620
|
+
replMode: true,
|
|
621
|
+
awaitPromise: true,
|
|
622
|
+
objectGroup: CELL_GROUP,
|
|
623
|
+
})
|
|
483
624
|
.then(asEvaluateResult);
|
|
484
625
|
}
|
|
485
626
|
/** Runs transformed cell code. Rejects with Interrupted after interrupt(). */
|
|
@@ -491,6 +632,10 @@ export class Target {
|
|
|
491
632
|
catch (err) {
|
|
492
633
|
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
493
634
|
throw new Interrupted("Execution interrupted");
|
|
635
|
+
// The control session's executionContextDestroyed event can arrive
|
|
636
|
+
// after the shell session's error reply (#271 moved it there).
|
|
637
|
+
if (err instanceof CdpError && err.message.includes("Execution context was destroyed"))
|
|
638
|
+
this.#onExit();
|
|
494
639
|
if (this.#exiting) {
|
|
495
640
|
// The child's exit, which drops this target in the kernel, comes
|
|
496
641
|
// first, so the next cell starts a fresh one.
|
|
@@ -500,6 +645,11 @@ export class Target {
|
|
|
500
645
|
throw err;
|
|
501
646
|
}
|
|
502
647
|
}
|
|
648
|
+
async releaseResults() {
|
|
649
|
+
if (this.#exiting)
|
|
650
|
+
return;
|
|
651
|
+
await this.#shell.send("Runtime.releaseObjectGroup", { objectGroup: CELL_GROUP }).catch(() => { });
|
|
652
|
+
}
|
|
503
653
|
#onExit() {
|
|
504
654
|
if (this.#exiting)
|
|
505
655
|
return;
|
|
@@ -532,12 +682,12 @@ export class Target {
|
|
|
532
682
|
* even a plain return value in a promise it holds only weakly (#55).
|
|
533
683
|
*/
|
|
534
684
|
callFunctionOn(functionDeclaration, args) {
|
|
535
|
-
return this.#shell.send("Runtime.callFunctionOn", {
|
|
685
|
+
return this.#withoutPausing(() => this.#shell.send("Runtime.callFunctionOn", {
|
|
536
686
|
functionDeclaration,
|
|
537
687
|
arguments: args,
|
|
538
688
|
executionContextId: this.#contextId,
|
|
539
689
|
returnByValue: true,
|
|
540
|
-
});
|
|
690
|
+
}));
|
|
541
691
|
}
|
|
542
692
|
/**
|
|
543
693
|
* For functions that return a promise. The target holds it until after
|
|
@@ -573,14 +723,19 @@ export class Target {
|
|
|
573
723
|
*/
|
|
574
724
|
async dispatchComm(msgType, content, metadata, buffers) {
|
|
575
725
|
this.#interrupted = false;
|
|
726
|
+
// Over stdin, not as call arguments: V8 caches an argument's source,
|
|
727
|
+
// so the data would stay in the target's memory (#287).
|
|
728
|
+
const record = Buffer.from(JSON.stringify({
|
|
729
|
+
msgType,
|
|
730
|
+
content,
|
|
731
|
+
metadata,
|
|
732
|
+
buffers: buffers.map((b) => Buffer.from(b).toString("base64")),
|
|
733
|
+
}));
|
|
734
|
+
this.#child.stdin.write(`comm ${record.length}\n`);
|
|
735
|
+
this.#child.stdin.write(record);
|
|
576
736
|
let res;
|
|
577
737
|
try {
|
|
578
|
-
res = await this.#callAsyncFunctionOn(`function (
|
|
579
|
-
{ value: msgType },
|
|
580
|
-
{ value: content },
|
|
581
|
-
{ value: metadata },
|
|
582
|
-
{ value: buffers.map((b) => Buffer.from(b).toString("base64")) },
|
|
583
|
-
]);
|
|
738
|
+
res = await this.#callAsyncFunctionOn(`function () { return globalThis.${SHIM}.commDispatchFromStdin(); }`, []);
|
|
584
739
|
}
|
|
585
740
|
catch (err) {
|
|
586
741
|
if (err instanceof CdpAbandoned || this.#interrupted)
|
|
@@ -611,11 +766,22 @@ export class Target {
|
|
|
611
766
|
*/
|
|
612
767
|
#evaluatePure(expression) {
|
|
613
768
|
return this.#shell
|
|
614
|
-
.send("Runtime.evaluate", { expression, contextId: this.#contextId, throwOnSideEffect: true })
|
|
769
|
+
.send("Runtime.evaluate", { expression, contextId: this.#contextId, throwOnSideEffect: true, objectGroup: LOOKUP_GROUP })
|
|
615
770
|
.then(asEvaluateResult)
|
|
616
771
|
.catch(() => null);
|
|
617
772
|
}
|
|
773
|
+
#releaseLookup() {
|
|
774
|
+
return this.#shell.send("Runtime.releaseObjectGroup", { objectGroup: LOOKUP_GROUP }).then(() => { }, () => { });
|
|
775
|
+
}
|
|
618
776
|
async propertyNames(expression) {
|
|
777
|
+
try {
|
|
778
|
+
return await this.#propertyNames(expression);
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
await this.#releaseLookup();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async #propertyNames(expression) {
|
|
619
785
|
const evaluated = await this.#evaluatePure(`(${expression})`);
|
|
620
786
|
const objectId = evaluated?.result.objectId;
|
|
621
787
|
if (!objectId || evaluated?.exceptionDetails)
|
|
@@ -625,6 +791,14 @@ export class Target {
|
|
|
625
791
|
return (list ?? []).map((p) => p.name);
|
|
626
792
|
}
|
|
627
793
|
async inspect(path, detail) {
|
|
794
|
+
try {
|
|
795
|
+
return await this.#inspect(path, detail);
|
|
796
|
+
}
|
|
797
|
+
finally {
|
|
798
|
+
await this.#releaseLookup();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
async #inspect(path, detail) {
|
|
628
802
|
const found = await this.#evaluatePure(path);
|
|
629
803
|
if (!found || found.exceptionDetails)
|
|
630
804
|
return null;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { LineMap } from "./sourcemap.ts";
|
|
2
|
+
export declare const TRACEBACK_MIME = "application/vnd.nteract.traceback+json";
|
|
3
|
+
/** The //# sourceURL a cell's script runs under while not debugging. */
|
|
4
|
+
export declare function cellName(code: string): string;
|
|
5
|
+
/** A cell that ran: its code, and where the notebook knows it from. */
|
|
6
|
+
export interface CellRecord {
|
|
7
|
+
code: string;
|
|
8
|
+
executionCount: number | null;
|
|
9
|
+
cellId: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface SourceLine extends Record<string, string | number | boolean> {
|
|
12
|
+
lineno: number;
|
|
13
|
+
source: string;
|
|
14
|
+
}
|
|
15
|
+
export interface TracebackFrame extends Record<string, unknown> {
|
|
16
|
+
filename: string;
|
|
17
|
+
lineno: number;
|
|
18
|
+
colno?: number;
|
|
19
|
+
name: string;
|
|
20
|
+
library: boolean;
|
|
21
|
+
lines: SourceLine[];
|
|
22
|
+
execution_count?: number;
|
|
23
|
+
cell_id?: string;
|
|
24
|
+
}
|
|
25
|
+
/** A frame of CDP's exceptionDetails.stackTrace (0-based positions). */
|
|
26
|
+
export interface CallFrame {
|
|
27
|
+
url: string;
|
|
28
|
+
lineNumber: number;
|
|
29
|
+
columnNumber: number;
|
|
30
|
+
}
|
|
31
|
+
/** A frame as parsed from one line of a stack. */
|
|
32
|
+
export interface StackFrame {
|
|
33
|
+
name: string;
|
|
34
|
+
file: string;
|
|
35
|
+
line: number | null;
|
|
36
|
+
column: number | null;
|
|
37
|
+
}
|
|
38
|
+
/** One line of a stack as a frame, or null for a line that isn't one (the message). */
|
|
39
|
+
export declare function parseFrame(text: string): StackFrame | null;
|
|
40
|
+
/**
|
|
41
|
+
* Frames from packages, runtimes and the kernel itself, which a frontend
|
|
42
|
+
* can dim or fold: a hint, not a boundary.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isLibraryFile(file: string, kernelFiles?: readonly string[]): boolean;
|
|
45
|
+
export interface TracebackSources {
|
|
46
|
+
/** The cell a script name belongs to, if it is one. */
|
|
47
|
+
cell(file: string): (CellRecord & {
|
|
48
|
+
lines: LineMap | null;
|
|
49
|
+
}) | undefined;
|
|
50
|
+
/** Another file's source, when it can be read. */
|
|
51
|
+
readFile?(file: string): string | undefined;
|
|
52
|
+
/** Path or URL prefixes of the kernel's own runtime files. */
|
|
53
|
+
kernelFiles?: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
export interface Traceback {
|
|
56
|
+
/** The classic traceback lines, with cell frames pointing at the cell. */
|
|
57
|
+
lines: string[];
|
|
58
|
+
/** The structured payload. */
|
|
59
|
+
payload: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `stack` is the error's stack as the target describes it (src/target.ts's
|
|
63
|
+
* describeError), message lines included. `callFrames`, from CDP, name the
|
|
64
|
+
* scripts of frames the stack shows as "<anonymous>": Deno formats stacks
|
|
65
|
+
* itself and doesn't use a script's //# sourceURL.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildTraceback(error: {
|
|
68
|
+
ename: string;
|
|
69
|
+
evalue: string;
|
|
70
|
+
stack: string;
|
|
71
|
+
callFrames?: readonly CallFrame[];
|
|
72
|
+
}, sources: TracebackSources, execution: {
|
|
73
|
+
executionCount: number | null;
|
|
74
|
+
cellId: string | null;
|
|
75
|
+
}): Traceback;
|