@tlockney/cdp-kernel 0.6.0 → 0.7.1

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.
Files changed (53) hide show
  1. package/README.md +5 -3
  2. package/dist/browser/channels.d.ts +27 -0
  3. package/dist/browser/channels.js +90 -0
  4. package/dist/browser/kernel.js +29800 -0
  5. package/dist/browser/kernel.js.map +7 -0
  6. package/dist/browser/mod.d.ts +25 -0
  7. package/dist/browser/mod.js +41 -0
  8. package/dist/browser/target.d.ts +94 -0
  9. package/dist/browser/target.js +264 -0
  10. package/dist/browser/worker.js +1071 -0
  11. package/dist/browser/worker.js.map +7 -0
  12. package/dist/bun-target.d.ts +2 -0
  13. package/dist/bun-target.js +15 -2
  14. package/dist/cdp.d.ts +11 -0
  15. package/dist/cdp.js +34 -7
  16. package/dist/celld-target.js +3 -2
  17. package/dist/debugger.d.ts +2 -0
  18. package/dist/debugger.js +63 -11
  19. package/dist/kernel-core.d.ts +78 -0
  20. package/dist/kernel-core.js +824 -0
  21. package/dist/kernel.d.ts +6 -1
  22. package/dist/kernel.js +104 -723
  23. package/dist/main.js +11 -4
  24. package/dist/message.d.ts +34 -0
  25. package/dist/message.js +14 -0
  26. package/dist/package.d.ts +2 -2
  27. package/dist/package.js +2 -2
  28. package/dist/target-api.d.ts +92 -0
  29. package/dist/target-api.js +48 -0
  30. package/dist/target.d.ts +5 -76
  31. package/dist/target.js +62 -33
  32. package/dist/traceback.d.ts +75 -0
  33. package/dist/traceback.js +200 -0
  34. package/dist/transform.d.ts +1 -0
  35. package/dist/transform.js +8 -1
  36. package/dist/wire.d.ts +4 -24
  37. package/dist/wire.js +3 -12
  38. package/dist/workerd-target.d.ts +6 -1
  39. package/dist/workerd-target.js +56 -12
  40. package/dist/workers-project.d.ts +1 -1
  41. package/dist/workers-project.js +4 -3
  42. package/dist/zmq-channels.d.ts +14 -0
  43. package/dist/zmq-channels.js +59 -0
  44. package/package.json +9 -2
  45. package/runtime/bootstrap.mjs +2 -0
  46. package/runtime/browser-worker.mjs +202 -0
  47. package/runtime/cdn.d.mts +2 -0
  48. package/runtime/cdn.mjs +17 -0
  49. package/runtime/display.mjs +202 -1
  50. package/runtime/evaluator.mjs +2 -0
  51. package/runtime/format.d.mts +2 -0
  52. package/runtime/format.mjs +125 -0
  53. package/runtime/workerd-worker.mjs +2 -0
package/dist/kernel.js CHANGED
@@ -1,29 +1,57 @@
1
- // Jupyter message handling: shell, control, stdin and iopub.
2
- //
3
- // Ported from startJupyterKernel in Deno's cli/js/jupyter_kernel.js
4
- // (v2.9.6), which is Copyright 2018-2026 the Deno authors, MIT license.
5
- // The thirteen op_jupyter_* calls into Deno's REPL isolate are replaced by
6
- // a Target driven over CDP (src/target.ts).
7
- import { Buffer } from "node:buffer";
8
- import { existsSync } from "node:fs";
1
+ // The kernel process: KernelCore (src/kernel-core.ts) on the connection
2
+ // file's ZeroMQ ports, with a target runtime launched as a child process,
3
+ // the language service in a worker thread, and the visual debugger.
4
+ import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { createRequire } from "node:module";
9
6
  import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
10
8
  import process from "node:process";
11
- import { randomUUID } from "node:crypto";
12
- import { decodeMessage, encodeMessage, makeHeader, PROTOCOL_VERSION, verify } from "./wire.js";
13
- import { listening, PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
14
- import { Interrupted, Target } from "./target.js";
9
+ import { Target } from "./target.js";
15
10
  import { BunTarget } from "./bun-target.js";
16
11
  import { Debugger } from "./debugger.js";
17
12
  import { WorkerdTarget } from "./workerd-target.js";
18
13
  import { launchCelld } from "./celld-target.js";
19
- import { CellSyntaxError, loadTypeStripper, objectLiteralCandidate, } from "./transform.js";
20
14
  import { loadDenoProject } from "./deno-config.js";
21
- import { checkIsComplete, codePointToUtf16Index, exprAtCursor, inspectTarget, utf16ToCodePointIndex } from "./text.js";
22
- import { queryHistory } from "./history.js";
23
15
  import { LanguageService } from "./language-service.js";
24
16
  import { findWranglerConfig } from "./workers-project.js";
25
- import { VERSION } from "./package.js";
17
+ import { cacheRoot, VERSION } from "./package.js";
26
18
  import { watchParent } from "./parent.js";
19
+ import { KernelCore } from "./kernel-core.js";
20
+ import { ZmqChannels } from "./zmq-channels.js";
21
+ /** The installed TypeScript's version, for kernel_info's language_info (#340). */
22
+ function typescriptVersion() {
23
+ try {
24
+ const { version } = createRequire(import.meta.url)("typescript/package.json");
25
+ return typeof version === "string" ? version : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ /** Larger files get no source lines in a traceback. */
32
+ const MAX_SOURCE_BYTES = 1 << 20;
33
+ /** A traceback frame's file, read from disk when it is a local one (#322). */
34
+ function readSource(file) {
35
+ try {
36
+ const path = file.startsWith("file:") ? fileURLToPath(file) : file;
37
+ if (!path.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(path))
38
+ return undefined;
39
+ if (statSync(path).size > MAX_SOURCE_BYTES)
40
+ return undefined;
41
+ return readFileSync(path, "utf8");
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /** Where the target loads the kernel's runtime/*.mjs from: in place, or a cached copy (src/package.ts). */
48
+ function kernelFiles() {
49
+ const runtime = new URL("../runtime/", import.meta.url);
50
+ const files = [runtime.href, join(cacheRoot(), "runtime")];
51
+ if (runtime.protocol === "file:")
52
+ files.push(fileURLToPath(runtime));
53
+ return files;
54
+ }
27
55
  export function parseConnectionInfo(json) {
28
56
  const raw = JSON.parse(json);
29
57
  if (typeof raw !== "object" || raw === null)
@@ -55,21 +83,9 @@ export function parseConnectionInfo(json) {
55
83
  hb_port: info.hb_port,
56
84
  };
57
85
  }
58
- /**
59
- * How long interrupt_request may wait on the target before it's answered
60
- * anyway (#121). Under the 5 s clients allow control requests (#253).
61
- */
62
- const INTERRUPT_REPLY_WAIT_MS = 1_000;
63
- const NO_INPUT = { value: null, interrupted: false };
64
- /** Shell requests other than execute_request, and their reply types. */
65
- const SHELL_REPLIES = {
66
- kernel_info_request: "kernel_info_reply",
67
- complete_request: "complete_reply",
68
- is_complete_request: "is_complete_reply",
69
- inspect_request: "inspect_reply",
70
- history_request: "history_reply",
71
- comm_info_request: "comm_info_reply",
72
- };
86
+ function log(text) {
87
+ process.stderr.write(`[cdp-kernel] ${text}\n`);
88
+ }
73
89
  /** The target Node's major version, for @types/node: the host's, which is usually the same binary. */
74
90
  function nodeMajor() {
75
91
  return process.versions.node?.split(".")[0] ?? "*";
@@ -100,715 +116,80 @@ function languageServiceOptions(runtime, notebookDir, project) {
100
116
  }
101
117
  }
102
118
  }
103
- /** Runtime inspection text without its Type and Signature lines, which the declared type replaces. */
104
- function withoutSignature(text) {
105
- return text.split("\n").filter((line) => !/^(?:Type|Signature): /.test(line)).join("\n");
106
- }
107
- /** A TypeScript completion kind as a Jupyter completion type (JupyterLab picks its icon by these). */
108
- function jupyterType(kind) {
109
- switch (kind) {
110
- case undefined:
111
- return "<unknown>";
112
- case "var":
113
- case "let":
114
- case "const":
115
- case "local var":
116
- case "parameter":
117
- return "variable";
118
- case "local function":
119
- return "function";
120
- case "type":
121
- case "alias":
122
- return "type";
123
- default:
124
- return kind;
125
- }
126
- }
119
+ /**
120
+ * A kernel on its connection file's ports, running cells on a target
121
+ * runtime launched as a child process. The message handling is
122
+ * KernelCore's (src/kernel-core.ts); this is its Node wiring.
123
+ */
127
124
  export class Kernel {
128
- #conn;
129
- #runtime;
130
- #denoCompat;
131
- #typeStrip;
132
- // A Deno target in a Deno project uses its config (#13).
133
- #project;
134
- #projectWarning = null;
135
- #noticeShownFor = null;
136
- /** JupyterLab's visual debugger, for targets driven over CDP (#18). */
137
- #debugger;
138
- /** Typed completion from TypeScript's language service (#273). */
139
- #types;
140
- /** Cells run with store_history, for history_request (#10). */
141
- #history = [];
142
- #session = randomUUID();
143
- #shell;
144
- #control;
145
- #stdin;
146
- #iopub;
147
- /** Settles once all five ports are bound (#257). */
148
- #bound;
149
- #target = null;
150
- #targetStarting = null;
151
- #executionCount = 0;
152
- #parent = {};
153
- #allowStdin = false;
154
- #executing = false;
155
- /**
156
- * stop_on_error (#123): how many of the shell messages queued when a cell
157
- * failed are still to come, and whether the one being handled is one.
158
- */
159
- #abortQueued = 0;
160
- #aborting = false;
161
- /** The input_request waiting for its input_reply, if any. */
162
- #input = null;
163
- /** An interrupt is waiting on the target. */
164
- #interrupting = false;
165
- /** The shell message being handled, and since when. */
166
- #handling = null;
167
- // Open comms, for comm_info_request: comm_id -> target_name (#9).
168
- #comms = new Map();
169
- #shuttingDown = false;
125
+ #core;
170
126
  constructor(conn, runtime, options = {}) {
171
- this.#conn = conn;
172
- this.#runtime = runtime;
173
- this.#denoCompat = options.denoCompat ?? false;
174
- this.#typeStrip = options.typeStrip ?? "esbuild";
175
127
  const notebookDir = options.notebookDir ?? process.cwd();
176
- this.#project = runtime === "deno" ? this.#loadProject(notebookDir) : null;
177
- this.#types = options.languageService === false
178
- ? null
179
- : new LanguageService(languageServiceOptions(runtime, notebookDir, this.#project), { log: (text) => this.#log(text) });
180
- this.#debugger = runtime === "node" || runtime === "deno"
181
- ? new Debugger((event) => this.#publish("debug_event", event), () => this.#transformOptions())
182
- : null;
183
- const heartbeat = startHeartbeat(conn.hb_port, conn.ip);
184
- this.#shell = new RouterSocket(conn.shell_port, conn.ip);
185
- this.#control = new RouterSocket(conn.control_port, conn.ip);
186
- this.#stdin = new RouterSocket(conn.stdin_port, conn.ip);
187
- this.#iopub = new PubSocket(conn.iopub_port, conn.ip);
188
- this.#bound = Promise.all([heartbeat, this.#shell.server, this.#control.server, this.#stdin.server, this.#iopub.server].map(listening));
128
+ let warning = null;
129
+ let project = null;
130
+ // A Deno target in a Deno project uses its config (#13).
131
+ if (runtime === "deno") {
132
+ try {
133
+ project = loadDenoProject(notebookDir);
134
+ }
135
+ catch (err) {
136
+ // A broken deno.json mustn't stop the kernel: run as in a plain
137
+ // directory and say why in the first cell.
138
+ const reason = err instanceof Error ? err.message : String(err);
139
+ warning = `${reason}; running without the project config`;
140
+ log(warning);
141
+ }
142
+ }
143
+ const denoCompat = options.denoCompat ?? false;
144
+ this.#core = new KernelCore({
145
+ runtime,
146
+ version: VERSION,
147
+ languageVersion: typescriptVersion(),
148
+ channels: new ZmqChannels(conn, log),
149
+ launchTarget(events) {
150
+ if (runtime === "bun")
151
+ return BunTarget.launch(events);
152
+ if (runtime === "workerd")
153
+ return WorkerdTarget.launch(events);
154
+ if (runtime === "celld")
155
+ return launchCelld(events);
156
+ return Target.launch(runtime, events, { denoCompat, denoConfig: project?.configPath });
157
+ },
158
+ typeStrip: options.typeStrip,
159
+ transform: project?.transform,
160
+ warning,
161
+ types: options.languageService === false
162
+ ? null
163
+ : new LanguageService(languageServiceOptions(runtime, notebookDir, project), { log }),
164
+ createDebugger: runtime === "node" || runtime === "deno"
165
+ ? (publish, transformOptions) => new Debugger(publish, transformOptions)
166
+ : undefined,
167
+ readSource,
168
+ kernelFiles: kernelFiles(),
169
+ log,
170
+ exit: () => process.exit(0),
171
+ });
189
172
  }
190
173
  async run() {
191
- // Start the runtime eagerly so the first cell doesn't pay for it.
192
- this.#ensureTarget().catch((err) => this.#log(`target failed to start: ${err}`));
193
- process.on("SIGINT", () => void this.#interrupt());
174
+ const core = this.#core;
175
+ process.on("SIGINT", () => void core.interrupt());
194
176
  // Exiting runs the exit hook that kills the runtime (src/children.ts);
195
177
  // these signals' default action would skip it (#218).
196
178
  for (const [signal, code] of [["SIGTERM", 143], ["SIGHUP", 129]]) {
197
179
  process.on(signal, () => {
198
- this.#shuttingDown = true;
180
+ core.stopping();
199
181
  process.exit(code);
200
182
  });
201
183
  }
202
184
  // A server killed before it could shut the kernel down (#254).
203
185
  watchParent(() => {
204
- this.#log(`parent ${process.env.JPY_PARENT_PID} exited; shutting down`);
205
- this.#shuttingDown = true;
186
+ log(`parent ${process.env.JPY_PARENT_PID} exited; shutting down`);
187
+ core.stopping();
206
188
  process.exit(1);
207
189
  });
208
190
  // A state dump on stderr, for a kernel that has stopped answering (#237).
209
191
  if (process.platform !== "win32")
210
- process.on("SIGUSR2", () => void this.#logState());
211
- // Transforms are synchronous, so the stripper loads before any cell.
212
- if (typeof this.#typeStrip === "string")
213
- await loadTypeStripper(this.#typeStrip);
214
- await Promise.all([this.#loop(this.#shell, (m, p) => this.#handleShell(m, p)),
215
- this.#loop(this.#control, (m, p) => this.#handleControl(m, p)),
216
- this.#loop(this.#stdin, (m) => this.#handleStdin(m))]);
217
- }
218
- #log(text) {
219
- process.stderr.write(`[cdp-kernel] ${text}\n`);
220
- }
221
- async #logState() {
222
- const handling = this.#handling;
223
- const target = this.#target;
224
- let targetState = target === null ? (this.#targetStarting === null ? null : "starting") : target.runtime;
225
- if (target?.diagnose)
226
- targetState = await target.diagnose().catch((err) => `diagnose failed: ${err}`);
227
- this.#log(`state: ${JSON.stringify({
228
- shell: handling && { msgType: handling.msgType, msgId: handling.msgId, ms: Date.now() - handling.since },
229
- queued: this.#shell.incoming.size,
230
- executing: this.#executing,
231
- interrupting: this.#interrupting,
232
- awaitingInput: this.#input !== null,
233
- target: targetState,
234
- })}`);
235
- }
236
- #loadProject(notebookDir) {
237
- try {
238
- return loadDenoProject(notebookDir);
239
- }
240
- catch (err) {
241
- // A broken deno.json mustn't stop the kernel: run as in a plain
242
- // directory and say why in the first cell.
243
- const reason = err instanceof Error ? err.message : String(err);
244
- this.#projectWarning = `${reason}; running without the project config`;
245
- this.#log(this.#projectWarning);
246
- return null;
247
- }
248
- }
249
- #transformOptions() {
250
- return { ...this.#project?.transform, typeStrip: this.#typeStrip };
251
- }
252
- // --- comms (#9) ------------------------------------------------------------
253
- #trackComm(msgType, content) {
254
- const commId = typeof content.comm_id === "string" ? content.comm_id : null;
255
- if (commId === null)
256
- return;
257
- if (msgType === "comm_open" && typeof content.target_name === "string")
258
- this.#comms.set(commId, content.target_name);
259
- else if (msgType === "comm_close")
260
- this.#comms.delete(commId);
261
- }
262
- #commInfo(content) {
263
- const filter = typeof content.target_name === "string" ? content.target_name : null;
264
- const comms = {};
265
- for (const [commId, targetName] of this.#comms) {
266
- if (filter === null || filter === targetName)
267
- comms[commId] = { target_name: targetName };
268
- }
269
- return comms;
270
- }
271
- /** A frontend comm message. The busy/idle status around it is the caller's. */
272
- async #handleComm(msg) {
273
- const type = msg.header.msg_type;
274
- const commId = String(msg.content.comm_id ?? "");
275
- const target = await this.#ensureTarget();
276
- // Kernel messages the handlers send are replies to this message.
277
- this.#parent = msg.header;
278
- this.#allowStdin = false;
279
- this.#executing = true;
280
- let handled = false;
281
- try {
282
- const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
283
- if ("error" in result) {
284
- const { name, message, stack } = result.error;
285
- this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` });
286
- }
287
- else {
288
- handled = result.handled;
289
- }
290
- }
291
- catch (err) {
292
- if (!(err instanceof Interrupted))
293
- throw err;
294
- this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` });
295
- }
296
- finally {
297
- this.#executing = false;
298
- }
299
- await target.flush().catch(() => { });
300
- if (type === "comm_open") {
301
- if (handled) {
302
- this.#comms.set(commId, String(msg.content.target_name ?? ""));
303
- }
304
- else {
305
- // An unknown target, or a handler that failed: the protocol asks the
306
- // kernel to close the comm so the frontend doesn't wait on it.
307
- this.#publish("comm_close", { comm_id: commId, data: {} });
308
- }
309
- }
310
- else if (type === "comm_close") {
311
- this.#comms.delete(commId);
312
- }
313
- }
314
- // --- target lifecycle -----------------------------------------------------
315
- #launchTarget() {
316
- if (this.#runtime === "bun")
317
- return BunTarget.launch(this.#targetEvents());
318
- if (this.#runtime === "workerd")
319
- return WorkerdTarget.launch(this.#targetEvents());
320
- if (this.#runtime === "celld")
321
- return launchCelld(this.#targetEvents());
322
- return Target.launch(this.#runtime, this.#targetEvents(), {
323
- denoCompat: this.#denoCompat,
324
- denoConfig: this.#project?.configPath,
325
- });
326
- }
327
- #ensureTarget() {
328
- if (this.#target)
329
- return Promise.resolve(this.#target);
330
- // Not before the kernel's own ports are bound: a Deno runtime binds its
331
- // inspector to port 0 within milliseconds of starting and can take a
332
- // connection-file port the kernel is still binding (#257).
333
- this.#targetStarting ??= this.#bound.then(() => this.#launchTarget()).then(async (target) => {
334
- // Before any cell runs on it, so breakpoints already apply.
335
- await this.#debugger?.attachTarget(target.debug ?? null);
336
- this.#target = target;
337
- this.#targetStarting = null;
338
- return target;
339
- }, (err) => {
340
- this.#targetStarting = null;
341
- throw err;
342
- });
343
- return this.#targetStarting;
344
- }
345
- #targetEvents() {
346
- return {
347
- stream: (name, text) => this.#publish("stream", { name, text }),
348
- message: (msg) => void this.#onTargetMessage(msg).catch((err) => this.#log(`error handling target message: ${err}`)),
349
- exit: (code, note) => {
350
- this.#target = null;
351
- // What its cells declared died with it.
352
- this.#types?.reset();
353
- // Its prompt, if any, died with it; a late reply is dropped.
354
- this.#input = null;
355
- void this.#debugger?.attachTarget(null);
356
- if (!this.#shuttingDown) {
357
- const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
358
- this.#publish("stream", { name: "stderr", text: `\n${text}\n` });
359
- }
360
- },
361
- };
362
- }
363
- async #onTargetMessage(msg) {
364
- if (msg.kind === "iopub") {
365
- this.#trackComm(msg.msg_type, msg.content);
366
- this.#publish(msg.msg_type, msg.content, {
367
- metadata: msg.metadata,
368
- buffers: msg.buffers.map((b) => new Uint8Array(Buffer.from(b, "base64"))),
369
- });
370
- return;
371
- }
372
- // The answer goes to the target that asked, not one started since.
373
- const target = this.#target;
374
- let answer = NO_INPUT;
375
- try {
376
- answer = await this.#requestInput(msg.prompt, msg.password);
377
- }
378
- finally {
379
- // Always answer exactly once so the target's blocking read returns.
380
- if (target !== null && target === this.#target)
381
- target.replyInput(answer.value, answer.interrupted);
382
- }
383
- }
384
- async #interrupt() {
385
- // A target blocked reading its prompt's answer can't run
386
- // terminateExecution, and would never answer it. Make the prompt throw
387
- // instead, which ends the cell (#121).
388
- const input = this.#input;
389
- if (input !== null) {
390
- this.#input = null;
391
- input.settle({ value: null, interrupted: true });
392
- return;
393
- }
394
- if (!this.#target || !this.#executing)
395
- return;
396
- this.#interrupting = true;
397
- try {
398
- await this.#target.interrupt();
399
- }
400
- finally {
401
- this.#interrupting = false;
402
- }
403
- }
404
- // --- messaging --------------------------------------------------------------
405
- #encode(msgType, content, parentHeader, extra = {}) {
406
- return encodeMessage(this.#session, this.#conn.key, msgType, content, { parentHeader, ...extra });
407
- }
408
- #publish(msgType, content, extra = {}) {
409
- this.#iopub.send(this.#encode(msgType, content, extra.parent ?? this.#parent, extra));
410
- }
411
- #status(state, parent) {
412
- this.#publish("status", { execution_state: state }, { parent });
413
- }
414
- #reply(socket, peer, request, msgType, content) {
415
- socket.send(peer, this.#encode(msgType, content, request.header));
416
- }
417
- async #loop(socket, handle) {
418
- while (!this.#shuttingDown) {
419
- const { peer, frames } = await socket.recv();
420
- if (socket === this.#shell) {
421
- this.#aborting = this.#abortQueued > 0;
422
- if (this.#aborting)
423
- this.#abortQueued--;
424
- }
425
- try {
426
- const msg = decodeMessage(frames);
427
- // Unsigned or forged messages are dropped, as in Deno.
428
- if (!verify(this.#conn.key, msg.signedParts, msg.sig))
429
- continue;
430
- if (socket === this.#shell)
431
- this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
432
- await handle(msg, peer);
433
- }
434
- catch (err) {
435
- this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
436
- }
437
- finally {
438
- if (socket === this.#shell)
439
- this.#handling = null;
440
- }
441
- }
442
- }
443
- #kernelInfo() {
444
- const versions = this.#target?.versions;
445
- return {
446
- status: "ok",
447
- protocol_version: PROTOCOL_VERSION,
448
- implementation: "cdp-kernel",
449
- implementation_version: VERSION,
450
- language_info: {
451
- name: "typescript",
452
- version: "5",
453
- mimetype: "text/x.typescript",
454
- file_extension: ".ts",
455
- pygments_lexer: "typescript",
456
- codemirror_mode: "typescript",
457
- nbconvert_exporter: "script",
458
- },
459
- // The runtime's versions are known once it is up.
460
- banner: versions ? `cdp-kernel on ${this.#runtime} ${versions.runtime} (${versions.engine || "?"})` : `cdp-kernel on ${this.#runtime}`,
461
- help_links: [],
462
- debugger: this.#debugger !== null,
463
- // What current ipykernel advertises; JupyterLab itself goes by the
464
- // kernelspec's metadata.debugger.
465
- supported_features: this.#debugger !== null ? ["debugger"] : [],
466
- };
467
- }
468
- // --- control channel ------------------------------------------------------
469
- async #handleControl(msg, peer) {
470
- // Every request is wrapped in busy/idle, as on shell (#124). JupyterLab
471
- // treats a debug request as done only once the kernel reports idle for
472
- // it. (Jupyter Server leaves debug_request out of the kernel's status.)
473
- this.#status("busy", msg.header);
474
- try {
475
- await this.#controlRequest(this.#control, msg, peer);
476
- }
477
- finally {
478
- this.#status("idle", msg.header);
479
- }
480
- }
481
- /** Control requests; shutdown and interrupt may also arrive on shell (#124). */
482
- async #controlRequest(socket, msg, peer) {
483
- switch (msg.header.msg_type) {
484
- case "kernel_info_request":
485
- this.#reply(socket, peer, msg, "kernel_info_reply", this.#kernelInfo());
486
- break;
487
- case "interrupt_request":
488
- // Answer even if the target doesn't (#121); the interrupt goes on.
489
- await Promise.race([
490
- this.#interrupt().catch((err) => this.#log(`interrupt failed: ${err}`)),
491
- new Promise((r) => setTimeout(r, INTERRUPT_REPLY_WAIT_MS)),
492
- ]);
493
- this.#reply(socket, peer, msg, "interrupt_reply", { status: "ok" });
494
- break;
495
- case "debug_request": {
496
- let reply;
497
- if (this.#debugger === null) {
498
- reply = { type: "response", request_seq: msg.content.seq, command: msg.content.command, success: false, message: `${this.#runtime} targets have no debugger`, body: {} };
499
- }
500
- else {
501
- // Everything but debugInfo talks to the target, so make sure there is one.
502
- if (msg.content.command !== "debugInfo")
503
- await this.#ensureTarget().catch(() => null);
504
- reply = await this.#debugger.handle(msg.content);
505
- }
506
- this.#reply(socket, peer, msg, "debug_reply", reply);
507
- break;
508
- }
509
- case "shutdown_request": {
510
- const restart = msg.content.restart === true;
511
- this.#reply(socket, peer, msg, "shutdown_reply", { status: "ok", restart });
512
- this.#shuttingDown = true;
513
- this.#target?.kill();
514
- // Frontends spawn a fresh kernel even on restart; exit after the
515
- // reply has had a moment to flush (denoland/deno#20556).
516
- setTimeout(() => process.exit(0), 100);
517
- break;
518
- }
519
- }
520
- }
521
- // --- shell channel ----------------------------------------------------------
522
- async #handleShell(msg, peer) {
523
- const type = msg.header.msg_type;
524
- if (type === "execute_request")
525
- return this.#execute(msg, peer);
526
- this.#status("busy", msg.header);
527
- try {
528
- switch (type) {
529
- case "kernel_info_request":
530
- // Without waiting for the runtime: clients probe with kernel_info
531
- // while it starts, and re-send until answered (#253).
532
- this.#reply(this.#shell, peer, msg, "kernel_info_reply", this.#kernelInfo());
533
- break;
534
- case "complete_request":
535
- this.#reply(this.#shell, peer, msg, "complete_reply", await this.#complete(msg));
536
- break;
537
- case "is_complete_request":
538
- this.#reply(this.#shell, peer, msg, "is_complete_reply", checkIsComplete(String(msg.content.code ?? "")));
539
- break;
540
- case "inspect_request":
541
- this.#reply(this.#shell, peer, msg, "inspect_reply", await this.#inspect(msg));
542
- break;
543
- case "history_request":
544
- this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: queryHistory(this.#history, msg.content) });
545
- break;
546
- case "comm_info_request":
547
- this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: this.#commInfo(msg.content) });
548
- break;
549
- case "comm_open":
550
- case "comm_msg":
551
- case "comm_close":
552
- await this.#handleComm(msg);
553
- break;
554
- case "shutdown_request":
555
- case "interrupt_request":
556
- // Deprecated on shell, but still allowed, and ipykernel takes them.
557
- await this.#controlRequest(this.#shell, msg, peer);
558
- break;
559
- default:
560
- this.#log(`ignoring unknown shell message ${type}`);
561
- }
562
- }
563
- catch (err) {
564
- // E.g. the runtime failed to launch. Answer anyway, so the frontend
565
- // doesn't wait on the request forever (#125).
566
- const replyType = SHELL_REPLIES[type];
567
- if (replyType !== undefined) {
568
- const name = err instanceof Error ? err.name : "Error";
569
- const message = err instanceof Error ? err.message : String(err);
570
- const error = { ename: name, evalue: message, traceback: [`${name}: ${message}`] };
571
- this.#reply(this.#shell, peer, msg, replyType, { status: "error", ...error });
572
- }
573
- throw err;
574
- }
575
- finally {
576
- this.#status("idle", msg.header);
577
- }
578
- }
579
- /**
580
- * The tooltip (Shift-Tab) for the name at the cursor (#10): its declared
581
- * type, signature and JSDoc when the language service knows them (#274),
582
- * then what the runtime finds.
583
- */
584
- async #inspect(msg) {
585
- const code = String(msg.content.code ?? "");
586
- const cursorCp = typeof msg.content.cursor_pos === "number"
587
- ? msg.content.cursor_pos
588
- : utf16ToCodePointIndex(code, code.length);
589
- const cursor = codePointToUtf16Index(code, cursorCp);
590
- const path = inspectTarget(code, cursor);
591
- const detail = msg.content.detail_level === 1 ? 1 : 0;
592
- const [runtime, typed] = await Promise.all([
593
- path === null ? null : (async () => (await this.#ensureTarget()).inspect(path, detail))(),
594
- this.#types?.quickInfo(code, cursor) ?? null,
595
- ]);
596
- const text = typed === null ? runtime : runtime === null ? typed : `${typed}\n\n${withoutSignature(runtime)}`;
597
- return text === null
598
- ? { status: "ok", found: false, data: {}, metadata: {} }
599
- : { status: "ok", found: true, data: { "text/plain": text }, metadata: {} };
600
- }
601
- async #complete(msg) {
602
- const code = String(msg.content.code ?? "");
603
- const cursorCp = typeof msg.content.cursor_pos === "number"
604
- ? msg.content.cursor_pos
605
- : utf16ToCodePointIndex(code, code.length);
606
- const cursor = codePointToUtf16Index(code, cursorCp);
607
- // Both at once: the language service may not answer within its budget.
608
- const [runtime, typed] = await Promise.all([
609
- this.#runtimeCompletions(code, cursor),
610
- this.#types?.complete(code, cursor) ?? null,
611
- ]);
612
- // Declared types first, then what only the runtime knows (a property
613
- // added at runtime, a global defined by a library).
614
- let matches = runtime.matches;
615
- let start = runtime.start;
616
- const kinds = new Map();
617
- if (typed !== null && typed.start === runtime.start) {
618
- for (const entry of typed.entries)
619
- kinds.set(entry.name, entry.kind);
620
- matches = [...kinds.keys(), ...runtime.matches.filter((name) => !kinds.has(name))];
621
- }
622
- else if (typed !== null && typed.entries.length > 0 && runtime.matches.length === 0) {
623
- // An expression the runtime can't complete, e.g. inside a call.
624
- for (const entry of typed.entries)
625
- kinds.set(entry.name, entry.kind);
626
- matches = [...kinds.keys()];
627
- start = typed.start;
628
- }
629
- const cursorStart = utf16ToCodePointIndex(code, start);
630
- return {
631
- status: "ok",
632
- matches,
633
- cursor_start: cursorStart,
634
- cursor_end: cursorCp,
635
- metadata: kinds.size === 0 ? {} : {
636
- // JupyterLab shows each match's type as an icon.
637
- _jupyter_types_experimental: matches.map((text) => ({
638
- start: cursorStart,
639
- end: cursorCp,
640
- text,
641
- type: jupyterType(kinds.get(text)),
642
- })),
643
- },
644
- };
645
- }
646
- /** Completion from the runtime's values: property names, globals, lexical names. */
647
- async #runtimeCompletions(code, cursor) {
648
- const expr = exprAtCursor(code, cursor);
649
- const target = await this.#ensureTarget();
650
- const dot = expr.lastIndexOf(".");
651
- if (dot !== -1) {
652
- const prefix = expr.slice(dot + 1);
653
- const matches = (await target.propertyNames(expr.slice(0, dot)))
654
- .filter((name) => name.startsWith(prefix) && !name.startsWith("Symbol("));
655
- return { matches, start: cursor - prefix.length };
656
- }
657
- const names = new Set([...(await target.propertyNames("globalThis")), ...(await target.lexicalNames())]);
658
- const matches = [...names].filter((name) => name.startsWith(expr) && !name.startsWith("__cdpk")).sort();
659
- return { matches, start: cursor - expr.length };
660
- }
661
- async #requestInput(prompt, password) {
662
- // The target asked while an interrupt was already waiting on it.
663
- if (this.#interrupting)
664
- return { value: null, interrupted: true };
665
- const parent = this.#parent;
666
- if (!this.#allowStdin || typeof parent.msg_id !== "string")
667
- return NO_INPUT;
668
- if (this.#stdin.peers.size === 0)
669
- await new Promise((r) => setTimeout(r, 100));
670
- if (this.#stdin.peers.size === 0)
671
- return NO_INPUT;
672
- const header = makeHeader(this.#session, "input_request");
673
- const answer = new Promise((settle) => (this.#input = { msgId: header.msg_id, settle }));
674
- this.#stdin.sendAll(encodeMessage(this.#session, this.#conn.key, "input_request", { prompt, password }, {
675
- header,
676
- parentHeader: parent,
677
- }));
678
- return answer;
679
- }
680
- /** stdin carries only input_replys. */
681
- async #handleStdin(reply) {
682
- if (reply.header.msg_type !== "input_reply")
683
- return;
684
- const input = this.#input;
685
- // A reply with nothing waiting is late (its prompt was interrupted), and
686
- // one naming another request is for an earlier prompt: drop both rather
687
- // than answer the next prompt with them (#121). Some clients
688
- // (jupyter_client's input()) send no parent header, so only a present
689
- // one is checked.
690
- if (input === null)
691
- return;
692
- const parentId = reply.parentHeader.msg_id;
693
- if (typeof parentId === "string" && parentId !== "" && parentId !== input.msgId)
694
- return;
695
- this.#input = null;
696
- input.settle({ value: typeof reply.content.value === "string" ? reply.content.value : null, interrupted: false });
697
- }
698
- async #evaluateCell(target, code) {
699
- // Object-literal wrapping (Deno's evaluate_line_with_object_wrapping):
700
- // try `({ ... })` first and fall back to the code as written.
701
- // While debugging, cells carry the path JupyterLab gave them and keep a
702
- // line map, so breakpoints bind and frames point at the cell (#18).
703
- const debugging = this.#debugger?.started === true && target.debug !== undefined;
704
- const transform = (source) => debugging && this.#debugger ? this.#debugger.cellScript(code, source) : target.transform(source, this.#transformOptions());
705
- const wrapped = objectLiteralCandidate(code);
706
- if (wrapped !== null) {
707
- try {
708
- const result = await target.evaluate(transform(wrapped));
709
- if (!result.exceptionDetails)
710
- return result;
711
- }
712
- catch (err) {
713
- if (!(err instanceof CellSyntaxError))
714
- throw err;
715
- }
716
- }
717
- return target.evaluate(transform(code));
718
- }
719
- async #execute(msg, peer) {
720
- const parent = msg.header;
721
- if (this.#aborting) {
722
- // Queued behind a failed cell: not run, not counted (#123).
723
- this.#status("busy", parent);
724
- this.#reply(this.#shell, peer, msg, "execute_reply", { status: "aborted" });
725
- this.#status("idle", parent);
726
- return;
727
- }
728
- const code = String(msg.content.code ?? "");
729
- const silent = msg.content.silent === true;
730
- if (!silent && msg.content.store_history !== false) {
731
- this.#executionCount++;
732
- this.#history.push({ line: this.#executionCount, input: code });
733
- }
734
- const count = this.#executionCount;
735
- this.#parent = parent;
736
- this.#allowStdin = msg.content.allow_stdin === true;
737
- this.#status("busy", parent);
738
- // Silent requests broadcast nothing on iopub but status (#124).
739
- if (!silent)
740
- this.#publish("execute_input", { code, execution_count: count });
741
- if (this.#projectWarning !== null) {
742
- // Shown once, in the first cell, where the user will see it.
743
- this.#publish("stream", { name: "stderr", text: `[cdp-kernel] ${this.#projectWarning}\n` });
744
- this.#projectWarning = null;
745
- }
746
- let error = null;
747
- this.#executing = true;
748
- try {
749
- const target = await this.#ensureTarget();
750
- if (target.notice !== undefined && this.#noticeShownFor !== target) {
751
- // Once per target, in the first cell it runs.
752
- this.#noticeShownFor = target;
753
- this.#publish("stream", { name: "stderr", text: `${target.notice}\n` });
754
- }
755
- const result = await this.#evaluateCell(target, code);
756
- // It ran, so its declarations exist, even if it threw part way.
757
- this.#types?.addCell(code);
758
- if (result.exceptionDetails) {
759
- const exception = result.exceptionDetails.exception;
760
- const described = exception
761
- ? await target.describeError(exception)
762
- : { name: "Error", message: result.exceptionDetails.text, stack: "" };
763
- error = {
764
- ename: described.name || "Error",
765
- evalue: described.message,
766
- traceback: (described.stack || `${described.name}: ${described.message}`).split("\n"),
767
- };
768
- }
769
- else if (!silent && result.result.type !== "undefined") {
770
- await target.broadcastResult(count, result.result);
771
- }
772
- await target.flush();
773
- }
774
- catch (err) {
775
- if (err instanceof CellSyntaxError) {
776
- // Deno reports these as an opaque "Execution failed".
777
- error = { ename: "SyntaxError", evalue: err.message, traceback: [`SyntaxError: ${err.message}`] };
778
- }
779
- else if (err instanceof Interrupted) {
780
- error = { ename: "Interrupted", evalue: "Execution interrupted", traceback: ["Execution interrupted"] };
781
- await this.#target?.flush().catch(() => { });
782
- }
783
- else {
784
- const message = err instanceof Error ? err.message : String(err);
785
- error = { ename: "KernelError", evalue: message, traceback: [message] };
786
- }
787
- }
788
- finally {
789
- this.#executing = false;
790
- }
791
- // The result and error are shown: nothing refers to them any more (#286).
792
- await this.#target?.releaseResults?.();
793
- if (error) {
794
- if (!silent && msg.content.stop_on_error !== false) {
795
- // Abort what was queued behind this cell (JupyterLab's Run All sends
796
- // every cell at once), but nothing sent after the client sees the
797
- // error. A macrotask first lets bytes already received be parsed.
798
- await new Promise((r) => setTimeout(r, 0));
799
- this.#abortQueued = this.#shell.incoming.size;
800
- }
801
- this.#publish("error", error);
802
- this.#reply(this.#shell, peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
803
- }
804
- else {
805
- this.#reply(this.#shell, peer, msg, "execute_reply", {
806
- status: "ok",
807
- execution_count: count,
808
- payload: [],
809
- user_expressions: {},
810
- });
811
- }
812
- this.#status("idle", parent);
192
+ process.on("SIGUSR2", () => void core.logState());
193
+ await core.run();
813
194
  }
814
195
  }