@tlockney/cdp-kernel 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +27 -6
  2. package/dist/browser/channels.d.ts +27 -0
  3. package/dist/browser/channels.js +90 -0
  4. package/dist/browser/kernel.js +29674 -0
  5. package/dist/browser/kernel.js.map +7 -0
  6. package/dist/browser/mod.d.ts +20 -0
  7. package/dist/browser/mod.js +38 -0
  8. package/dist/browser/target.d.ts +71 -0
  9. package/dist/browser/target.js +206 -0
  10. package/dist/browser/worker.js +1000 -0
  11. package/dist/browser/worker.js.map +7 -0
  12. package/dist/cdp.d.ts +11 -0
  13. package/dist/cdp.js +6 -1
  14. package/dist/celld-target.js +9 -12
  15. package/dist/children.d.ts +25 -3
  16. package/dist/children.js +84 -2
  17. package/dist/debugger.d.ts +2 -0
  18. package/dist/debugger.js +12 -6
  19. package/dist/deno-types.d.ts +30 -0
  20. package/dist/deno-types.js +93 -0
  21. package/dist/kernel-core.d.ts +69 -0
  22. package/dist/kernel-core.js +754 -0
  23. package/dist/kernel.d.ts +12 -1
  24. package/dist/kernel.js +132 -605
  25. package/dist/language-service-worker.d.ts +1 -0
  26. package/dist/language-service-worker.js +88 -0
  27. package/dist/language-service.d.ts +64 -0
  28. package/dist/language-service.js +128 -0
  29. package/dist/main.js +102 -8
  30. package/dist/message.d.ts +34 -0
  31. package/dist/message.js +14 -0
  32. package/dist/mod.d.ts +1 -0
  33. package/dist/mod.js +1 -0
  34. package/dist/notebook-types.d.ts +62 -0
  35. package/dist/notebook-types.js +310 -0
  36. package/dist/package.d.ts +2 -2
  37. package/dist/package.js +2 -2
  38. package/dist/parent.d.ts +12 -0
  39. package/dist/parent.js +39 -0
  40. package/dist/port-holder.d.ts +13 -0
  41. package/dist/port-holder.js +105 -0
  42. package/dist/sourcemap.d.ts +7 -2
  43. package/dist/sourcemap.js +9 -3
  44. package/dist/strip-esbuild.d.ts +2 -0
  45. package/dist/strip-esbuild.js +44 -0
  46. package/dist/strip-sucrase.d.ts +2 -0
  47. package/dist/strip-sucrase.js +78 -0
  48. package/dist/target-api.d.ts +80 -0
  49. package/dist/target-api.js +22 -0
  50. package/dist/target.d.ts +26 -74
  51. package/dist/target.js +232 -58
  52. package/dist/traceback.d.ts +75 -0
  53. package/dist/traceback.js +200 -0
  54. package/dist/transform.d.ts +22 -1
  55. package/dist/transform.js +46 -43
  56. package/dist/wire.d.ts +4 -24
  57. package/dist/wire.js +3 -12
  58. package/dist/workerd-target.js +1 -1
  59. package/dist/workers-project.js +13 -2
  60. package/dist/zmq-channels.d.ts +14 -0
  61. package/dist/zmq-channels.js +59 -0
  62. package/dist/zmtp.d.ts +2 -0
  63. package/dist/zmtp.js +12 -0
  64. package/package.json +15 -5
  65. package/runtime/bootstrap.mjs +113 -33
  66. package/runtime/browser-worker.mjs +119 -0
  67. package/runtime/cdn.d.mts +2 -0
  68. package/runtime/cdn.mjs +17 -0
  69. package/runtime/display.mjs +202 -1
  70. package/runtime/format.d.mts +2 -0
  71. package/runtime/format.mjs +125 -0
  72. package/runtime/introspect.mjs +1 -1
  73. package/runtime/node-hooks.d.mts +6 -0
  74. package/runtime/node-hooks.mjs +11 -0
package/dist/kernel.js CHANGED
@@ -1,24 +1,46 @@
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";
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 { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
8
7
  import process from "node:process";
9
- import { randomUUID } from "node:crypto";
10
- import { decodeMessage, encodeMessage, makeHeader, PROTOCOL_VERSION, verify } from "./wire.js";
11
- import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
12
- import { Interrupted, Target } from "./target.js";
8
+ import { Target } from "./target.js";
13
9
  import { BunTarget } from "./bun-target.js";
14
10
  import { Debugger } from "./debugger.js";
15
11
  import { WorkerdTarget } from "./workerd-target.js";
16
12
  import { launchCelld } from "./celld-target.js";
17
- import { CellSyntaxError, objectLiteralCandidate } from "./transform.js";
18
13
  import { loadDenoProject } from "./deno-config.js";
19
- import { checkIsComplete, codePointToUtf16Index, exprAtCursor, inspectTarget, utf16ToCodePointIndex } from "./text.js";
20
- import { queryHistory } from "./history.js";
21
- import { VERSION } from "./package.js";
14
+ import { LanguageService } from "./language-service.js";
15
+ import { findWranglerConfig } from "./workers-project.js";
16
+ import { cacheRoot, VERSION } from "./package.js";
17
+ import { watchParent } from "./parent.js";
18
+ import { KernelCore } from "./kernel-core.js";
19
+ import { ZmqChannels } from "./zmq-channels.js";
20
+ /** Larger files get no source lines in a traceback. */
21
+ const MAX_SOURCE_BYTES = 1 << 20;
22
+ /** A traceback frame's file, read from disk when it is a local one (#322). */
23
+ function readSource(file) {
24
+ try {
25
+ const path = file.startsWith("file:") ? fileURLToPath(file) : file;
26
+ if (!path.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(path))
27
+ return undefined;
28
+ if (statSync(path).size > MAX_SOURCE_BYTES)
29
+ return undefined;
30
+ return readFileSync(path, "utf8");
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ /** Where the target loads the kernel's runtime/*.mjs from: in place, or a cached copy (src/package.ts). */
37
+ function kernelFiles() {
38
+ const runtime = new URL("../runtime/", import.meta.url);
39
+ const files = [runtime.href, join(cacheRoot(), "runtime")];
40
+ if (runtime.protocol === "file:")
41
+ files.push(fileURLToPath(runtime));
42
+ return files;
43
+ }
22
44
  export function parseConnectionInfo(json) {
23
45
  const raw = JSON.parse(json);
24
46
  if (typeof raw !== "object" || raw === null)
@@ -50,607 +72,112 @@ export function parseConnectionInfo(json) {
50
72
  hb_port: info.hb_port,
51
73
  };
52
74
  }
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
- };
75
+ function log(text) {
76
+ process.stderr.write(`[cdp-kernel] ${text}\n`);
77
+ }
78
+ /** The target Node's major version, for @types/node: the host's, which is usually the same binary. */
79
+ function nodeMajor() {
80
+ return process.versions.node?.split(".")[0] ?? "*";
81
+ }
82
+ /**
83
+ * Where the language service gets each target's globals and imports from
84
+ * (#273, #275). A `--deno-compat` Node target gets Node's types only: its
85
+ * `Deno` namespace is partial, and Deno's declarations would promise
86
+ * members it doesn't have.
87
+ */
88
+ function languageServiceOptions(runtime, notebookDir, project) {
89
+ const base = { notebookDir, npmCache: "npm", deno: null, denoTypes: false, typeFiles: [], typePackages: [] };
90
+ switch (runtime) {
91
+ case "node":
92
+ return { ...base, typePackages: [{ name: "@types/node", range: nodeMajor() }] };
93
+ case "deno":
94
+ return { ...base, npmCache: null, deno: { config: project?.configPath ?? null }, denoTypes: true };
95
+ case "bun":
96
+ return { ...base, npmCache: "bun", typePackages: [{ name: "@types/bun", range: "1" }] };
97
+ case "workerd":
98
+ case "celld": {
99
+ // A Workers project's own types (`wrangler types`) know its bindings.
100
+ const config = findWranglerConfig(notebookDir);
101
+ const generated = config === null ? null : join(dirname(config), "worker-configuration.d.ts");
102
+ return generated !== null && existsSync(generated)
103
+ ? { ...base, typeFiles: [generated] }
104
+ : { ...base, typePackages: [{ name: "@cloudflare/workers-types", range: "*" }] };
105
+ }
106
+ }
107
+ }
108
+ /**
109
+ * A kernel on its connection file's ports, running cells on a target
110
+ * runtime launched as a child process. The message handling is
111
+ * KernelCore's (src/kernel-core.ts); this is its Node wiring.
112
+ */
65
113
  export class Kernel {
66
- #conn;
67
- #runtime;
68
- #denoCompat;
69
- // A Deno target in a Deno project uses its config (#13).
70
- #project;
71
- #projectWarning = null;
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 = [];
77
- #session = randomUUID();
78
- #shell;
79
- #control;
80
- #stdin;
81
- #iopub;
82
- #target = null;
83
- #targetStarting = null;
84
- #executionCount = 0;
85
- #parent = {};
86
- #allowStdin = false;
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;
98
- // Open comms, for comm_info_request: comm_id -> target_name (#9).
99
- #comms = new Map();
100
- #shuttingDown = false;
114
+ #core;
101
115
  constructor(conn, runtime, options = {}) {
102
- this.#conn = conn;
103
- this.#runtime = runtime;
104
- this.#denoCompat = options.denoCompat ?? false;
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;
109
- startHeartbeat(conn.hb_port, conn.ip);
110
- this.#shell = new RouterSocket(conn.shell_port, conn.ip);
111
- this.#control = new RouterSocket(conn.control_port, conn.ip);
112
- this.#stdin = new RouterSocket(conn.stdin_port, conn.ip);
113
- this.#iopub = new PubSocket(conn.iopub_port, conn.ip);
116
+ const notebookDir = options.notebookDir ?? process.cwd();
117
+ let warning = null;
118
+ let project = null;
119
+ // A Deno target in a Deno project uses its config (#13).
120
+ if (runtime === "deno") {
121
+ try {
122
+ project = loadDenoProject(notebookDir);
123
+ }
124
+ catch (err) {
125
+ // A broken deno.json mustn't stop the kernel: run as in a plain
126
+ // directory and say why in the first cell.
127
+ const reason = err instanceof Error ? err.message : String(err);
128
+ warning = `${reason}; running without the project config`;
129
+ log(warning);
130
+ }
131
+ }
132
+ const denoCompat = options.denoCompat ?? false;
133
+ this.#core = new KernelCore({
134
+ runtime,
135
+ version: VERSION,
136
+ channels: new ZmqChannels(conn, log),
137
+ launchTarget(events) {
138
+ if (runtime === "bun")
139
+ return BunTarget.launch(events);
140
+ if (runtime === "workerd")
141
+ return WorkerdTarget.launch(events);
142
+ if (runtime === "celld")
143
+ return launchCelld(events);
144
+ return Target.launch(runtime, events, { denoCompat, denoConfig: project?.configPath });
145
+ },
146
+ typeStrip: options.typeStrip,
147
+ transform: project?.transform,
148
+ warning,
149
+ types: options.languageService === false
150
+ ? null
151
+ : new LanguageService(languageServiceOptions(runtime, notebookDir, project), { log }),
152
+ createDebugger: runtime === "node" || runtime === "deno"
153
+ ? (publish, transformOptions) => new Debugger(publish, transformOptions)
154
+ : undefined,
155
+ readSource,
156
+ kernelFiles: kernelFiles(),
157
+ log,
158
+ exit: () => process.exit(0),
159
+ });
114
160
  }
115
161
  async run() {
116
- // Start the runtime eagerly so the first cell doesn't pay for it.
117
- this.#ensureTarget().catch((err) => this.#log(`target failed to start: ${err}`));
118
- process.on("SIGINT", () => void this.#interrupt());
162
+ const core = this.#core;
163
+ process.on("SIGINT", () => void core.interrupt());
119
164
  // Exiting runs the exit hook that kills the runtime (src/children.ts);
120
165
  // these signals' default action would skip it (#218).
121
166
  for (const [signal, code] of [["SIGTERM", 143], ["SIGHUP", 129]]) {
122
167
  process.on(signal, () => {
123
- this.#shuttingDown = true;
168
+ core.stopping();
124
169
  process.exit(code);
125
170
  });
126
171
  }
127
- await Promise.all([this.#loop(this.#shell, (m, p) => this.#handleShell(m, p)),
128
- this.#loop(this.#control, (m, p) => this.#handleControl(m, p)),
129
- this.#loop(this.#stdin, (m) => this.#handleStdin(m))]);
130
- }
131
- #log(text) {
132
- process.stderr.write(`[cdp-kernel] ${text}\n`);
133
- }
134
- #loadProject(notebookDir) {
135
- try {
136
- return loadDenoProject(notebookDir);
137
- }
138
- catch (err) {
139
- // A broken deno.json mustn't stop the kernel: run as in a plain
140
- // directory and say why in the first cell.
141
- const reason = err instanceof Error ? err.message : String(err);
142
- this.#projectWarning = `${reason}; running without the project config`;
143
- this.#log(this.#projectWarning);
144
- return null;
145
- }
146
- }
147
- #transformOptions() {
148
- return this.#project?.transform ?? {};
149
- }
150
- // --- comms (#9) ------------------------------------------------------------
151
- #trackComm(msgType, content) {
152
- const commId = typeof content.comm_id === "string" ? content.comm_id : null;
153
- if (commId === null)
154
- return;
155
- if (msgType === "comm_open" && typeof content.target_name === "string")
156
- this.#comms.set(commId, content.target_name);
157
- else if (msgType === "comm_close")
158
- this.#comms.delete(commId);
159
- }
160
- #commInfo(content) {
161
- const filter = typeof content.target_name === "string" ? content.target_name : null;
162
- const comms = {};
163
- for (const [commId, targetName] of this.#comms) {
164
- if (filter === null || filter === targetName)
165
- comms[commId] = { target_name: targetName };
166
- }
167
- return comms;
168
- }
169
- /** A frontend comm message. The busy/idle status around it is the caller's. */
170
- async #handleComm(msg) {
171
- const type = msg.header.msg_type;
172
- const commId = String(msg.content.comm_id ?? "");
173
- const target = await this.#ensureTarget();
174
- // Kernel messages the handlers send are replies to this message.
175
- this.#parent = msg.header;
176
- this.#allowStdin = false;
177
- this.#executing = true;
178
- let handled = false;
179
- try {
180
- const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
181
- if ("error" in result) {
182
- const { name, message, stack } = result.error;
183
- this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` });
184
- }
185
- else {
186
- handled = result.handled;
187
- }
188
- }
189
- catch (err) {
190
- if (!(err instanceof Interrupted))
191
- throw err;
192
- this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` });
193
- }
194
- finally {
195
- this.#executing = false;
196
- }
197
- await target.flush().catch(() => { });
198
- if (type === "comm_open") {
199
- if (handled) {
200
- this.#comms.set(commId, String(msg.content.target_name ?? ""));
201
- }
202
- else {
203
- // An unknown target, or a handler that failed: the protocol asks the
204
- // kernel to close the comm so the frontend doesn't wait on it.
205
- this.#publish("comm_close", { comm_id: commId, data: {} });
206
- }
207
- }
208
- else if (type === "comm_close") {
209
- this.#comms.delete(commId);
210
- }
211
- }
212
- // --- target lifecycle -----------------------------------------------------
213
- #launchTarget() {
214
- if (this.#runtime === "bun")
215
- return BunTarget.launch(this.#targetEvents());
216
- if (this.#runtime === "workerd")
217
- return WorkerdTarget.launch(this.#targetEvents());
218
- if (this.#runtime === "celld")
219
- return launchCelld(this.#targetEvents());
220
- return Target.launch(this.#runtime, this.#targetEvents(), {
221
- denoCompat: this.#denoCompat,
222
- denoConfig: this.#project?.configPath,
172
+ // A server killed before it could shut the kernel down (#254).
173
+ watchParent(() => {
174
+ log(`parent ${process.env.JPY_PARENT_PID} exited; shutting down`);
175
+ core.stopping();
176
+ process.exit(1);
223
177
  });
224
- }
225
- #ensureTarget() {
226
- if (this.#target)
227
- return Promise.resolve(this.#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);
231
- this.#target = target;
232
- this.#targetStarting = null;
233
- return target;
234
- }, (err) => {
235
- this.#targetStarting = null;
236
- throw err;
237
- });
238
- return this.#targetStarting;
239
- }
240
- #targetEvents() {
241
- return {
242
- stream: (name, text) => this.#publish("stream", { name, text }),
243
- message: (msg) => void this.#onTargetMessage(msg).catch((err) => this.#log(`error handling target message: ${err}`)),
244
- exit: (code, note) => {
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);
249
- if (!this.#shuttingDown) {
250
- const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
251
- this.#publish("stream", { name: "stderr", text: `\n${text}\n` });
252
- }
253
- },
254
- };
255
- }
256
- async #onTargetMessage(msg) {
257
- if (msg.kind === "iopub") {
258
- this.#trackComm(msg.msg_type, msg.content);
259
- this.#publish(msg.msg_type, msg.content, {
260
- metadata: msg.metadata,
261
- buffers: msg.buffers.map((b) => new Uint8Array(Buffer.from(b, "base64"))),
262
- });
263
- return;
264
- }
265
- // The answer goes to the target that asked, not one started since.
266
- const target = this.#target;
267
- let answer = NO_INPUT;
268
- try {
269
- answer = await this.#requestInput(msg.prompt, msg.password);
270
- }
271
- finally {
272
- // Always answer exactly once so the target's blocking read returns.
273
- if (target !== null && target === this.#target)
274
- target.replyInput(answer.value, answer.interrupted);
275
- }
276
- }
277
- async #interrupt() {
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 {
291
- await this.#target.interrupt();
292
- }
293
- finally {
294
- this.#interrupting = false;
295
- }
296
- }
297
- // --- messaging --------------------------------------------------------------
298
- #encode(msgType, content, parentHeader, extra = {}) {
299
- return encodeMessage(this.#session, this.#conn.key, msgType, content, { parentHeader, ...extra });
300
- }
301
- #publish(msgType, content, extra = {}) {
302
- this.#iopub.send(this.#encode(msgType, content, extra.parent ?? this.#parent, extra));
303
- }
304
- #status(state, parent) {
305
- this.#publish("status", { execution_state: state }, { parent });
306
- }
307
- #reply(socket, peer, request, msgType, content) {
308
- socket.send(peer, this.#encode(msgType, content, request.header));
309
- }
310
- async #loop(socket, handle) {
311
- while (!this.#shuttingDown) {
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
- }
318
- try {
319
- const msg = decodeMessage(frames);
320
- // Unsigned or forged messages are dropped, as in Deno.
321
- if (!verify(this.#conn.key, msg.signedParts, msg.sig))
322
- continue;
323
- await handle(msg, peer);
324
- }
325
- catch (err) {
326
- this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
327
- }
328
- }
329
- }
330
- #kernelInfo() {
331
- const versions = this.#target?.versions;
332
- return {
333
- status: "ok",
334
- protocol_version: PROTOCOL_VERSION,
335
- implementation: "cdp-kernel",
336
- implementation_version: VERSION,
337
- language_info: {
338
- name: "typescript",
339
- version: "5",
340
- mimetype: "text/x.typescript",
341
- file_extension: ".ts",
342
- pygments_lexer: "typescript",
343
- codemirror_mode: "typescript",
344
- nbconvert_exporter: "script",
345
- },
346
- banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (${versions?.engine || "?"})`,
347
- help_links: [],
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"] : [],
352
- };
353
- }
354
- // --- control channel ------------------------------------------------------
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) {
369
- switch (msg.header.msg_type) {
370
- case "kernel_info_request":
371
- this.#reply(socket, peer, msg, "kernel_info_reply", this.#kernelInfo());
372
- break;
373
- case "interrupt_request":
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" });
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
- }
395
- case "shutdown_request": {
396
- const restart = msg.content.restart === true;
397
- this.#reply(socket, peer, msg, "shutdown_reply", { status: "ok", restart });
398
- this.#shuttingDown = true;
399
- this.#target?.kill();
400
- // Frontends spawn a fresh kernel even on restart; exit after the
401
- // reply has had a moment to flush (denoland/deno#20556).
402
- setTimeout(() => process.exit(0), 100);
403
- break;
404
- }
405
- }
406
- }
407
- // --- shell channel ----------------------------------------------------------
408
- async #handleShell(msg, peer) {
409
- const type = msg.header.msg_type;
410
- if (type === "execute_request")
411
- return this.#execute(msg, peer);
412
- this.#status("busy", msg.header);
413
- try {
414
- switch (type) {
415
- case "kernel_info_request":
416
- await this.#ensureTarget().catch(() => null);
417
- this.#reply(this.#shell, peer, msg, "kernel_info_reply", this.#kernelInfo());
418
- break;
419
- case "complete_request":
420
- this.#reply(this.#shell, peer, msg, "complete_reply", await this.#complete(msg));
421
- break;
422
- case "is_complete_request":
423
- this.#reply(this.#shell, peer, msg, "is_complete_reply", checkIsComplete(String(msg.content.code ?? "")));
424
- break;
425
- case "inspect_request":
426
- this.#reply(this.#shell, peer, msg, "inspect_reply", await this.#inspect(msg));
427
- break;
428
- case "history_request":
429
- this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: queryHistory(this.#history, msg.content) });
430
- break;
431
- case "comm_info_request":
432
- this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: this.#commInfo(msg.content) });
433
- break;
434
- case "comm_open":
435
- case "comm_msg":
436
- case "comm_close":
437
- await this.#handleComm(msg);
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 });
457
- }
458
- throw err;
459
- }
460
- finally {
461
- this.#status("idle", msg.header);
462
- }
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
- }
477
- async #complete(msg) {
478
- const code = String(msg.content.code ?? "");
479
- const cursorCp = typeof msg.content.cursor_pos === "number"
480
- ? msg.content.cursor_pos
481
- : utf16ToCodePointIndex(code, code.length);
482
- const cursor = codePointToUtf16Index(code, cursorCp);
483
- const expr = exprAtCursor(code, cursor);
484
- const target = await this.#ensureTarget();
485
- let matches;
486
- let start;
487
- const dot = expr.lastIndexOf(".");
488
- if (dot !== -1) {
489
- const prefix = expr.slice(dot + 1);
490
- matches = (await target.propertyNames(expr.slice(0, dot)))
491
- .filter((name) => name.startsWith(prefix) && !name.startsWith("Symbol("));
492
- start = cursor - prefix.length;
493
- }
494
- else {
495
- const names = new Set([...(await target.propertyNames("globalThis")), ...(await target.lexicalNames())]);
496
- matches = [...names].filter((name) => name.startsWith(expr) && !name.startsWith("__cdpk")).sort();
497
- start = cursor - expr.length;
498
- }
499
- return {
500
- status: "ok",
501
- matches,
502
- cursor_start: utf16ToCodePointIndex(code, start),
503
- cursor_end: cursorCp,
504
- metadata: {},
505
- };
506
- }
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 };
511
- const parent = this.#parent;
512
- if (!this.#allowStdin || typeof parent.msg_id !== "string")
513
- return NO_INPUT;
514
- if (this.#stdin.peers.size === 0)
515
- await new Promise((r) => setTimeout(r, 100));
516
- if (this.#stdin.peers.size === 0)
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 });
543
- }
544
- async #evaluateCell(target, code) {
545
- // Object-literal wrapping (Deno's evaluate_line_with_object_wrapping):
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());
551
- const wrapped = objectLiteralCandidate(code);
552
- if (wrapped !== null) {
553
- try {
554
- const result = await target.evaluate(transform(wrapped));
555
- if (!result.exceptionDetails)
556
- return result;
557
- }
558
- catch (err) {
559
- if (!(err instanceof CellSyntaxError))
560
- throw err;
561
- }
562
- }
563
- return target.evaluate(transform(code));
564
- }
565
- async #execute(msg, peer) {
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
- }
574
- const code = String(msg.content.code ?? "");
575
- const silent = msg.content.silent === true;
576
- if (!silent && msg.content.store_history !== false) {
577
- this.#executionCount++;
578
- this.#history.push({ line: this.#executionCount, input: code });
579
- }
580
- const count = this.#executionCount;
581
- this.#parent = parent;
582
- this.#allowStdin = msg.content.allow_stdin === true;
583
- this.#status("busy", parent);
584
- // Silent requests broadcast nothing on iopub but status (#124).
585
- if (!silent)
586
- this.#publish("execute_input", { code, execution_count: count });
587
- if (this.#projectWarning !== null) {
588
- // Shown once, in the first cell, where the user will see it.
589
- this.#publish("stream", { name: "stderr", text: `[cdp-kernel] ${this.#projectWarning}\n` });
590
- this.#projectWarning = null;
591
- }
592
- let error = null;
593
- this.#executing = true;
594
- try {
595
- const target = await this.#ensureTarget();
596
- if (target.notice !== undefined && this.#noticeShownFor !== target) {
597
- // Once per target, in the first cell it runs.
598
- this.#noticeShownFor = target;
599
- this.#publish("stream", { name: "stderr", text: `${target.notice}\n` });
600
- }
601
- const result = await this.#evaluateCell(target, code);
602
- if (result.exceptionDetails) {
603
- const exception = result.exceptionDetails.exception;
604
- const described = exception
605
- ? await target.describeError(exception)
606
- : { name: "Error", message: result.exceptionDetails.text, stack: "" };
607
- error = {
608
- ename: described.name || "Error",
609
- evalue: described.message,
610
- traceback: (described.stack || `${described.name}: ${described.message}`).split("\n"),
611
- };
612
- }
613
- else if (!silent && result.result.type !== "undefined") {
614
- await target.broadcastResult(count, result.result);
615
- }
616
- await target.flush();
617
- }
618
- catch (err) {
619
- if (err instanceof CellSyntaxError) {
620
- // Deno reports these as an opaque "Execution failed".
621
- error = { ename: "SyntaxError", evalue: err.message, traceback: [`SyntaxError: ${err.message}`] };
622
- }
623
- else if (err instanceof Interrupted) {
624
- error = { ename: "Interrupted", evalue: "Execution interrupted", traceback: ["Execution interrupted"] };
625
- await this.#target?.flush().catch(() => { });
626
- }
627
- else {
628
- const message = err instanceof Error ? err.message : String(err);
629
- error = { ename: "KernelError", evalue: message, traceback: [message] };
630
- }
631
- }
632
- finally {
633
- this.#executing = false;
634
- }
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
- }
643
- this.#publish("error", error);
644
- this.#reply(this.#shell, peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
645
- }
646
- else {
647
- this.#reply(this.#shell, peer, msg, "execute_reply", {
648
- status: "ok",
649
- execution_count: count,
650
- payload: [],
651
- user_expressions: {},
652
- });
653
- }
654
- this.#status("idle", parent);
178
+ // A state dump on stderr, for a kernel that has stopped answering (#237).
179
+ if (process.platform !== "win32")
180
+ process.on("SIGUSR2", () => void core.logState());
181
+ await core.run();
655
182
  }
656
183
  }