@tlockney/cdp-kernel 0.7.1 → 0.8.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/browser/kernel.js +216 -43
- package/dist/browser/kernel.js.map +2 -2
- package/dist/browser/target.d.ts +5 -3
- package/dist/browser/target.js +42 -9
- package/dist/browser/worker.js +47 -4
- package/dist/browser/worker.js.map +2 -2
- package/dist/bun-target.d.ts +5 -1
- package/dist/bun-target.js +32 -7
- package/dist/crash-reports.d.ts +47 -0
- package/dist/crash-reports.js +155 -0
- package/dist/kernel-core.d.ts +7 -0
- package/dist/kernel-core.js +212 -35
- package/dist/kernel.js +10 -0
- package/dist/main.js +31 -14
- package/dist/message.d.ts +1 -1
- package/dist/message.js +1 -1
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/target-api.d.ts +11 -0
- package/dist/target.d.ts +9 -2
- package/dist/target.js +66 -12
- package/dist/transform.js +3 -1
- package/dist/usage.d.ts +25 -0
- package/dist/usage.js +197 -0
- package/dist/workerd-target.d.ts +23 -2
- package/dist/workerd-target.js +84 -10
- package/dist/workers-project.js +2 -0
- package/package.json +1 -1
- package/runtime/bootstrap.mjs +65 -7
- package/runtime/browser-worker.mjs +6 -2
- package/runtime/display.d.mts +24 -2
- package/runtime/display.mjs +42 -1
- package/runtime/evaluator.d.mts +5 -1
- package/runtime/evaluator.mjs +12 -1
- package/runtime/workerd-worker.mjs +6 -2
package/dist/workerd-target.js
CHANGED
|
@@ -18,10 +18,10 @@ import { Buffer } from "node:buffer";
|
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { execFileSync, spawn } from "node:child_process";
|
|
20
20
|
import { randomBytes } from "node:crypto";
|
|
21
|
-
import { copyFileSync, existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
-
import { builtinModules } from "node:module";
|
|
21
|
+
import { copyFileSync, existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { builtinModules, createRequire } from "node:module";
|
|
23
23
|
import { tmpdir } from "node:os";
|
|
24
|
-
import { dirname, join } from "node:path";
|
|
24
|
+
import { basename, delimiter, dirname, isAbsolute, join } from "node:path";
|
|
25
25
|
import { pathToFileURL } from "node:url";
|
|
26
26
|
import { build } from "esbuild";
|
|
27
27
|
import { asEvaluateResult } from "./cdp.js";
|
|
@@ -30,6 +30,7 @@ import { runtimeDir } from "./package.js";
|
|
|
30
30
|
import { killOnExit } from "./children.js";
|
|
31
31
|
import { Interrupted, isTargetMessage, parseBundle, parseJson } from "./target.js";
|
|
32
32
|
import { withStartTimeout } from "./target-api.js";
|
|
33
|
+
import { processesUsage } from "./usage.js";
|
|
33
34
|
import { connectUnixWebSocket } from "./ws-client.js";
|
|
34
35
|
import { findWranglerConfig, startMiniflare } from "./workers-project.js";
|
|
35
36
|
import { ensureInstalledAsync, findLockFile, jsrToNpm, parsePackageSpecifier } from "../runtime/node-hooks.mjs";
|
|
@@ -88,6 +89,50 @@ export function findWorkerd(cwd) {
|
|
|
88
89
|
return "workerd";
|
|
89
90
|
}
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* The native workerd binary behind `executable`. npm's workerd package
|
|
94
|
+
* installs bin/workerd as a Node script that runs the platform package's
|
|
95
|
+
* binary with execFileSync, and only its postinstall swaps the binary in.
|
|
96
|
+
* When install scripts don't run (the npm that comes with Node 27 blocks
|
|
97
|
+
* those not in allowScripts), killing that script leaves the binary running
|
|
98
|
+
* with the driver socket open, so it's never what the target spawns (#397).
|
|
99
|
+
* Anything that isn't the workerd package's script is returned as given.
|
|
100
|
+
*/
|
|
101
|
+
export function nativeWorkerd(executable) {
|
|
102
|
+
const candidates = [];
|
|
103
|
+
let path = executable;
|
|
104
|
+
if (!isAbsolute(path) && !path.includes("/") && !path.includes("\\")) {
|
|
105
|
+
const dir = (process.env.PATH ?? "").split(delimiter).find((d) => d && existsSync(join(d, path)));
|
|
106
|
+
if (dir === undefined)
|
|
107
|
+
return executable;
|
|
108
|
+
path = join(dir, path);
|
|
109
|
+
}
|
|
110
|
+
// node_modules/.bin/workerd, a link or (on Windows) a .cmd shim.
|
|
111
|
+
if (basename(dirname(path)) === ".bin")
|
|
112
|
+
candidates.push(join(dirname(dirname(path)), "workerd"));
|
|
113
|
+
try {
|
|
114
|
+
// A link to <package>/bin/workerd, as a global install makes.
|
|
115
|
+
candidates.push(dirname(dirname(realpathSync(path))));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return executable;
|
|
119
|
+
}
|
|
120
|
+
for (const pkg of candidates) {
|
|
121
|
+
try {
|
|
122
|
+
const { name } = JSON.parse(readFileSync(join(pkg, "package.json"), "utf8"));
|
|
123
|
+
if (name !== "workerd")
|
|
124
|
+
continue;
|
|
125
|
+
// The package's main export is the binary's path.
|
|
126
|
+
const binary = createRequire(join(pkg, "package.json"))(join(pkg, "lib", "main.js")).default;
|
|
127
|
+
if (typeof binary === "string" && existsSync(binary))
|
|
128
|
+
return binary;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Not the npm package, or one this doesn't understand: use it as given.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return executable;
|
|
135
|
+
}
|
|
91
136
|
/** "workerd 2026-10-15" -> "2026-10-15": the newest date it supports. */
|
|
92
137
|
export function compatibilityDateOf(versionOutput) {
|
|
93
138
|
const m = /\b(\d{4}-\d{2}-\d{2})\b/.exec(versionOutput);
|
|
@@ -265,13 +310,16 @@ export class RawWorkerd {
|
|
|
265
310
|
runtime;
|
|
266
311
|
channel;
|
|
267
312
|
#child;
|
|
313
|
+
get pids() {
|
|
314
|
+
return this.#child.pid === undefined ? [] : [this.#child.pid];
|
|
315
|
+
}
|
|
268
316
|
constructor(child, channel, runtime) {
|
|
269
317
|
this.#child = child;
|
|
270
318
|
this.channel = channel;
|
|
271
319
|
this.runtime = runtime;
|
|
272
320
|
}
|
|
273
321
|
static async start(cwd, executable) {
|
|
274
|
-
const workerd = executable ?? findWorkerd(cwd);
|
|
322
|
+
const workerd = nativeWorkerd(executable ?? findWorkerd(cwd));
|
|
275
323
|
const compatibilityDate = compatibilityDateOf(execFileSync(workerd, ["--version"], { encoding: "utf8" }));
|
|
276
324
|
const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
|
|
277
325
|
// mkdtemp makes the directory 0700: only this user reaches the socket,
|
|
@@ -365,9 +413,13 @@ export class WorkerdTarget {
|
|
|
365
413
|
/** Requests that interrupt() abandons when the driver can still answer. */
|
|
366
414
|
#interruptible = new Set();
|
|
367
415
|
#nextId = 0;
|
|
368
|
-
|
|
416
|
+
/** Counts interrupt() calls: a call that saw one start during it was interrupted (#417). */
|
|
417
|
+
#interrupts = 0;
|
|
418
|
+
/** Cells running: a comm handler that starts with none gets its own jupyter.signal (#421). */
|
|
419
|
+
#cells = 0;
|
|
369
420
|
#stopNote;
|
|
370
421
|
#exitedOnce = false;
|
|
422
|
+
#startedAt = Date.now();
|
|
371
423
|
constructor(host, cwd, events, runtime) {
|
|
372
424
|
this.runtime = runtime;
|
|
373
425
|
this.#host = host;
|
|
@@ -486,17 +538,26 @@ export class WorkerdTarget {
|
|
|
486
538
|
});
|
|
487
539
|
}
|
|
488
540
|
async #interruptibly(op, params, what) {
|
|
489
|
-
|
|
541
|
+
const interrupts = this.#interrupts;
|
|
490
542
|
try {
|
|
491
543
|
return await this.#request(op, params, true);
|
|
492
544
|
}
|
|
493
545
|
catch (err) {
|
|
494
|
-
if (err instanceof Abandoned || this.#
|
|
546
|
+
if (err instanceof Abandoned || this.#interrupts !== interrupts)
|
|
495
547
|
throw new Interrupted(`${what} interrupted`);
|
|
496
548
|
throw err;
|
|
497
549
|
}
|
|
498
550
|
}
|
|
499
551
|
async evaluate(script) {
|
|
552
|
+
this.#cells++;
|
|
553
|
+
try {
|
|
554
|
+
return await this.#evaluate(script);
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
this.#cells--;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async #evaluate(script) {
|
|
500
561
|
const modules = {};
|
|
501
562
|
for (const specifier of importSpecifiers(script)) {
|
|
502
563
|
if (BUILTIN.test(specifier) || this.#sent.has(specifier))
|
|
@@ -516,11 +577,14 @@ export class WorkerdTarget {
|
|
|
516
577
|
/**
|
|
517
578
|
* A cell that's waiting on something leaves the driver free: it answers a
|
|
518
579
|
* ping, and the cell is abandoned with the session intact (its pending
|
|
519
|
-
* work still runs, as on Bun
|
|
520
|
-
* stopped and restarts on the next
|
|
580
|
+
* work still runs, as on Bun, and jupyter.signal tells it to stop). A busy
|
|
581
|
+
* cell can't be reached, so workerd is stopped and restarts on the next
|
|
582
|
+
* cell.
|
|
521
583
|
*/
|
|
522
584
|
async interrupt() {
|
|
523
|
-
this.#
|
|
585
|
+
this.#interrupts++;
|
|
586
|
+
// Before abandoning the cell's request, which ends the count.
|
|
587
|
+
const cellRunning = this.#cells > 0;
|
|
524
588
|
const running = [...this.#interruptible];
|
|
525
589
|
const answered = await Promise.race([
|
|
526
590
|
this.#request("ping").then(() => true, () => false),
|
|
@@ -532,6 +596,10 @@ export class WorkerdTarget {
|
|
|
532
596
|
this.#pending.delete(id);
|
|
533
597
|
this.#interruptible.delete(id);
|
|
534
598
|
}
|
|
599
|
+
// After the cell is abandoned, so its reply can't end it first
|
|
600
|
+
// (#418). Not awaited: requests are answered in order, and an abort
|
|
601
|
+
// listener that never returns mustn't hold up the interrupt.
|
|
602
|
+
void this.#request("abort", { cell: cellRunning }).catch(() => { });
|
|
535
603
|
return;
|
|
536
604
|
}
|
|
537
605
|
this.#stopNote = restartNote(this.runtime);
|
|
@@ -552,6 +620,7 @@ export class WorkerdTarget {
|
|
|
552
620
|
content,
|
|
553
621
|
metadata,
|
|
554
622
|
buffers: buffers.map((b) => Buffer.from(b).toString("base64")),
|
|
623
|
+
ownSignal: this.#cells === 0,
|
|
555
624
|
}, "Comm handler");
|
|
556
625
|
if (isRecord(reply.error)) {
|
|
557
626
|
const { name, message, stack } = reply.error;
|
|
@@ -584,6 +653,11 @@ export class WorkerdTarget {
|
|
|
584
653
|
}
|
|
585
654
|
/** Never asked for: prompt() in workerd answers null without asking. */
|
|
586
655
|
replyInput() { }
|
|
656
|
+
/** The workerd processes' resident memory and CPU time, from the OS (#410). */
|
|
657
|
+
async usage() {
|
|
658
|
+
const pids = this.#host.pids ?? [];
|
|
659
|
+
return { startedAt: this.#startedAt, ...(await processesUsage(pids)) };
|
|
660
|
+
}
|
|
587
661
|
kill() {
|
|
588
662
|
// workerd's graceful shutdown waits for open connections, the session
|
|
589
663
|
// included, so close it first.
|
package/dist/workers-project.js
CHANGED
|
@@ -132,8 +132,10 @@ class MiniflareHost {
|
|
|
132
132
|
#exitListeners = [];
|
|
133
133
|
#stopped = false;
|
|
134
134
|
#forget;
|
|
135
|
+
pids;
|
|
135
136
|
constructor(mf, channel, version, project, workerd) {
|
|
136
137
|
this.#mf = mf;
|
|
138
|
+
this.pids = workerd;
|
|
137
139
|
// Miniflare kills its workerd only on a normal exit; the helper in
|
|
138
140
|
// src/children.ts kills it if this process is SIGKILLed (#241).
|
|
139
141
|
this.#forget = workerd.map(reap);
|
package/package.json
CHANGED
package/runtime/bootstrap.mjs
CHANGED
|
@@ -97,7 +97,9 @@ const jupyter = createJupyter({
|
|
|
97
97
|
const reply = JSON.parse(line);
|
|
98
98
|
if (reply.interrupted === true) {
|
|
99
99
|
// Interrupted while waiting for the answer: unwind the cell, as
|
|
100
|
-
// KeyboardInterrupt does in Python's input() (#121).
|
|
100
|
+
// KeyboardInterrupt does in Python's input() (#121). The host
|
|
101
|
+
// doesn't interrupt the target then, so abort jupyter.signal here.
|
|
102
|
+
jupyter.abortSignal(reply.cell !== false);
|
|
101
103
|
const err = new Error("Execution interrupted");
|
|
102
104
|
err.name = "Interrupted";
|
|
103
105
|
throw err;
|
|
@@ -217,6 +219,10 @@ globalThis.__cdpk = {
|
|
|
217
219
|
broadcastResult: jupyter.broadcastResult,
|
|
218
220
|
// execute_request's user_expressions (#340).
|
|
219
221
|
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
222
|
+
// jupyter.signal (#418, #421): a new one as each cell starts; the host
|
|
223
|
+
// aborts the running ones on interrupt.
|
|
224
|
+
startCell: jupyter.startCell,
|
|
225
|
+
abortSignal: jupyter.abortSignal,
|
|
220
226
|
// The debugger's rich variable viewer (#18), synchronously: it runs while
|
|
221
227
|
// the target is paused. A value whose $display is async shows as text.
|
|
222
228
|
richBundle(value) {
|
|
@@ -225,10 +231,10 @@ globalThis.__cdpk = {
|
|
|
225
231
|
},
|
|
226
232
|
// Frontend comm messages (#9), from the host's shell channel. Handlers
|
|
227
233
|
// run after one macrotask, like cells, so interrupt can reach them.
|
|
228
|
-
async commDispatch(msgType, content, metadata, buffers64) {
|
|
234
|
+
async commDispatch(msgType, content, metadata, buffers64, ownSignal) {
|
|
229
235
|
await yieldTask();
|
|
230
236
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
231
|
-
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
237
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers, ownSignal);
|
|
232
238
|
},
|
|
233
239
|
// The same, for a message the host wrote to stdin (Node and Deno). A
|
|
234
240
|
// value passed as a CDP call argument is compiled as source, and V8's
|
|
@@ -236,8 +242,23 @@ globalThis.__cdpk = {
|
|
|
236
242
|
// each message's data would stay in memory (#287). Read before the first
|
|
237
243
|
// await, so messages are taken in the order the host wrote them.
|
|
238
244
|
commDispatchFromStdin() {
|
|
239
|
-
const
|
|
240
|
-
|
|
245
|
+
const dispatch = () => {
|
|
246
|
+
const { msgType, content, metadata, buffers, ownSignal } = readCommRecord();
|
|
247
|
+
return globalThis.__cdpk.commDispatch(msgType, content, metadata, buffers, ownSignal === true);
|
|
248
|
+
};
|
|
249
|
+
// Node's inspector can run this call in the middle of a blocking stdin
|
|
250
|
+
// read, e.g. a prompt's, when the message came on a kernel subshell
|
|
251
|
+
// (#417): reading then would take bytes from the middle of that read.
|
|
252
|
+
if (stdinReading) {
|
|
253
|
+
const resumed = new Promise((resolve) => afterStdinRead.push(resolve));
|
|
254
|
+
// A read is synchronous, so by the next task it has ended. If the flag
|
|
255
|
+
// is still set, an interrupt terminated the read before its finally.
|
|
256
|
+
yieldTask().then(() => {
|
|
257
|
+
if (stdinReading) endStdinRead();
|
|
258
|
+
});
|
|
259
|
+
return resumed.then(dispatch);
|
|
260
|
+
}
|
|
261
|
+
return dispatch();
|
|
241
262
|
},
|
|
242
263
|
describeError(value) {
|
|
243
264
|
if (value instanceof Error) {
|
|
@@ -445,18 +466,55 @@ function readLine() {
|
|
|
445
466
|
}
|
|
446
467
|
}
|
|
447
468
|
|
|
448
|
-
/**
|
|
469
|
+
/**
|
|
470
|
+
* Comm records read while waiting for a prompt's reply. A comm message on
|
|
471
|
+
* a kernel subshell (#417) can be written while a cell waits in input();
|
|
472
|
+
* its dispatch runs after, and takes it from here.
|
|
473
|
+
*/
|
|
474
|
+
const commRecords = [];
|
|
475
|
+
|
|
476
|
+
/** Whether a read from stdin is in progress, and what waits for it to end. */
|
|
477
|
+
let stdinReading = false;
|
|
478
|
+
const afterStdinRead = [];
|
|
479
|
+
|
|
480
|
+
function readingStdin(read) {
|
|
481
|
+
stdinReading = true;
|
|
482
|
+
try {
|
|
483
|
+
return read();
|
|
484
|
+
} finally {
|
|
485
|
+
endStdinRead();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function endStdinRead() {
|
|
490
|
+
stdinReading = false;
|
|
491
|
+
// In order, so comm records are taken as the host wrote them.
|
|
492
|
+
for (const resume of afterStdinRead.splice(0)) resume();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The next prompt reply's line; comm records before it are kept. */
|
|
449
496
|
function readInputReply() {
|
|
497
|
+
return readingStdin(readInputReplyNow);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function readInputReplyNow() {
|
|
450
501
|
for (;;) {
|
|
451
502
|
const line = readLine();
|
|
452
503
|
const comm = line === null ? null : COMM_HEADER.exec(line);
|
|
453
504
|
if (comm === null) return line;
|
|
454
|
-
readExactly(Number(comm[1]));
|
|
505
|
+
const body = readExactly(Number(comm[1]));
|
|
506
|
+
if (body !== null) commRecords.push(body);
|
|
455
507
|
}
|
|
456
508
|
}
|
|
457
509
|
|
|
458
510
|
/** The next comm message, past any prompt reply left unread. */
|
|
459
511
|
function readCommRecord() {
|
|
512
|
+
return readingStdin(readCommRecordNow);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function readCommRecordNow() {
|
|
516
|
+
const kept = commRecords.shift();
|
|
517
|
+
if (kept !== undefined) return JSON.parse(new TextDecoder().decode(kept));
|
|
460
518
|
for (;;) {
|
|
461
519
|
const line = readLine();
|
|
462
520
|
if (line === null) throw new Error("stdin closed before a comm message arrived");
|
|
@@ -164,9 +164,13 @@ function install({ base, cdn: cdnBase }) {
|
|
|
164
164
|
broadcastResult: jupyter.broadcastResult,
|
|
165
165
|
// execute_request's user_expressions (#340).
|
|
166
166
|
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
167
|
-
|
|
167
|
+
// jupyter.signal (#418, #421): a new one as each cell starts; the host
|
|
168
|
+
// aborts the running ones on interrupt.
|
|
169
|
+
startCell: jupyter.startCell,
|
|
170
|
+
abortSignal: jupyter.abortSignal,
|
|
171
|
+
async commDispatch(msgType, content, metadata, buffers64, ownSignal) {
|
|
168
172
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
169
|
-
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
173
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers, ownSignal);
|
|
170
174
|
},
|
|
171
175
|
describeError(value) {
|
|
172
176
|
if (value instanceof Error) {
|
package/runtime/display.d.mts
CHANGED
|
@@ -42,6 +42,12 @@ export interface JupyterNamespace {
|
|
|
42
42
|
comm: CommApi;
|
|
43
43
|
/** An anywidget-compatible widget; display the result to show it. */
|
|
44
44
|
widget(options: WidgetOptions): Widget;
|
|
45
|
+
/**
|
|
46
|
+
* Aborted when the kernel interrupts this cell. Each cell gets its own,
|
|
47
|
+
* as does a comm handler that starts while no cell runs. Read it when
|
|
48
|
+
* the work starts: a later read gets the signal of whatever started last.
|
|
49
|
+
*/
|
|
50
|
+
readonly signal: AbortSignal;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export interface Jupyter {
|
|
@@ -51,8 +57,24 @@ export interface Jupyter {
|
|
|
51
57
|
formatNow(obj: unknown): unknown;
|
|
52
58
|
confirm(message?: string): boolean;
|
|
53
59
|
prompt(message?: string, defaultValue?: string, options?: { password?: boolean }): string | null;
|
|
54
|
-
/**
|
|
55
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Routes a frontend comm message into the kernel's comms. `ownSignal`
|
|
62
|
+
* (no cell is running) gives its handlers their own jupyter.signal.
|
|
63
|
+
*/
|
|
64
|
+
commDispatch(
|
|
65
|
+
msgType: string,
|
|
66
|
+
content: { [key: string]: unknown },
|
|
67
|
+
metadata: { [key: string]: unknown },
|
|
68
|
+
buffers: Uint8Array[],
|
|
69
|
+
ownSignal?: boolean,
|
|
70
|
+
): Promise<boolean>;
|
|
71
|
+
/** A new jupyter.signal: a cell is starting (#421). */
|
|
72
|
+
startCell(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Aborts the running comm handlers' jupyter.signal, and the last cell's
|
|
75
|
+
* unless `cellRunning` is false: the kernel was interrupted.
|
|
76
|
+
*/
|
|
77
|
+
abortSignal(cellRunning?: boolean): void;
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
export function escapeHTML(str: string): string;
|
package/runtime/display.mjs
CHANGED
|
@@ -441,8 +441,47 @@ export function createJupyter(host) {
|
|
|
441
441
|
|
|
442
442
|
const comms = createComms((msgType, content, metadata, buffers) => host.send(msgType, content, metadata, buffers));
|
|
443
443
|
|
|
444
|
+
// jupyter.signal (#418): aborted when the kernel is interrupted, so cell
|
|
445
|
+
// code can stop waiting on what the interrupt can't reach, such as a comm
|
|
446
|
+
// reply. Each cell gets its own, as does a comm handler that starts while
|
|
447
|
+
// no cell runs; a handler during a cell shares the cell's. An interrupt
|
|
448
|
+
// aborts only those of the cell and handlers running (#421).
|
|
449
|
+
/** The last cell's controller. */
|
|
450
|
+
let cell = new AbortController();
|
|
451
|
+
/** What jupyter.signal returns: that of whatever started last. */
|
|
452
|
+
let current = cell;
|
|
453
|
+
/** Controllers of running comm handlers, with how many handlers use each. */
|
|
454
|
+
const handlers = new Map();
|
|
455
|
+
function startCell() {
|
|
456
|
+
current = cell = new AbortController();
|
|
457
|
+
}
|
|
458
|
+
/** `cellRunning`: the host has a cell in flight, so the last cell's signal is aborted too. */
|
|
459
|
+
function abortSignal(cellRunning = true) {
|
|
460
|
+
const reason = new DOMException("Execution interrupted", "AbortError");
|
|
461
|
+
if (cellRunning) cell.abort(reason);
|
|
462
|
+
for (const controller of handlers.keys()) controller.abort(reason);
|
|
463
|
+
handlers.clear();
|
|
464
|
+
}
|
|
465
|
+
async function commDispatch(msgType, content, metadata, buffers, ownSignal = false) {
|
|
466
|
+
// A handler during a cell shares the cell's signal, and is tracked
|
|
467
|
+
// apart from it: it can outlive the cell.
|
|
468
|
+
const controller = ownSignal ? new AbortController() : cell;
|
|
469
|
+
if (ownSignal) current = controller;
|
|
470
|
+
handlers.set(controller, (handlers.get(controller) ?? 0) + 1);
|
|
471
|
+
try {
|
|
472
|
+
return await comms.dispatch(msgType, content, metadata, buffers);
|
|
473
|
+
} finally {
|
|
474
|
+
const left = (handlers.get(controller) ?? 0) - 1;
|
|
475
|
+
if (left > 0) handlers.set(controller, left);
|
|
476
|
+
else handlers.delete(controller);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
444
480
|
return {
|
|
445
481
|
namespace: {
|
|
482
|
+
get signal() {
|
|
483
|
+
return current.signal;
|
|
484
|
+
},
|
|
446
485
|
broadcast,
|
|
447
486
|
display,
|
|
448
487
|
format,
|
|
@@ -458,6 +497,8 @@ export function createJupyter(host) {
|
|
|
458
497
|
formatNow,
|
|
459
498
|
confirm,
|
|
460
499
|
prompt,
|
|
461
|
-
commDispatch
|
|
500
|
+
commDispatch,
|
|
501
|
+
startCell,
|
|
502
|
+
abortSignal,
|
|
462
503
|
};
|
|
463
504
|
}
|
package/runtime/evaluator.d.mts
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
export interface EvaluatorShim {
|
|
3
3
|
broadcastResult(executionCount: number, value: unknown): void | Promise<void>;
|
|
4
4
|
describeError(value: unknown): string;
|
|
5
|
-
commDispatch(msgType: string, content: object, metadata: object, buffers64: string[]): Promise<unknown>;
|
|
5
|
+
commDispatch(msgType: string, content: object, metadata: object, buffers64: string[], ownSignal?: boolean): Promise<unknown>;
|
|
6
6
|
barrier(): number;
|
|
7
7
|
/** inspect_request's text for a dotted path, or null (#10). */
|
|
8
8
|
inspectPath(path: string, detail: number): string | null;
|
|
9
9
|
/** Resolves a dotted path without running getters; see introspect.mjs. */
|
|
10
10
|
resolvePath?(path: string): import("./introspect.mjs").Resolved;
|
|
11
|
+
/** A new jupyter.signal, as a cell starts (#421). */
|
|
12
|
+
startCell?(): void;
|
|
13
|
+
/** Aborts the running cell's and comm handlers' jupyter.signal, after an interrupt (#418). */
|
|
14
|
+
abortSignal?(cellRunning?: boolean): void;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/** A request from the host: an `op` plus its parameters. */
|
package/runtime/evaluator.mjs
CHANGED
|
@@ -44,6 +44,7 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
44
44
|
case "eval": {
|
|
45
45
|
// The host uses a result's handle before its next cell.
|
|
46
46
|
handles.clear();
|
|
47
|
+
shim.startCell?.();
|
|
47
48
|
try {
|
|
48
49
|
const box = await evaluate(request.code);
|
|
49
50
|
return { result: box === undefined ? { type: "undefined" } : remote(box[0]) };
|
|
@@ -62,7 +63,13 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
62
63
|
return { value: shim.describeError(take(request.handle)) };
|
|
63
64
|
case "comm":
|
|
64
65
|
try {
|
|
65
|
-
const handled = await shim.commDispatch(
|
|
66
|
+
const handled = await shim.commDispatch(
|
|
67
|
+
request.msgType,
|
|
68
|
+
request.content,
|
|
69
|
+
request.metadata,
|
|
70
|
+
request.buffers,
|
|
71
|
+
request.ownSignal === true,
|
|
72
|
+
);
|
|
66
73
|
return { handled: handled === true };
|
|
67
74
|
} catch (err) {
|
|
68
75
|
return { error: JSON.parse(shim.describeError(err)) };
|
|
@@ -86,6 +93,10 @@ export function createHandler(shim, evaluate = (code) => (0, eval)(code)) {
|
|
|
86
93
|
return { text: shim.inspectPath(request.path, request.detail ?? 0) };
|
|
87
94
|
case "barrier":
|
|
88
95
|
return { value: shim.barrier() };
|
|
96
|
+
case "abort":
|
|
97
|
+
// After an interrupt: jupyter.signal (#418).
|
|
98
|
+
shim.abortSignal?.(request.cell !== false);
|
|
99
|
+
return {};
|
|
89
100
|
default:
|
|
90
101
|
throw new Error(`unknown request ${JSON.stringify(request.op)}`);
|
|
91
102
|
}
|
|
@@ -143,9 +143,13 @@ function install(env) {
|
|
|
143
143
|
broadcastResult: jupyter.broadcastResult,
|
|
144
144
|
// execute_request's user_expressions (#340).
|
|
145
145
|
formatJson: async (value) => JSON.stringify(await jupyter.namespace.format(value)),
|
|
146
|
-
|
|
146
|
+
// jupyter.signal (#418, #421): a new one as each cell starts; the host
|
|
147
|
+
// aborts the running ones on interrupt.
|
|
148
|
+
startCell: jupyter.startCell,
|
|
149
|
+
abortSignal: jupyter.abortSignal,
|
|
150
|
+
async commDispatch(msgType, content, metadata, buffers64, ownSignal) {
|
|
147
151
|
const buffers = buffers64.map((b) => Uint8Array.from(atob(b), (c) => c.charCodeAt(0)));
|
|
148
|
-
return await jupyter.commDispatch(msgType, content, metadata, buffers);
|
|
152
|
+
return await jupyter.commDispatch(msgType, content, metadata, buffers, ownSignal);
|
|
149
153
|
},
|
|
150
154
|
describeError(value) {
|
|
151
155
|
if (value instanceof Error) {
|