@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
package/dist/kernel.js
CHANGED
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
import { Buffer } from "node:buffer";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import { decodeMessage, encodeMessage, PROTOCOL_VERSION, verify } from "./wire.js";
|
|
10
|
+
import { decodeMessage, encodeMessage, makeHeader, PROTOCOL_VERSION, verify } from "./wire.js";
|
|
11
11
|
import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
|
|
12
12
|
import { Interrupted, Target } from "./target.js";
|
|
13
13
|
import { BunTarget } from "./bun-target.js";
|
|
14
|
+
import { Debugger } from "./debugger.js";
|
|
14
15
|
import { WorkerdTarget } from "./workerd-target.js";
|
|
15
16
|
import { launchCelld } from "./celld-target.js";
|
|
16
17
|
import { CellSyntaxError, objectLiteralCandidate } from "./transform.js";
|
|
17
18
|
import { loadDenoProject } from "./deno-config.js";
|
|
18
|
-
import { checkIsComplete, codePointToUtf16Index, exprAtCursor, utf16ToCodePointIndex } from "./text.js";
|
|
19
|
+
import { checkIsComplete, codePointToUtf16Index, exprAtCursor, inspectTarget, utf16ToCodePointIndex } from "./text.js";
|
|
20
|
+
import { queryHistory } from "./history.js";
|
|
19
21
|
import { VERSION } from "./package.js";
|
|
20
22
|
export function parseConnectionInfo(json) {
|
|
21
23
|
const raw = JSON.parse(json);
|
|
@@ -26,6 +28,10 @@ export function parseConnectionInfo(json) {
|
|
|
26
28
|
if (typeof info[port] !== "number")
|
|
27
29
|
throw new Error(`connection file: missing ${port}`);
|
|
28
30
|
}
|
|
31
|
+
for (const field of ["ip", "key"]) {
|
|
32
|
+
if (info[field] !== undefined && typeof info[field] !== "string")
|
|
33
|
+
throw new Error(`connection file: ${field} is not a string`);
|
|
34
|
+
}
|
|
29
35
|
if (info.transport !== undefined && info.transport !== "tcp") {
|
|
30
36
|
throw new Error(`unsupported transport ${info.transport}; only tcp is implemented`);
|
|
31
37
|
}
|
|
@@ -44,6 +50,18 @@ export function parseConnectionInfo(json) {
|
|
|
44
50
|
hb_port: info.hb_port,
|
|
45
51
|
};
|
|
46
52
|
}
|
|
53
|
+
/** How long interrupt_request may wait on the target before it's answered anyway (#121). */
|
|
54
|
+
const INTERRUPT_REPLY_WAIT_MS = 5_000;
|
|
55
|
+
const NO_INPUT = { value: null, interrupted: false };
|
|
56
|
+
/** Shell requests other than execute_request, and their reply types. */
|
|
57
|
+
const SHELL_REPLIES = {
|
|
58
|
+
kernel_info_request: "kernel_info_reply",
|
|
59
|
+
complete_request: "complete_reply",
|
|
60
|
+
is_complete_request: "is_complete_reply",
|
|
61
|
+
inspect_request: "inspect_reply",
|
|
62
|
+
history_request: "history_reply",
|
|
63
|
+
comm_info_request: "comm_info_reply",
|
|
64
|
+
};
|
|
47
65
|
export class Kernel {
|
|
48
66
|
#conn;
|
|
49
67
|
#runtime;
|
|
@@ -52,6 +70,10 @@ export class Kernel {
|
|
|
52
70
|
#project;
|
|
53
71
|
#projectWarning = null;
|
|
54
72
|
#noticeShownFor = null;
|
|
73
|
+
/** JupyterLab's visual debugger, for targets driven over CDP (#18). */
|
|
74
|
+
#debugger;
|
|
75
|
+
/** Cells run with store_history, for history_request (#10). */
|
|
76
|
+
#history = [];
|
|
55
77
|
#session = randomUUID();
|
|
56
78
|
#shell;
|
|
57
79
|
#control;
|
|
@@ -63,6 +85,16 @@ export class Kernel {
|
|
|
63
85
|
#parent = {};
|
|
64
86
|
#allowStdin = false;
|
|
65
87
|
#executing = false;
|
|
88
|
+
/**
|
|
89
|
+
* stop_on_error (#123): how many of the shell messages queued when a cell
|
|
90
|
+
* failed are still to come, and whether the one being handled is one.
|
|
91
|
+
*/
|
|
92
|
+
#abortQueued = 0;
|
|
93
|
+
#aborting = false;
|
|
94
|
+
/** The input_request waiting for its input_reply, if any. */
|
|
95
|
+
#input = null;
|
|
96
|
+
/** An interrupt is waiting on the target. */
|
|
97
|
+
#interrupting = false;
|
|
66
98
|
// Open comms, for comm_info_request: comm_id -> target_name (#9).
|
|
67
99
|
#comms = new Map();
|
|
68
100
|
#shuttingDown = false;
|
|
@@ -71,6 +103,9 @@ export class Kernel {
|
|
|
71
103
|
this.#runtime = runtime;
|
|
72
104
|
this.#denoCompat = options.denoCompat ?? false;
|
|
73
105
|
this.#project = runtime === "deno" ? this.#loadProject(options.notebookDir ?? process.cwd()) : null;
|
|
106
|
+
this.#debugger = runtime === "node" || runtime === "deno"
|
|
107
|
+
? new Debugger((event) => this.#publish("debug_event", event), () => this.#transformOptions())
|
|
108
|
+
: null;
|
|
74
109
|
startHeartbeat(conn.hb_port, conn.ip);
|
|
75
110
|
this.#shell = new RouterSocket(conn.shell_port, conn.ip);
|
|
76
111
|
this.#control = new RouterSocket(conn.control_port, conn.ip);
|
|
@@ -81,8 +116,17 @@ export class Kernel {
|
|
|
81
116
|
// Start the runtime eagerly so the first cell doesn't pay for it.
|
|
82
117
|
this.#ensureTarget().catch((err) => this.#log(`target failed to start: ${err}`));
|
|
83
118
|
process.on("SIGINT", () => void this.#interrupt());
|
|
119
|
+
// Exiting runs the exit hook that kills the runtime (src/children.ts);
|
|
120
|
+
// these signals' default action would skip it (#218).
|
|
121
|
+
for (const [signal, code] of [["SIGTERM", 143], ["SIGHUP", 129]]) {
|
|
122
|
+
process.on(signal, () => {
|
|
123
|
+
this.#shuttingDown = true;
|
|
124
|
+
process.exit(code);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
84
127
|
await Promise.all([this.#loop(this.#shell, (m, p) => this.#handleShell(m, p)),
|
|
85
|
-
this.#loop(this.#control, (m, p) => this.#handleControl(m, p))
|
|
128
|
+
this.#loop(this.#control, (m, p) => this.#handleControl(m, p)),
|
|
129
|
+
this.#loop(this.#stdin, (m) => this.#handleStdin(m))]);
|
|
86
130
|
}
|
|
87
131
|
#log(text) {
|
|
88
132
|
process.stderr.write(`[cdp-kernel] ${text}\n`);
|
|
@@ -181,7 +225,9 @@ export class Kernel {
|
|
|
181
225
|
#ensureTarget() {
|
|
182
226
|
if (this.#target)
|
|
183
227
|
return Promise.resolve(this.#target);
|
|
184
|
-
this.#targetStarting ??= this.#launchTarget().then((target) => {
|
|
228
|
+
this.#targetStarting ??= this.#launchTarget().then(async (target) => {
|
|
229
|
+
// Before any cell runs on it, so breakpoints already apply.
|
|
230
|
+
await this.#debugger?.attachTarget(target.debug ?? null);
|
|
185
231
|
this.#target = target;
|
|
186
232
|
this.#targetStarting = null;
|
|
187
233
|
return target;
|
|
@@ -194,9 +240,12 @@ export class Kernel {
|
|
|
194
240
|
#targetEvents() {
|
|
195
241
|
return {
|
|
196
242
|
stream: (name, text) => this.#publish("stream", { name, text }),
|
|
197
|
-
message: (msg) => void this.#onTargetMessage(msg),
|
|
243
|
+
message: (msg) => void this.#onTargetMessage(msg).catch((err) => this.#log(`error handling target message: ${err}`)),
|
|
198
244
|
exit: (code, note) => {
|
|
199
245
|
this.#target = null;
|
|
246
|
+
// Its prompt, if any, died with it; a late reply is dropped.
|
|
247
|
+
this.#input = null;
|
|
248
|
+
void this.#debugger?.attachTarget(null);
|
|
200
249
|
if (!this.#shuttingDown) {
|
|
201
250
|
const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
|
|
202
251
|
this.#publish("stream", { name: "stderr", text: `\n${text}\n` });
|
|
@@ -213,18 +262,37 @@ export class Kernel {
|
|
|
213
262
|
});
|
|
214
263
|
return;
|
|
215
264
|
}
|
|
216
|
-
|
|
265
|
+
// The answer goes to the target that asked, not one started since.
|
|
266
|
+
const target = this.#target;
|
|
267
|
+
let answer = NO_INPUT;
|
|
217
268
|
try {
|
|
218
|
-
|
|
269
|
+
answer = await this.#requestInput(msg.prompt, msg.password);
|
|
219
270
|
}
|
|
220
271
|
finally {
|
|
221
272
|
// Always answer exactly once so the target's blocking read returns.
|
|
222
|
-
this.#target
|
|
273
|
+
if (target !== null && target === this.#target)
|
|
274
|
+
target.replyInput(answer.value, answer.interrupted);
|
|
223
275
|
}
|
|
224
276
|
}
|
|
225
277
|
async #interrupt() {
|
|
226
|
-
|
|
278
|
+
// A target blocked reading its prompt's answer can't run
|
|
279
|
+
// terminateExecution, and would never answer it. Make the prompt throw
|
|
280
|
+
// instead, which ends the cell (#121).
|
|
281
|
+
const input = this.#input;
|
|
282
|
+
if (input !== null) {
|
|
283
|
+
this.#input = null;
|
|
284
|
+
input.settle({ value: null, interrupted: true });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!this.#target || !this.#executing)
|
|
288
|
+
return;
|
|
289
|
+
this.#interrupting = true;
|
|
290
|
+
try {
|
|
227
291
|
await this.#target.interrupt();
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
this.#interrupting = false;
|
|
295
|
+
}
|
|
228
296
|
}
|
|
229
297
|
// --- messaging --------------------------------------------------------------
|
|
230
298
|
#encode(msgType, content, parentHeader, extra = {}) {
|
|
@@ -242,6 +310,11 @@ export class Kernel {
|
|
|
242
310
|
async #loop(socket, handle) {
|
|
243
311
|
while (!this.#shuttingDown) {
|
|
244
312
|
const { peer, frames } = await socket.recv();
|
|
313
|
+
if (socket === this.#shell) {
|
|
314
|
+
this.#aborting = this.#abortQueued > 0;
|
|
315
|
+
if (this.#aborting)
|
|
316
|
+
this.#abortQueued--;
|
|
317
|
+
}
|
|
245
318
|
try {
|
|
246
319
|
const msg = decodeMessage(frames);
|
|
247
320
|
// Unsigned or forged messages are dropped, as in Deno.
|
|
@@ -272,22 +345,56 @@ export class Kernel {
|
|
|
272
345
|
},
|
|
273
346
|
banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (${versions?.engine || "?"})`,
|
|
274
347
|
help_links: [],
|
|
275
|
-
debugger:
|
|
348
|
+
debugger: this.#debugger !== null,
|
|
349
|
+
// What current ipykernel advertises; JupyterLab itself goes by the
|
|
350
|
+
// kernelspec's metadata.debugger.
|
|
351
|
+
supported_features: this.#debugger !== null ? ["debugger"] : [],
|
|
276
352
|
};
|
|
277
353
|
}
|
|
278
354
|
// --- control channel ------------------------------------------------------
|
|
279
355
|
async #handleControl(msg, peer) {
|
|
356
|
+
// Every request is wrapped in busy/idle, as on shell (#124). JupyterLab
|
|
357
|
+
// treats a debug request as done only once the kernel reports idle for
|
|
358
|
+
// it. (Jupyter Server leaves debug_request out of the kernel's status.)
|
|
359
|
+
this.#status("busy", msg.header);
|
|
360
|
+
try {
|
|
361
|
+
await this.#controlRequest(this.#control, msg, peer);
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
this.#status("idle", msg.header);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** Control requests; shutdown and interrupt may also arrive on shell (#124). */
|
|
368
|
+
async #controlRequest(socket, msg, peer) {
|
|
280
369
|
switch (msg.header.msg_type) {
|
|
281
370
|
case "kernel_info_request":
|
|
282
|
-
this.#reply(
|
|
371
|
+
this.#reply(socket, peer, msg, "kernel_info_reply", this.#kernelInfo());
|
|
283
372
|
break;
|
|
284
373
|
case "interrupt_request":
|
|
285
|
-
|
|
286
|
-
|
|
374
|
+
// Answer even if the target doesn't (#121); the interrupt goes on.
|
|
375
|
+
await Promise.race([
|
|
376
|
+
this.#interrupt().catch((err) => this.#log(`interrupt failed: ${err}`)),
|
|
377
|
+
new Promise((r) => setTimeout(r, INTERRUPT_REPLY_WAIT_MS)),
|
|
378
|
+
]);
|
|
379
|
+
this.#reply(socket, peer, msg, "interrupt_reply", { status: "ok" });
|
|
287
380
|
break;
|
|
381
|
+
case "debug_request": {
|
|
382
|
+
let reply;
|
|
383
|
+
if (this.#debugger === null) {
|
|
384
|
+
reply = { type: "response", request_seq: msg.content.seq, command: msg.content.command, success: false, message: `${this.#runtime} targets have no debugger`, body: {} };
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// Everything but debugInfo talks to the target, so make sure there is one.
|
|
388
|
+
if (msg.content.command !== "debugInfo")
|
|
389
|
+
await this.#ensureTarget().catch(() => null);
|
|
390
|
+
reply = await this.#debugger.handle(msg.content);
|
|
391
|
+
}
|
|
392
|
+
this.#reply(socket, peer, msg, "debug_reply", reply);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
288
395
|
case "shutdown_request": {
|
|
289
396
|
const restart = msg.content.restart === true;
|
|
290
|
-
this.#reply(
|
|
397
|
+
this.#reply(socket, peer, msg, "shutdown_reply", { status: "ok", restart });
|
|
291
398
|
this.#shuttingDown = true;
|
|
292
399
|
this.#target?.kill();
|
|
293
400
|
// Frontends spawn a fresh kernel even on restart; exit after the
|
|
@@ -316,10 +423,10 @@ export class Kernel {
|
|
|
316
423
|
this.#reply(this.#shell, peer, msg, "is_complete_reply", checkIsComplete(String(msg.content.code ?? "")));
|
|
317
424
|
break;
|
|
318
425
|
case "inspect_request":
|
|
319
|
-
this.#reply(this.#shell, peer, msg, "inspect_reply",
|
|
426
|
+
this.#reply(this.#shell, peer, msg, "inspect_reply", await this.#inspect(msg));
|
|
320
427
|
break;
|
|
321
428
|
case "history_request":
|
|
322
|
-
this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history:
|
|
429
|
+
this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: queryHistory(this.#history, msg.content) });
|
|
323
430
|
break;
|
|
324
431
|
case "comm_info_request":
|
|
325
432
|
this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: this.#commInfo(msg.content) });
|
|
@@ -329,12 +436,44 @@ export class Kernel {
|
|
|
329
436
|
case "comm_close":
|
|
330
437
|
await this.#handleComm(msg);
|
|
331
438
|
break;
|
|
439
|
+
case "shutdown_request":
|
|
440
|
+
case "interrupt_request":
|
|
441
|
+
// Deprecated on shell, but still allowed, and ipykernel takes them.
|
|
442
|
+
await this.#controlRequest(this.#shell, msg, peer);
|
|
443
|
+
break;
|
|
444
|
+
default:
|
|
445
|
+
this.#log(`ignoring unknown shell message ${type}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
// E.g. the runtime failed to launch. Answer anyway, so the frontend
|
|
450
|
+
// doesn't wait on the request forever (#125).
|
|
451
|
+
const replyType = SHELL_REPLIES[type];
|
|
452
|
+
if (replyType !== undefined) {
|
|
453
|
+
const name = err instanceof Error ? err.name : "Error";
|
|
454
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
455
|
+
const error = { ename: name, evalue: message, traceback: [`${name}: ${message}`] };
|
|
456
|
+
this.#reply(this.#shell, peer, msg, replyType, { status: "error", ...error });
|
|
332
457
|
}
|
|
458
|
+
throw err;
|
|
333
459
|
}
|
|
334
460
|
finally {
|
|
335
461
|
this.#status("idle", msg.header);
|
|
336
462
|
}
|
|
337
463
|
}
|
|
464
|
+
/** The tooltip (Shift-Tab) for the name at the cursor (#10). */
|
|
465
|
+
async #inspect(msg) {
|
|
466
|
+
const code = String(msg.content.code ?? "");
|
|
467
|
+
const cursorCp = typeof msg.content.cursor_pos === "number"
|
|
468
|
+
? msg.content.cursor_pos
|
|
469
|
+
: utf16ToCodePointIndex(code, code.length);
|
|
470
|
+
const path = inspectTarget(code, codePointToUtf16Index(code, cursorCp));
|
|
471
|
+
const detail = msg.content.detail_level === 1 ? 1 : 0;
|
|
472
|
+
const text = path === null ? null : await (await this.#ensureTarget()).inspect(path, detail);
|
|
473
|
+
return text === null
|
|
474
|
+
? { status: "ok", found: false, data: {}, metadata: {} }
|
|
475
|
+
: { status: "ok", found: true, data: { "text/plain": text }, metadata: {} };
|
|
476
|
+
}
|
|
338
477
|
async #complete(msg) {
|
|
339
478
|
const code = String(msg.content.code ?? "");
|
|
340
479
|
const cursorCp = typeof msg.content.cursor_pos === "number"
|
|
@@ -366,31 +505,53 @@ export class Kernel {
|
|
|
366
505
|
};
|
|
367
506
|
}
|
|
368
507
|
async #requestInput(prompt, password) {
|
|
508
|
+
// The target asked while an interrupt was already waiting on it.
|
|
509
|
+
if (this.#interrupting)
|
|
510
|
+
return { value: null, interrupted: true };
|
|
369
511
|
const parent = this.#parent;
|
|
370
512
|
if (!this.#allowStdin || typeof parent.msg_id !== "string")
|
|
371
|
-
return
|
|
513
|
+
return NO_INPUT;
|
|
372
514
|
if (this.#stdin.peers.size === 0)
|
|
373
515
|
await new Promise((r) => setTimeout(r, 100));
|
|
374
516
|
if (this.#stdin.peers.size === 0)
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
517
|
+
return NO_INPUT;
|
|
518
|
+
const header = makeHeader(this.#session, "input_request");
|
|
519
|
+
const answer = new Promise((settle) => (this.#input = { msgId: header.msg_id, settle }));
|
|
520
|
+
this.#stdin.sendAll(encodeMessage(this.#session, this.#conn.key, "input_request", { prompt, password }, {
|
|
521
|
+
header,
|
|
522
|
+
parentHeader: parent,
|
|
523
|
+
}));
|
|
524
|
+
return answer;
|
|
525
|
+
}
|
|
526
|
+
/** stdin carries only input_replys. */
|
|
527
|
+
async #handleStdin(reply) {
|
|
528
|
+
if (reply.header.msg_type !== "input_reply")
|
|
529
|
+
return;
|
|
530
|
+
const input = this.#input;
|
|
531
|
+
// A reply with nothing waiting is late (its prompt was interrupted), and
|
|
532
|
+
// one naming another request is for an earlier prompt: drop both rather
|
|
533
|
+
// than answer the next prompt with them (#121). Some clients
|
|
534
|
+
// (jupyter_client's input()) send no parent header, so only a present
|
|
535
|
+
// one is checked.
|
|
536
|
+
if (input === null)
|
|
537
|
+
return;
|
|
538
|
+
const parentId = reply.parentHeader.msg_id;
|
|
539
|
+
if (typeof parentId === "string" && parentId !== "" && parentId !== input.msgId)
|
|
540
|
+
return;
|
|
541
|
+
this.#input = null;
|
|
542
|
+
input.settle({ value: typeof reply.content.value === "string" ? reply.content.value : null, interrupted: false });
|
|
386
543
|
}
|
|
387
544
|
async #evaluateCell(target, code) {
|
|
388
545
|
// Object-literal wrapping (Deno's evaluate_line_with_object_wrapping):
|
|
389
546
|
// try `({ ... })` first and fall back to the code as written.
|
|
547
|
+
// While debugging, cells carry the path JupyterLab gave them and keep a
|
|
548
|
+
// line map, so breakpoints bind and frames point at the cell (#18).
|
|
549
|
+
const debugging = this.#debugger?.started === true && target.debug !== undefined;
|
|
550
|
+
const transform = (source) => debugging && this.#debugger ? this.#debugger.cellScript(code, source) : target.transform(source, this.#transformOptions());
|
|
390
551
|
const wrapped = objectLiteralCandidate(code);
|
|
391
552
|
if (wrapped !== null) {
|
|
392
553
|
try {
|
|
393
|
-
const result = await target.evaluate(
|
|
554
|
+
const result = await target.evaluate(transform(wrapped));
|
|
394
555
|
if (!result.exceptionDetails)
|
|
395
556
|
return result;
|
|
396
557
|
}
|
|
@@ -399,19 +560,30 @@ export class Kernel {
|
|
|
399
560
|
throw err;
|
|
400
561
|
}
|
|
401
562
|
}
|
|
402
|
-
return target.evaluate(
|
|
563
|
+
return target.evaluate(transform(code));
|
|
403
564
|
}
|
|
404
565
|
async #execute(msg, peer) {
|
|
405
566
|
const parent = msg.header;
|
|
567
|
+
if (this.#aborting) {
|
|
568
|
+
// Queued behind a failed cell: not run, not counted (#123).
|
|
569
|
+
this.#status("busy", parent);
|
|
570
|
+
this.#reply(this.#shell, peer, msg, "execute_reply", { status: "aborted" });
|
|
571
|
+
this.#status("idle", parent);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
406
574
|
const code = String(msg.content.code ?? "");
|
|
407
575
|
const silent = msg.content.silent === true;
|
|
408
|
-
if (!silent && msg.content.store_history !== false)
|
|
576
|
+
if (!silent && msg.content.store_history !== false) {
|
|
409
577
|
this.#executionCount++;
|
|
578
|
+
this.#history.push({ line: this.#executionCount, input: code });
|
|
579
|
+
}
|
|
410
580
|
const count = this.#executionCount;
|
|
411
581
|
this.#parent = parent;
|
|
412
582
|
this.#allowStdin = msg.content.allow_stdin === true;
|
|
413
583
|
this.#status("busy", parent);
|
|
414
|
-
|
|
584
|
+
// Silent requests broadcast nothing on iopub but status (#124).
|
|
585
|
+
if (!silent)
|
|
586
|
+
this.#publish("execute_input", { code, execution_count: count });
|
|
415
587
|
if (this.#projectWarning !== null) {
|
|
416
588
|
// Shown once, in the first cell, where the user will see it.
|
|
417
589
|
this.#publish("stream", { name: "stderr", text: `[cdp-kernel] ${this.#projectWarning}\n` });
|
|
@@ -461,6 +633,13 @@ export class Kernel {
|
|
|
461
633
|
this.#executing = false;
|
|
462
634
|
}
|
|
463
635
|
if (error) {
|
|
636
|
+
if (!silent && msg.content.stop_on_error !== false) {
|
|
637
|
+
// Abort what was queued behind this cell (JupyterLab's Run All sends
|
|
638
|
+
// every cell at once), but nothing sent after the client sees the
|
|
639
|
+
// error. A macrotask first lets bytes already received be parsed.
|
|
640
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
641
|
+
this.#abortQueued = this.#shell.incoming.size;
|
|
642
|
+
}
|
|
464
643
|
this.#publish("error", error);
|
|
465
644
|
this.#reply(this.#shell, peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
|
|
466
645
|
}
|
package/dist/main.js
CHANGED
|
@@ -42,7 +42,9 @@ function install(runtime, denoCompat, name, display) {
|
|
|
42
42
|
// Interrupts arrive as interrupt_request on the control channel rather
|
|
43
43
|
// than as SIGINT to this process.
|
|
44
44
|
interrupt_mode: "message",
|
|
45
|
-
|
|
45
|
+
// JupyterLab enables its visual debugger from metadata.debugger (#18);
|
|
46
|
+
// only the targets driven over CDP have one.
|
|
47
|
+
metadata: { runtime, denoCompat, version: VERSION, debugger: runtime === "node" || runtime === "deno" },
|
|
46
48
|
};
|
|
47
49
|
writeFileSync(join(dir, "kernel.json"), JSON.stringify(spec, null, 2) + "\n");
|
|
48
50
|
console.log(`installed kernelspec ${name} -> ${dir}`);
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cdp-kernel:
|
|
3
|
-
*
|
|
4
|
-
* Deno
|
|
2
|
+
* cdp-kernel: a Jupyter kernel for TypeScript and JavaScript, extracted from
|
|
3
|
+
* Deno's, that runs notebooks on several JavaScript runtimes. The kernel host
|
|
4
|
+
* runs on Node or Deno; cells run in a separate target runtime: Node or Deno
|
|
5
|
+
* over the Chrome DevTools Protocol, or Bun, workerd and celld, which
|
|
6
|
+
* evaluate cells in-process.
|
|
5
7
|
*
|
|
6
8
|
* Most people want the CLI (`cdp-kernel install`). This module exposes
|
|
7
9
|
* the kernel for embedding: give it a parsed connection file and a target
|
package/dist/mod.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cdp-kernel:
|
|
3
|
-
*
|
|
4
|
-
* Deno
|
|
2
|
+
* cdp-kernel: a Jupyter kernel for TypeScript and JavaScript, extracted from
|
|
3
|
+
* Deno's, that runs notebooks on several JavaScript runtimes. The kernel host
|
|
4
|
+
* runs on Node or Deno; cells run in a separate target runtime: Node or Deno
|
|
5
|
+
* over the Chrome DevTools Protocol, or Bun, workerd and celld, which
|
|
6
|
+
* evaluate cells in-process.
|
|
5
7
|
*
|
|
6
8
|
* Most people want the CLI (`cdp-kernel install`). This module exposes
|
|
7
9
|
* the kernel for embedding: give it a parsed connection file and a target
|
package/dist/murmur2.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// MurmurHash2 (32-bit), as the Jupyter debugger uses it to name a cell's
|
|
2
|
+
// source (#18). JupyterLab computes a cell's path itself, as tmpFilePrefix +
|
|
3
|
+
// murmur2(code, hashSeed) + tmpFileSuffix, and the kernel must name the
|
|
4
|
+
// cell's script the same way, so this follows JupyterLab's
|
|
5
|
+
// packages/debugger/src/hash.ts (Modified BSD License, Copyright (c) Jupyter
|
|
6
|
+
// Development Team), itself adapted from Gary Court's murmurhash2_gc.js
|
|
7
|
+
// (MIT License, Copyright (c) 2011 Gary Court).
|
|
8
|
+
const M = 0x5bd1e995;
|
|
9
|
+
const ENCODER = new TextEncoder();
|
|
10
|
+
/** 32-bit multiply by M, without leaving the safe-integer range. */
|
|
11
|
+
function mul(x) {
|
|
12
|
+
return (x & 0xffff) * M + ((((x >>> 16) * M) & 0xffff) << 16);
|
|
13
|
+
}
|
|
14
|
+
/** The hash of a string's UTF-8 bytes. */
|
|
15
|
+
export function murmur2(str, seed) {
|
|
16
|
+
const data = ENCODER.encode(str);
|
|
17
|
+
let len = data.length;
|
|
18
|
+
let h = seed ^ len;
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (len >= 4) {
|
|
21
|
+
let k = data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24);
|
|
22
|
+
k = mul(k);
|
|
23
|
+
k ^= k >>> 24;
|
|
24
|
+
k = mul(k);
|
|
25
|
+
h = mul(h) ^ k;
|
|
26
|
+
len -= 4;
|
|
27
|
+
i += 4;
|
|
28
|
+
}
|
|
29
|
+
if (len === 3)
|
|
30
|
+
h ^= data[i + 2] << 16;
|
|
31
|
+
if (len >= 2)
|
|
32
|
+
h ^= data[i + 1] << 8;
|
|
33
|
+
if (len >= 1) {
|
|
34
|
+
h ^= data[i];
|
|
35
|
+
h = mul(h);
|
|
36
|
+
}
|
|
37
|
+
h ^= h >>> 13;
|
|
38
|
+
h = mul(h);
|
|
39
|
+
h ^= h >>> 15;
|
|
40
|
+
return h >>> 0;
|
|
41
|
+
}
|
package/dist/package.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
/** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
3
|
-
|
|
2
|
+
export declare const VERSION = "0.5.0";
|
|
3
|
+
/**
|
|
4
|
+
* The UTC day this version was released, set with VERSION. The publish
|
|
5
|
+
* workflow refuses a tag unless it's today or yesterday, so it can't go
|
|
6
|
+
* stale: see dependencyCutoff.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RELEASE_DATE = "2026-10-16";
|
|
9
|
+
/**
|
|
10
|
+
* Deno blocks packages less than 24 hours old (minimum dependency age), so
|
|
11
|
+
* a kernelspec relaunching a release from JSR would fail on release day. It
|
|
12
|
+
* relaunches with this cutoff instead (#12): anything published up to two
|
|
13
|
+
* days after the release is allowed (the release itself included, however
|
|
14
|
+
* late in the day its tag was pushed), and nothing newer ever is.
|
|
15
|
+
*/
|
|
16
|
+
export declare function dependencyCutoff(releaseDate?: string): string;
|
|
17
|
+
export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "bun-channel.mjs", "evaluator.mjs", "introspect.mjs", "workerd-worker.mjs", "display.mjs", "comm.mjs", "widget.mjs", "node-hooks.mjs", "deno-compat.mjs", "node-modules-resolve.mjs"];
|
|
4
18
|
export type HostRuntime = "node" | "deno";
|
|
5
19
|
export declare function hostRuntime(): HostRuntime;
|
|
6
20
|
/** Where the CLI module was loaded from. */
|
package/dist/package.js
CHANGED
|
@@ -14,11 +14,32 @@ import { join, sep } from "node:path";
|
|
|
14
14
|
import process from "node:process";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
/** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
|
|
17
|
-
export const VERSION = "0.
|
|
17
|
+
export const VERSION = "0.5.0";
|
|
18
|
+
/**
|
|
19
|
+
* The UTC day this version was released, set with VERSION. The publish
|
|
20
|
+
* workflow refuses a tag unless it's today or yesterday, so it can't go
|
|
21
|
+
* stale: see dependencyCutoff.
|
|
22
|
+
*/
|
|
23
|
+
export const RELEASE_DATE = "2026-10-16";
|
|
24
|
+
/**
|
|
25
|
+
* Deno blocks packages less than 24 hours old (minimum dependency age), so
|
|
26
|
+
* a kernelspec relaunching a release from JSR would fail on release day. It
|
|
27
|
+
* relaunches with this cutoff instead (#12): anything published up to two
|
|
28
|
+
* days after the release is allowed (the release itself included, however
|
|
29
|
+
* late in the day its tag was pushed), and nothing newer ever is.
|
|
30
|
+
*/
|
|
31
|
+
export function dependencyCutoff(releaseDate = RELEASE_DATE) {
|
|
32
|
+
const day = new Date(`${releaseDate}T00:00:00Z`);
|
|
33
|
+
if (Number.isNaN(day.getTime()))
|
|
34
|
+
throw new Error(`not a date: ${releaseDate}`);
|
|
35
|
+
day.setUTCDate(day.getUTCDate() + 2);
|
|
36
|
+
return day.toISOString().slice(0, 10);
|
|
37
|
+
}
|
|
18
38
|
export const RUNTIME_FILES = [
|
|
19
39
|
"bootstrap.mjs",
|
|
20
40
|
"bun-channel.mjs",
|
|
21
41
|
"evaluator.mjs",
|
|
42
|
+
"introspect.mjs",
|
|
22
43
|
"workerd-worker.mjs",
|
|
23
44
|
"display.mjs",
|
|
24
45
|
"comm.mjs",
|
|
@@ -54,12 +75,14 @@ export function kernelArgv(host, execPath, origin, args) {
|
|
|
54
75
|
}
|
|
55
76
|
// Deno: a checkout needs its deno.jsonc import map; a JSR package brings
|
|
56
77
|
// its own, so skip whatever config the notebook's directory has.
|
|
57
|
-
const flags = origin.kind === "jsr"
|
|
78
|
+
const flags = origin.kind === "jsr"
|
|
79
|
+
? ["--no-config", "--no-lock", `--min-dep-age=${dependencyCutoff()}`]
|
|
80
|
+
: ["--no-lock"];
|
|
58
81
|
const entry = origin.kind === "jsr" ? origin.specifier : origin.path;
|
|
59
82
|
return [execPath, "run", "--allow-all", ...flags, entry, ...args];
|
|
60
83
|
}
|
|
61
84
|
export function cacheRoot() {
|
|
62
|
-
const base = process.env.XDG_CACHE_HOME
|
|
85
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
63
86
|
return join(base, "cdp-kernel");
|
|
64
87
|
}
|
|
65
88
|
async function fetchText(url) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** One segment's VLQ fields: generated column, source, line, column, name. */
|
|
2
|
+
export declare function decodeVlq(segment: string): number[];
|
|
3
|
+
/**
|
|
4
|
+
* For each generated line (0-based), the first original line (0-based) a
|
|
5
|
+
* segment on it maps to, or -1. Source-map fields are relative to the
|
|
6
|
+
* previous segment's; only the original line is tracked.
|
|
7
|
+
*/
|
|
8
|
+
export declare function generatedToOriginalLines(mappings: string): number[];
|
|
9
|
+
/** Both directions between the lines of a cell and of its script (0-based). */
|
|
10
|
+
export declare class LineMap {
|
|
11
|
+
#private;
|
|
12
|
+
/** `offset`: lines the script adds before esbuild's output (the prelude). */
|
|
13
|
+
constructor(mappings: string, offset?: number);
|
|
14
|
+
/** The cell line a script line came from: its own mapping, or the nearest one above. */
|
|
15
|
+
toOriginal(generated: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* The script line for a cell line, or for the next cell line that has
|
|
18
|
+
* code (a breakpoint on a blank or comment line moves down, as debuggers
|
|
19
|
+
* do), or null past the end.
|
|
20
|
+
*/
|
|
21
|
+
toGenerated(original: number): number | null;
|
|
22
|
+
}
|