@tlockney/cdp-kernel 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -478
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +2 -2
- package/dist/bun-target.js +30 -8
- package/dist/cdp.d.ts +8 -1
- package/dist/cdp.js +31 -11
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +54 -19
- package/dist/children.d.ts +29 -0
- package/dist/children.js +114 -0
- package/dist/debugger.d.ts +1 -1
- package/dist/debugger.js +21 -10
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.d.ts +7 -1
- package/dist/kernel.js +348 -68
- package/dist/language-service-worker.d.ts +1 -0
- package/dist/language-service-worker.js +88 -0
- package/dist/language-service.d.ts +64 -0
- package/dist/language-service.js +128 -0
- package/dist/main.js +102 -8
- package/dist/mod.d.ts +6 -3
- package/dist/mod.js +6 -3
- package/dist/notebook-types.d.ts +62 -0
- package/dist/notebook-types.js +310 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +3 -3
- package/dist/parent.d.ts +12 -0
- package/dist/parent.js +39 -0
- package/dist/port-holder.d.ts +13 -0
- package/dist/port-holder.js +105 -0
- package/dist/sourcemap.d.ts +7 -2
- package/dist/sourcemap.js +9 -3
- package/dist/strip-esbuild.d.ts +2 -0
- package/dist/strip-esbuild.js +44 -0
- package/dist/strip-sucrase.d.ts +2 -0
- package/dist/strip-sucrase.js +78 -0
- package/dist/target.d.ts +58 -6
- package/dist/target.js +412 -54
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +30 -1
- package/dist/transform.js +68 -44
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +7 -1
- package/dist/workerd-target.js +102 -8
- package/dist/workers-project.js +13 -2
- package/dist/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +9 -1
- package/dist/zmtp.js +42 -1
- package/package.json +17 -6
- package/runtime/bootstrap.mjs +233 -42
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +2 -2
- package/runtime/comm.mjs +11 -1
- package/runtime/deno-compat.d.mts +2 -0
- package/runtime/deno-compat.mjs +14 -1
- package/runtime/display.mjs +8 -1
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +48 -0
- package/runtime/node-hooks.mjs +257 -41
- package/runtime/workerd-worker.mjs +77 -6
package/dist/kernel.js
CHANGED
|
@@ -5,20 +5,25 @@
|
|
|
5
5
|
// The thirteen op_jupyter_* calls into Deno's REPL isolate are replaced by
|
|
6
6
|
// a Target driven over CDP (src/target.ts).
|
|
7
7
|
import { Buffer } from "node:buffer";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
8
10
|
import process from "node:process";
|
|
9
11
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import { decodeMessage, encodeMessage, PROTOCOL_VERSION, verify } from "./wire.js";
|
|
11
|
-
import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
|
|
12
|
+
import { decodeMessage, encodeMessage, makeHeader, PROTOCOL_VERSION, verify } from "./wire.js";
|
|
13
|
+
import { listening, PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
|
|
12
14
|
import { Interrupted, Target } from "./target.js";
|
|
13
15
|
import { BunTarget } from "./bun-target.js";
|
|
14
16
|
import { Debugger } from "./debugger.js";
|
|
15
17
|
import { WorkerdTarget } from "./workerd-target.js";
|
|
16
18
|
import { launchCelld } from "./celld-target.js";
|
|
17
|
-
import { CellSyntaxError, objectLiteralCandidate } from "./transform.js";
|
|
19
|
+
import { CellSyntaxError, loadTypeStripper, objectLiteralCandidate, } from "./transform.js";
|
|
18
20
|
import { loadDenoProject } from "./deno-config.js";
|
|
19
21
|
import { checkIsComplete, codePointToUtf16Index, exprAtCursor, inspectTarget, utf16ToCodePointIndex } from "./text.js";
|
|
20
22
|
import { queryHistory } from "./history.js";
|
|
23
|
+
import { LanguageService } from "./language-service.js";
|
|
24
|
+
import { findWranglerConfig } from "./workers-project.js";
|
|
21
25
|
import { VERSION } from "./package.js";
|
|
26
|
+
import { watchParent } from "./parent.js";
|
|
22
27
|
export function parseConnectionInfo(json) {
|
|
23
28
|
const raw = JSON.parse(json);
|
|
24
29
|
if (typeof raw !== "object" || raw === null)
|
|
@@ -28,6 +33,10 @@ export function parseConnectionInfo(json) {
|
|
|
28
33
|
if (typeof info[port] !== "number")
|
|
29
34
|
throw new Error(`connection file: missing ${port}`);
|
|
30
35
|
}
|
|
36
|
+
for (const field of ["ip", "key"]) {
|
|
37
|
+
if (info[field] !== undefined && typeof info[field] !== "string")
|
|
38
|
+
throw new Error(`connection file: ${field} is not a string`);
|
|
39
|
+
}
|
|
31
40
|
if (info.transport !== undefined && info.transport !== "tcp") {
|
|
32
41
|
throw new Error(`unsupported transport ${info.transport}; only tcp is implemented`);
|
|
33
42
|
}
|
|
@@ -46,16 +55,88 @@ export function parseConnectionInfo(json) {
|
|
|
46
55
|
hb_port: info.hb_port,
|
|
47
56
|
};
|
|
48
57
|
}
|
|
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
|
+
};
|
|
73
|
+
/** The target Node's major version, for @types/node: the host's, which is usually the same binary. */
|
|
74
|
+
function nodeMajor() {
|
|
75
|
+
return process.versions.node?.split(".")[0] ?? "*";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Where the language service gets each target's globals and imports from
|
|
79
|
+
* (#273, #275). A `--deno-compat` Node target gets Node's types only: its
|
|
80
|
+
* `Deno` namespace is partial, and Deno's declarations would promise
|
|
81
|
+
* members it doesn't have.
|
|
82
|
+
*/
|
|
83
|
+
function languageServiceOptions(runtime, notebookDir, project) {
|
|
84
|
+
const base = { notebookDir, npmCache: "npm", deno: null, denoTypes: false, typeFiles: [], typePackages: [] };
|
|
85
|
+
switch (runtime) {
|
|
86
|
+
case "node":
|
|
87
|
+
return { ...base, typePackages: [{ name: "@types/node", range: nodeMajor() }] };
|
|
88
|
+
case "deno":
|
|
89
|
+
return { ...base, npmCache: null, deno: { config: project?.configPath ?? null }, denoTypes: true };
|
|
90
|
+
case "bun":
|
|
91
|
+
return { ...base, npmCache: "bun", typePackages: [{ name: "@types/bun", range: "1" }] };
|
|
92
|
+
case "workerd":
|
|
93
|
+
case "celld": {
|
|
94
|
+
// A Workers project's own types (`wrangler types`) know its bindings.
|
|
95
|
+
const config = findWranglerConfig(notebookDir);
|
|
96
|
+
const generated = config === null ? null : join(dirname(config), "worker-configuration.d.ts");
|
|
97
|
+
return generated !== null && existsSync(generated)
|
|
98
|
+
? { ...base, typeFiles: [generated] }
|
|
99
|
+
: { ...base, typePackages: [{ name: "@cloudflare/workers-types", range: "*" }] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
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
|
+
}
|
|
49
127
|
export class Kernel {
|
|
50
128
|
#conn;
|
|
51
129
|
#runtime;
|
|
52
130
|
#denoCompat;
|
|
131
|
+
#typeStrip;
|
|
53
132
|
// A Deno target in a Deno project uses its config (#13).
|
|
54
133
|
#project;
|
|
55
134
|
#projectWarning = null;
|
|
56
135
|
#noticeShownFor = null;
|
|
57
136
|
/** JupyterLab's visual debugger, for targets driven over CDP (#18). */
|
|
58
137
|
#debugger;
|
|
138
|
+
/** Typed completion from TypeScript's language service (#273). */
|
|
139
|
+
#types;
|
|
59
140
|
/** Cells run with store_history, for history_request (#10). */
|
|
60
141
|
#history = [];
|
|
61
142
|
#session = randomUUID();
|
|
@@ -63,12 +144,26 @@ export class Kernel {
|
|
|
63
144
|
#control;
|
|
64
145
|
#stdin;
|
|
65
146
|
#iopub;
|
|
147
|
+
/** Settles once all five ports are bound (#257). */
|
|
148
|
+
#bound;
|
|
66
149
|
#target = null;
|
|
67
150
|
#targetStarting = null;
|
|
68
151
|
#executionCount = 0;
|
|
69
152
|
#parent = {};
|
|
70
153
|
#allowStdin = false;
|
|
71
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;
|
|
72
167
|
// Open comms, for comm_info_request: comm_id -> target_name (#9).
|
|
73
168
|
#comms = new Map();
|
|
74
169
|
#shuttingDown = false;
|
|
@@ -76,26 +171,68 @@ export class Kernel {
|
|
|
76
171
|
this.#conn = conn;
|
|
77
172
|
this.#runtime = runtime;
|
|
78
173
|
this.#denoCompat = options.denoCompat ?? false;
|
|
79
|
-
this.#
|
|
174
|
+
this.#typeStrip = options.typeStrip ?? "esbuild";
|
|
175
|
+
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) });
|
|
80
180
|
this.#debugger = runtime === "node" || runtime === "deno"
|
|
81
181
|
? new Debugger((event) => this.#publish("debug_event", event), () => this.#transformOptions())
|
|
82
182
|
: null;
|
|
83
|
-
startHeartbeat(conn.hb_port, conn.ip);
|
|
183
|
+
const heartbeat = startHeartbeat(conn.hb_port, conn.ip);
|
|
84
184
|
this.#shell = new RouterSocket(conn.shell_port, conn.ip);
|
|
85
185
|
this.#control = new RouterSocket(conn.control_port, conn.ip);
|
|
86
186
|
this.#stdin = new RouterSocket(conn.stdin_port, conn.ip);
|
|
87
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));
|
|
88
189
|
}
|
|
89
190
|
async run() {
|
|
90
191
|
// Start the runtime eagerly so the first cell doesn't pay for it.
|
|
91
192
|
this.#ensureTarget().catch((err) => this.#log(`target failed to start: ${err}`));
|
|
92
193
|
process.on("SIGINT", () => void this.#interrupt());
|
|
194
|
+
// Exiting runs the exit hook that kills the runtime (src/children.ts);
|
|
195
|
+
// these signals' default action would skip it (#218).
|
|
196
|
+
for (const [signal, code] of [["SIGTERM", 143], ["SIGHUP", 129]]) {
|
|
197
|
+
process.on(signal, () => {
|
|
198
|
+
this.#shuttingDown = true;
|
|
199
|
+
process.exit(code);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// A server killed before it could shut the kernel down (#254).
|
|
203
|
+
watchParent(() => {
|
|
204
|
+
this.#log(`parent ${process.env.JPY_PARENT_PID} exited; shutting down`);
|
|
205
|
+
this.#shuttingDown = true;
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|
|
208
|
+
// A state dump on stderr, for a kernel that has stopped answering (#237).
|
|
209
|
+
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);
|
|
93
214
|
await Promise.all([this.#loop(this.#shell, (m, p) => this.#handleShell(m, p)),
|
|
94
|
-
this.#loop(this.#control, (m, p) => this.#handleControl(m, p))
|
|
215
|
+
this.#loop(this.#control, (m, p) => this.#handleControl(m, p)),
|
|
216
|
+
this.#loop(this.#stdin, (m) => this.#handleStdin(m))]);
|
|
95
217
|
}
|
|
96
218
|
#log(text) {
|
|
97
219
|
process.stderr.write(`[cdp-kernel] ${text}\n`);
|
|
98
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
|
+
}
|
|
99
236
|
#loadProject(notebookDir) {
|
|
100
237
|
try {
|
|
101
238
|
return loadDenoProject(notebookDir);
|
|
@@ -110,7 +247,7 @@ export class Kernel {
|
|
|
110
247
|
}
|
|
111
248
|
}
|
|
112
249
|
#transformOptions() {
|
|
113
|
-
return this.#project?.transform
|
|
250
|
+
return { ...this.#project?.transform, typeStrip: this.#typeStrip };
|
|
114
251
|
}
|
|
115
252
|
// --- comms (#9) ------------------------------------------------------------
|
|
116
253
|
#trackComm(msgType, content) {
|
|
@@ -190,7 +327,10 @@ export class Kernel {
|
|
|
190
327
|
#ensureTarget() {
|
|
191
328
|
if (this.#target)
|
|
192
329
|
return Promise.resolve(this.#target);
|
|
193
|
-
|
|
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) => {
|
|
194
334
|
// Before any cell runs on it, so breakpoints already apply.
|
|
195
335
|
await this.#debugger?.attachTarget(target.debug ?? null);
|
|
196
336
|
this.#target = target;
|
|
@@ -205,9 +345,13 @@ export class Kernel {
|
|
|
205
345
|
#targetEvents() {
|
|
206
346
|
return {
|
|
207
347
|
stream: (name, text) => this.#publish("stream", { name, text }),
|
|
208
|
-
message: (msg) => void this.#onTargetMessage(msg),
|
|
348
|
+
message: (msg) => void this.#onTargetMessage(msg).catch((err) => this.#log(`error handling target message: ${err}`)),
|
|
209
349
|
exit: (code, note) => {
|
|
210
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;
|
|
211
355
|
void this.#debugger?.attachTarget(null);
|
|
212
356
|
if (!this.#shuttingDown) {
|
|
213
357
|
const text = note ?? `[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell`;
|
|
@@ -225,18 +369,37 @@ export class Kernel {
|
|
|
225
369
|
});
|
|
226
370
|
return;
|
|
227
371
|
}
|
|
228
|
-
|
|
372
|
+
// The answer goes to the target that asked, not one started since.
|
|
373
|
+
const target = this.#target;
|
|
374
|
+
let answer = NO_INPUT;
|
|
229
375
|
try {
|
|
230
|
-
|
|
376
|
+
answer = await this.#requestInput(msg.prompt, msg.password);
|
|
231
377
|
}
|
|
232
378
|
finally {
|
|
233
379
|
// Always answer exactly once so the target's blocking read returns.
|
|
234
|
-
this.#target
|
|
380
|
+
if (target !== null && target === this.#target)
|
|
381
|
+
target.replyInput(answer.value, answer.interrupted);
|
|
235
382
|
}
|
|
236
383
|
}
|
|
237
384
|
async #interrupt() {
|
|
238
|
-
|
|
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 {
|
|
239
398
|
await this.#target.interrupt();
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
this.#interrupting = false;
|
|
402
|
+
}
|
|
240
403
|
}
|
|
241
404
|
// --- messaging --------------------------------------------------------------
|
|
242
405
|
#encode(msgType, content, parentHeader, extra = {}) {
|
|
@@ -254,16 +417,27 @@ export class Kernel {
|
|
|
254
417
|
async #loop(socket, handle) {
|
|
255
418
|
while (!this.#shuttingDown) {
|
|
256
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
|
+
}
|
|
257
425
|
try {
|
|
258
426
|
const msg = decodeMessage(frames);
|
|
259
427
|
// Unsigned or forged messages are dropped, as in Deno.
|
|
260
428
|
if (!verify(this.#conn.key, msg.signedParts, msg.sig))
|
|
261
429
|
continue;
|
|
430
|
+
if (socket === this.#shell)
|
|
431
|
+
this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
|
|
262
432
|
await handle(msg, peer);
|
|
263
433
|
}
|
|
264
434
|
catch (err) {
|
|
265
435
|
this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
|
|
266
436
|
}
|
|
437
|
+
finally {
|
|
438
|
+
if (socket === this.#shell)
|
|
439
|
+
this.#handling = null;
|
|
440
|
+
}
|
|
267
441
|
}
|
|
268
442
|
}
|
|
269
443
|
#kernelInfo() {
|
|
@@ -282,7 +456,8 @@ export class Kernel {
|
|
|
282
456
|
codemirror_mode: "typescript",
|
|
283
457
|
nbconvert_exporter: "script",
|
|
284
458
|
},
|
|
285
|
-
|
|
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}`,
|
|
286
461
|
help_links: [],
|
|
287
462
|
debugger: this.#debugger !== null,
|
|
288
463
|
// What current ipykernel advertises; JupyterLab itself goes by the
|
|
@@ -292,40 +467,48 @@ export class Kernel {
|
|
|
292
467
|
}
|
|
293
468
|
// --- control channel ------------------------------------------------------
|
|
294
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) {
|
|
295
483
|
switch (msg.header.msg_type) {
|
|
296
484
|
case "kernel_info_request":
|
|
297
|
-
this.#reply(
|
|
485
|
+
this.#reply(socket, peer, msg, "kernel_info_reply", this.#kernelInfo());
|
|
298
486
|
break;
|
|
299
487
|
case "interrupt_request":
|
|
300
|
-
|
|
301
|
-
|
|
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" });
|
|
302
494
|
break;
|
|
303
495
|
case "debug_request": {
|
|
304
|
-
// JupyterLab treats a debug request as done only once the kernel
|
|
305
|
-
// reports idle for it, as ipykernel does for control messages.
|
|
306
|
-
// (Jupyter Server leaves debug_request out of the kernel's status.)
|
|
307
|
-
this.#status("busy", msg.header);
|
|
308
496
|
let reply;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
reply = { type: "response", request_seq: msg.content.seq, command: msg.content.command, success: false, message: `${this.#runtime} targets have no debugger`, body: {} };
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
// Everything but debugInfo talks to the target, so make sure there is one.
|
|
315
|
-
if (msg.content.command !== "debugInfo")
|
|
316
|
-
await this.#ensureTarget().catch(() => null);
|
|
317
|
-
reply = await this.#debugger.handle(msg.content);
|
|
318
|
-
}
|
|
319
|
-
this.#reply(this.#control, peer, msg, "debug_reply", 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: {} };
|
|
320
499
|
}
|
|
321
|
-
|
|
322
|
-
|
|
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);
|
|
323
505
|
}
|
|
506
|
+
this.#reply(socket, peer, msg, "debug_reply", reply);
|
|
324
507
|
break;
|
|
325
508
|
}
|
|
326
509
|
case "shutdown_request": {
|
|
327
510
|
const restart = msg.content.restart === true;
|
|
328
|
-
this.#reply(
|
|
511
|
+
this.#reply(socket, peer, msg, "shutdown_reply", { status: "ok", restart });
|
|
329
512
|
this.#shuttingDown = true;
|
|
330
513
|
this.#target?.kill();
|
|
331
514
|
// Frontends spawn a fresh kernel even on restart; exit after the
|
|
@@ -344,7 +527,8 @@ export class Kernel {
|
|
|
344
527
|
try {
|
|
345
528
|
switch (type) {
|
|
346
529
|
case "kernel_info_request":
|
|
347
|
-
|
|
530
|
+
// Without waiting for the runtime: clients probe with kernel_info
|
|
531
|
+
// while it starts, and re-send until answered (#253).
|
|
348
532
|
this.#reply(this.#shell, peer, msg, "kernel_info_reply", this.#kernelInfo());
|
|
349
533
|
break;
|
|
350
534
|
case "complete_request":
|
|
@@ -367,21 +551,49 @@ export class Kernel {
|
|
|
367
551
|
case "comm_close":
|
|
368
552
|
await this.#handleComm(msg);
|
|
369
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}`);
|
|
370
561
|
}
|
|
371
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
|
+
}
|
|
372
575
|
finally {
|
|
373
576
|
this.#status("idle", msg.header);
|
|
374
577
|
}
|
|
375
578
|
}
|
|
376
|
-
/**
|
|
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
|
+
*/
|
|
377
584
|
async #inspect(msg) {
|
|
378
585
|
const code = String(msg.content.code ?? "");
|
|
379
586
|
const cursorCp = typeof msg.content.cursor_pos === "number"
|
|
380
587
|
? msg.content.cursor_pos
|
|
381
588
|
: utf16ToCodePointIndex(code, code.length);
|
|
382
|
-
const
|
|
589
|
+
const cursor = codePointToUtf16Index(code, cursorCp);
|
|
590
|
+
const path = inspectTarget(code, cursor);
|
|
383
591
|
const detail = msg.content.detail_level === 1 ? 1 : 0;
|
|
384
|
-
const
|
|
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)}`;
|
|
385
597
|
return text === null
|
|
386
598
|
? { status: "ok", found: false, data: {}, metadata: {} }
|
|
387
599
|
: { status: "ok", found: true, data: { "text/plain": text }, metadata: {} };
|
|
@@ -392,48 +604,96 @@ export class Kernel {
|
|
|
392
604
|
? msg.content.cursor_pos
|
|
393
605
|
: utf16ToCodePointIndex(code, code.length);
|
|
394
606
|
const cursor = codePointToUtf16Index(code, cursorCp);
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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))];
|
|
405
621
|
}
|
|
406
|
-
else {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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;
|
|
410
628
|
}
|
|
629
|
+
const cursorStart = utf16ToCodePointIndex(code, start);
|
|
411
630
|
return {
|
|
412
631
|
status: "ok",
|
|
413
632
|
matches,
|
|
414
|
-
cursor_start:
|
|
633
|
+
cursor_start: cursorStart,
|
|
415
634
|
cursor_end: cursorCp,
|
|
416
|
-
metadata: {}
|
|
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
|
+
},
|
|
417
644
|
};
|
|
418
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
|
+
}
|
|
419
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 };
|
|
420
665
|
const parent = this.#parent;
|
|
421
666
|
if (!this.#allowStdin || typeof parent.msg_id !== "string")
|
|
422
|
-
return
|
|
667
|
+
return NO_INPUT;
|
|
423
668
|
if (this.#stdin.peers.size === 0)
|
|
424
669
|
await new Promise((r) => setTimeout(r, 100));
|
|
425
670
|
if (this.#stdin.peers.size === 0)
|
|
426
|
-
return
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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 });
|
|
437
697
|
}
|
|
438
698
|
async #evaluateCell(target, code) {
|
|
439
699
|
// Object-literal wrapping (Deno's evaluate_line_with_object_wrapping):
|
|
@@ -458,6 +718,13 @@ export class Kernel {
|
|
|
458
718
|
}
|
|
459
719
|
async #execute(msg, peer) {
|
|
460
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
|
+
}
|
|
461
728
|
const code = String(msg.content.code ?? "");
|
|
462
729
|
const silent = msg.content.silent === true;
|
|
463
730
|
if (!silent && msg.content.store_history !== false) {
|
|
@@ -468,7 +735,9 @@ export class Kernel {
|
|
|
468
735
|
this.#parent = parent;
|
|
469
736
|
this.#allowStdin = msg.content.allow_stdin === true;
|
|
470
737
|
this.#status("busy", parent);
|
|
471
|
-
|
|
738
|
+
// Silent requests broadcast nothing on iopub but status (#124).
|
|
739
|
+
if (!silent)
|
|
740
|
+
this.#publish("execute_input", { code, execution_count: count });
|
|
472
741
|
if (this.#projectWarning !== null) {
|
|
473
742
|
// Shown once, in the first cell, where the user will see it.
|
|
474
743
|
this.#publish("stream", { name: "stderr", text: `[cdp-kernel] ${this.#projectWarning}\n` });
|
|
@@ -484,6 +753,8 @@ export class Kernel {
|
|
|
484
753
|
this.#publish("stream", { name: "stderr", text: `${target.notice}\n` });
|
|
485
754
|
}
|
|
486
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);
|
|
487
758
|
if (result.exceptionDetails) {
|
|
488
759
|
const exception = result.exceptionDetails.exception;
|
|
489
760
|
const described = exception
|
|
@@ -517,7 +788,16 @@ export class Kernel {
|
|
|
517
788
|
finally {
|
|
518
789
|
this.#executing = false;
|
|
519
790
|
}
|
|
791
|
+
// The result and error are shown: nothing refers to them any more (#286).
|
|
792
|
+
await this.#target?.releaseResults?.();
|
|
520
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
|
+
}
|
|
521
801
|
this.#publish("error", error);
|
|
522
802
|
this.#reply(this.#shell, peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
|
|
523
803
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|