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