@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.
- package/README.md +27 -6
- package/dist/browser/channels.d.ts +27 -0
- package/dist/browser/channels.js +90 -0
- package/dist/browser/kernel.js +29674 -0
- package/dist/browser/kernel.js.map +7 -0
- package/dist/browser/mod.d.ts +20 -0
- package/dist/browser/mod.js +38 -0
- package/dist/browser/target.d.ts +71 -0
- package/dist/browser/target.js +206 -0
- package/dist/browser/worker.js +1000 -0
- package/dist/browser/worker.js.map +7 -0
- package/dist/cdp.d.ts +11 -0
- package/dist/cdp.js +6 -1
- package/dist/celld-target.js +9 -12
- package/dist/children.d.ts +25 -3
- package/dist/children.js +84 -2
- package/dist/debugger.d.ts +2 -0
- package/dist/debugger.js +12 -6
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/kernel-core.d.ts +69 -0
- package/dist/kernel-core.js +754 -0
- package/dist/kernel.d.ts +12 -1
- package/dist/kernel.js +132 -605
- 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/message.d.ts +34 -0
- package/dist/message.js +14 -0
- package/dist/mod.d.ts +1 -0
- package/dist/mod.js +1 -0
- 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 +2 -2
- 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-api.d.ts +80 -0
- package/dist/target-api.js +22 -0
- package/dist/target.d.ts +26 -74
- package/dist/target.js +232 -58
- package/dist/traceback.d.ts +75 -0
- package/dist/traceback.js +200 -0
- package/dist/transform.d.ts +22 -1
- package/dist/transform.js +46 -43
- package/dist/wire.d.ts +4 -24
- package/dist/wire.js +3 -12
- package/dist/workerd-target.js +1 -1
- package/dist/workers-project.js +13 -2
- package/dist/zmq-channels.d.ts +14 -0
- package/dist/zmq-channels.js +59 -0
- package/dist/zmtp.d.ts +2 -0
- package/dist/zmtp.js +12 -0
- package/package.json +15 -5
- package/runtime/bootstrap.mjs +113 -33
- package/runtime/browser-worker.mjs +119 -0
- package/runtime/cdn.d.mts +2 -0
- package/runtime/cdn.mjs +17 -0
- package/runtime/display.mjs +202 -1
- package/runtime/format.d.mts +2 -0
- package/runtime/format.mjs +125 -0
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +6 -0
- package/runtime/node-hooks.mjs +11 -0
package/dist/kernel.js
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
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 {
|
|
20
|
-
import {
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
168
|
+
core.stopping();
|
|
124
169
|
process.exit(code);
|
|
125
170
|
});
|
|
126
171
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
}
|