@tlockney/cdp-kernel 0.7.2 → 0.8.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/dist/browser/kernel.js +203 -41
- package/dist/browser/kernel.js.map +2 -2
- package/dist/browser/target.d.ts +2 -1
- package/dist/browser/target.js +29 -7
- package/dist/browser/worker.js +47 -4
- package/dist/browser/worker.js.map +2 -2
- package/dist/bun-target.d.ts +5 -1
- package/dist/bun-target.js +32 -7
- package/dist/crash-reports.d.ts +47 -0
- package/dist/crash-reports.js +155 -0
- package/dist/kernel-core.d.ts +7 -0
- package/dist/kernel-core.js +212 -35
- package/dist/kernel.js +10 -0
- package/dist/main.js +31 -14
- package/dist/message.d.ts +1 -1
- package/dist/message.js +1 -1
- package/dist/package.d.ts +2 -2
- package/dist/package.js +2 -2
- package/dist/target-api.d.ts +11 -0
- package/dist/target.d.ts +9 -2
- package/dist/target.js +66 -12
- package/dist/transform.js +3 -1
- package/dist/usage.d.ts +25 -0
- package/dist/usage.js +197 -0
- package/dist/workerd-target.d.ts +13 -2
- package/dist/workerd-target.js +36 -6
- package/dist/workers-project.js +2 -0
- package/package.json +1 -1
- package/runtime/bootstrap.mjs +65 -7
- package/runtime/browser-worker.mjs +6 -2
- package/runtime/display.d.mts +24 -2
- package/runtime/display.mjs +42 -1
- package/runtime/evaluator.d.mts +5 -1
- package/runtime/evaluator.mjs +12 -1
- package/runtime/workerd-worker.mjs +6 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** The cdp-kernel project's DSN. It only lets a client send events. */
|
|
2
|
+
export declare const DSN = "https://91c2265acb5b0b0115914a43375be9ba@o4512380800598016.ingest.us.sentry.io/4512380976168960";
|
|
3
|
+
export interface Frame {
|
|
4
|
+
filename: string;
|
|
5
|
+
function?: string;
|
|
6
|
+
lineno?: number;
|
|
7
|
+
colno?: number;
|
|
8
|
+
in_app: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** `text` with home directories as `~`, URLs cut to their path, and at most MAX_MESSAGE characters. */
|
|
11
|
+
export declare function scrubText(text: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* A frame's file as cdp-kernel's own, `app:///src/kernel-core.ts`, or null
|
|
14
|
+
* when it isn't: its path from the package root (`src/`, `dist/` or
|
|
15
|
+
* `runtime/` and below), wherever the package is installed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function packageFile(file: string): string | null;
|
|
18
|
+
/** V8's stack lines as Sentry frames, oldest first. Files outside the package are dropped to `<other>`. */
|
|
19
|
+
export declare function parseStack(stack: string | undefined): Frame[];
|
|
20
|
+
export interface ReportContext {
|
|
21
|
+
/** Where it was caught, e.g. `shell handler`. */
|
|
22
|
+
where: string;
|
|
23
|
+
/** The target runtime: node, deno, bun, workerd or celld. */
|
|
24
|
+
target?: string;
|
|
25
|
+
}
|
|
26
|
+
/** The Sentry event for `err`. Pure but for the clock and the ids. */
|
|
27
|
+
export declare function buildEvent(err: unknown, context: ReportContext): Record<string, unknown>;
|
|
28
|
+
export interface Reporter {
|
|
29
|
+
/** Sends a report; resolves once sent or given up on. Never throws. */
|
|
30
|
+
report(err: unknown, where: string): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The reporter for this process: a real one when CDP_KERNEL_CRASH_REPORTS
|
|
34
|
+
* is `1`, otherwise one that does nothing. `send` is for tests.
|
|
35
|
+
*/
|
|
36
|
+
export declare function crashReporter(options?: {
|
|
37
|
+
env?: Record<string, string | undefined>;
|
|
38
|
+
target?: string;
|
|
39
|
+
send?: typeof fetch;
|
|
40
|
+
}): Reporter | null;
|
|
41
|
+
/**
|
|
42
|
+
* Reports an exception that would end the process, then ends it as Node
|
|
43
|
+
* would have: the stack on stderr and exit code 1. Only installed when
|
|
44
|
+
* reporting is on, so with it off a crash is exactly as before. An
|
|
45
|
+
* unhandled rejection arrives here too (Node's default `throw` mode).
|
|
46
|
+
*/
|
|
47
|
+
export declare function reportUncaught(reporter: Reporter): void;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Opt-in reports of faults in cdp-kernel's own code (#422,
|
|
2
|
+
// tlockney/ganymede#384), sent to Sentry's cdp-kernel project.
|
|
3
|
+
//
|
|
4
|
+
// Off unless the host sets CDP_KERNEL_CRASH_REPORTS=1, which Ganymede does
|
|
5
|
+
// when its user opted in. While off nothing here runs: no handler, no
|
|
6
|
+
// request. Errors from user cells never come through here: the target
|
|
7
|
+
// reports those to KernelCore as exception details, and KernelCore answers
|
|
8
|
+
// them as cell errors. What does: an exception that escapes a message
|
|
9
|
+
// handler, the target-message handler, or the process.
|
|
10
|
+
//
|
|
11
|
+
// No Sentry SDK: an event is a small JSON envelope, posted with fetch.
|
|
12
|
+
// That keeps the npm and JSR packages free of a dependency and its
|
|
13
|
+
// install-time cost for everyone who never opts in.
|
|
14
|
+
//
|
|
15
|
+
// Sent: the error's type, message (home directories as `~`, URLs cut to
|
|
16
|
+
// their path, 300 characters at most) and stack (frames in cdp-kernel's
|
|
17
|
+
// own files by their path in the package; others by name only), the
|
|
18
|
+
// version, host and target runtime, OS and architecture. Not sent: cell
|
|
19
|
+
// source, outputs, variables, file paths outside the package, env vars,
|
|
20
|
+
// host name.
|
|
21
|
+
import process from "node:process";
|
|
22
|
+
import { arch, platform, release } from "node:os";
|
|
23
|
+
import { hostRuntime, VERSION } from "./package.js";
|
|
24
|
+
/** The cdp-kernel project's DSN. It only lets a client send events. */
|
|
25
|
+
export const DSN = "https://91c2265acb5b0b0115914a43375be9ba@o4512380800598016.ingest.us.sentry.io/4512380976168960";
|
|
26
|
+
/** At most this many reports per process: a fault in a loop sends a few, not thousands. */
|
|
27
|
+
const MAX_REPORTS = 10;
|
|
28
|
+
const MAX_MESSAGE = 300;
|
|
29
|
+
const SEND_TIMEOUT_MS = 5000;
|
|
30
|
+
/** `text` with home directories as `~`, URLs cut to their path, and at most MAX_MESSAGE characters. */
|
|
31
|
+
export function scrubText(text) {
|
|
32
|
+
const out = text
|
|
33
|
+
.replace(/\b(?:https?|wss?|file):\/\/[^\s/'"`)]*([^\s?#'"`)]*)(?:[?#][^\s'"`)]*)?/g, "$1")
|
|
34
|
+
.replace(/(?:\/Users|\/home)\/[^/\s'"`)]+/g, "~")
|
|
35
|
+
.replace(/[A-Za-z]:\\Users\\[^\\\s'"`)]+/g, "~");
|
|
36
|
+
return out.length > MAX_MESSAGE ? `${out.slice(0, MAX_MESSAGE)}…` : out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A frame's file as cdp-kernel's own, `app:///src/kernel-core.ts`, or null
|
|
40
|
+
* when it isn't: its path from the package root (`src/`, `dist/` or
|
|
41
|
+
* `runtime/` and below), wherever the package is installed.
|
|
42
|
+
*/
|
|
43
|
+
export function packageFile(file) {
|
|
44
|
+
const path = file.replace(/^file:\/\//, "").replaceAll("\\", "/");
|
|
45
|
+
// From npm, from JSR (jsr.io/@tlockney/cdp-kernel/<version>/src/…), or a checkout.
|
|
46
|
+
const match = /(?:^|\/)(?:@tlockney\/cdp-kernel(?:\/[^/]+)?|cdp-kernel)\/((?:src|dist|runtime)\/[^?#]+)$/.exec(path);
|
|
47
|
+
return match ? `app:///${match[1]}` : null;
|
|
48
|
+
}
|
|
49
|
+
const V8_FRAME = /^\s*at (?:(.+?) \()?(.+?):(\d+):(\d+)\)?$/;
|
|
50
|
+
/** V8's stack lines as Sentry frames, oldest first. Files outside the package are dropped to `<other>`. */
|
|
51
|
+
export function parseStack(stack) {
|
|
52
|
+
const frames = [];
|
|
53
|
+
for (const line of (stack ?? "").split("\n")) {
|
|
54
|
+
const m = V8_FRAME.exec(line);
|
|
55
|
+
if (!m)
|
|
56
|
+
continue;
|
|
57
|
+
const [, fn, file, lineno, colno] = m;
|
|
58
|
+
const own = packageFile(file);
|
|
59
|
+
const internal = file.startsWith("node:") || file.startsWith("ext:");
|
|
60
|
+
frames.push({
|
|
61
|
+
filename: own ?? (internal ? file : "<other>"),
|
|
62
|
+
function: fn,
|
|
63
|
+
lineno: own || internal ? Number(lineno) : undefined,
|
|
64
|
+
colno: own || internal ? Number(colno) : undefined,
|
|
65
|
+
in_app: own !== null,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return frames.reverse();
|
|
69
|
+
}
|
|
70
|
+
/** The Sentry event for `err`. Pure but for the clock and the ids. */
|
|
71
|
+
export function buildEvent(err, context) {
|
|
72
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
73
|
+
return {
|
|
74
|
+
event_id: crypto.randomUUID().replaceAll("-", ""),
|
|
75
|
+
timestamp: Date.now() / 1000,
|
|
76
|
+
platform: "node",
|
|
77
|
+
level: "error",
|
|
78
|
+
release: `cdp-kernel@${VERSION}`,
|
|
79
|
+
environment: "production",
|
|
80
|
+
tags: { where: context.where, host: hostRuntime(), ...(context.target ? { target: context.target } : {}) },
|
|
81
|
+
contexts: {
|
|
82
|
+
os: { name: platform(), kernel_version: release() },
|
|
83
|
+
device: { arch: arch() },
|
|
84
|
+
runtime: { name: hostRuntime(), version: process.versions.deno ?? process.versions.node },
|
|
85
|
+
},
|
|
86
|
+
exception: {
|
|
87
|
+
values: [{
|
|
88
|
+
type: error.name,
|
|
89
|
+
value: scrubText(error.message),
|
|
90
|
+
stacktrace: { frames: parseStack(error.stack) },
|
|
91
|
+
mechanism: { type: context.where, handled: context.where !== "uncaught" },
|
|
92
|
+
}],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function parseDsn(dsn) {
|
|
97
|
+
const url = new URL(dsn);
|
|
98
|
+
const project = url.pathname.replace(/^\//, "");
|
|
99
|
+
return { key: url.username, envelope: `${url.protocol}//${url.host}/api/${project}/envelope/` };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The reporter for this process: a real one when CDP_KERNEL_CRASH_REPORTS
|
|
103
|
+
* is `1`, otherwise one that does nothing. `send` is for tests.
|
|
104
|
+
*/
|
|
105
|
+
export function crashReporter(options = {}) {
|
|
106
|
+
const env = options.env ?? process.env;
|
|
107
|
+
if (env.CDP_KERNEL_CRASH_REPORTS !== "1")
|
|
108
|
+
return null;
|
|
109
|
+
const dsn = parseDsn(env.CDP_KERNEL_SENTRY_DSN || DSN);
|
|
110
|
+
const send = options.send ?? fetch;
|
|
111
|
+
let sent = 0;
|
|
112
|
+
return {
|
|
113
|
+
async report(err, where) {
|
|
114
|
+
if (sent >= MAX_REPORTS)
|
|
115
|
+
return;
|
|
116
|
+
sent++;
|
|
117
|
+
try {
|
|
118
|
+
const event = buildEvent(err, { where, target: options.target });
|
|
119
|
+
const body = [
|
|
120
|
+
JSON.stringify({ event_id: event.event_id, sent_at: new Date().toISOString() }),
|
|
121
|
+
JSON.stringify({ type: "event" }),
|
|
122
|
+
JSON.stringify(event),
|
|
123
|
+
].join("\n");
|
|
124
|
+
await send(dsn.envelope, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"content-type": "application/x-sentry-envelope",
|
|
128
|
+
"x-sentry-auth": `Sentry sentry_version=7, sentry_key=${dsn.key}, sentry_client=cdp-kernel/${VERSION}`,
|
|
129
|
+
},
|
|
130
|
+
body,
|
|
131
|
+
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Reporting must never take the kernel down.
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Reports an exception that would end the process, then ends it as Node
|
|
142
|
+
* would have: the stack on stderr and exit code 1. Only installed when
|
|
143
|
+
* reporting is on, so with it off a crash is exactly as before. An
|
|
144
|
+
* unhandled rejection arrives here too (Node's default `throw` mode).
|
|
145
|
+
*/
|
|
146
|
+
export function reportUncaught(reporter) {
|
|
147
|
+
let ending = false;
|
|
148
|
+
process.on("uncaughtException", (err) => {
|
|
149
|
+
if (ending)
|
|
150
|
+
return;
|
|
151
|
+
ending = true;
|
|
152
|
+
console.error(err instanceof Error ? err.stack ?? String(err) : err);
|
|
153
|
+
void reporter.report(err, "uncaught").finally(() => process.exit(1));
|
|
154
|
+
});
|
|
155
|
+
}
|
package/dist/kernel-core.d.ts
CHANGED
|
@@ -52,11 +52,18 @@ export interface KernelHost {
|
|
|
52
52
|
readonly capabilities?: JsonObject;
|
|
53
53
|
/** JupyterLab's visual debugger, for targets driven over CDP (#18). */
|
|
54
54
|
createDebugger?(publish: (event: JsonObject) => void, transformOptions: () => CellTransformOptions): Debugger;
|
|
55
|
+
/** usage_reply's content: the host's and the running target's (null while none runs) memory and CPU (#409). */
|
|
56
|
+
usage?(target: KernelTarget | null): Promise<JsonObject>;
|
|
55
57
|
/** A file's source, for the lines around a traceback frame (#322). */
|
|
56
58
|
readSource?(file: string): string | undefined;
|
|
57
59
|
/** Path or URL prefixes of the kernel's own runtime files, whose frames a traceback marks as library code. */
|
|
58
60
|
readonly kernelFiles?: readonly string[];
|
|
59
61
|
log(text: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* A fault in the kernel's own code, caught where it would otherwise only
|
|
64
|
+
* be logged: for opt-in crash reports (#422). Never a cell's error.
|
|
65
|
+
*/
|
|
66
|
+
fault?(err: unknown, where: string): void;
|
|
60
67
|
/** After shutdown_reply has gone out. */
|
|
61
68
|
exit(): void;
|
|
62
69
|
}
|
package/dist/kernel-core.js
CHANGED
|
@@ -19,6 +19,8 @@ import { buildTraceback, cellName, TRACEBACK_MIME } from "./traceback.js";
|
|
|
19
19
|
* installed TypeScript's: the major version package.json depends on.
|
|
20
20
|
*/
|
|
21
21
|
export const TYPESCRIPT_VERSION = "6";
|
|
22
|
+
/** Errors from launching a target, which `KernelHost.fault` doesn't get. */
|
|
23
|
+
const launchFailures = new WeakSet();
|
|
22
24
|
function fromBase64(text) {
|
|
23
25
|
return Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
24
26
|
}
|
|
@@ -28,8 +30,46 @@ function fromBase64(text) {
|
|
|
28
30
|
*/
|
|
29
31
|
const INTERRUPT_REPLY_WAIT_MS = 1_000;
|
|
30
32
|
const NO_INPUT = { value: null, interrupted: false };
|
|
33
|
+
/**
|
|
34
|
+
* Shell messages handled one at a time, in order: the main shell, or a
|
|
35
|
+
* kernel subshell (JEP 91, #417), each with its own.
|
|
36
|
+
*/
|
|
37
|
+
class ShellQueue {
|
|
38
|
+
items = [];
|
|
39
|
+
#wake = null;
|
|
40
|
+
#closed = false;
|
|
41
|
+
push(item) {
|
|
42
|
+
if (this.#closed)
|
|
43
|
+
return;
|
|
44
|
+
this.items.push(item);
|
|
45
|
+
this.#wake?.();
|
|
46
|
+
}
|
|
47
|
+
/** The next message; null once closed. */
|
|
48
|
+
async next() {
|
|
49
|
+
for (;;) {
|
|
50
|
+
if (this.#closed)
|
|
51
|
+
return null;
|
|
52
|
+
const item = this.items.shift();
|
|
53
|
+
if (item !== undefined)
|
|
54
|
+
return item;
|
|
55
|
+
await new Promise((resolve) => (this.#wake = resolve));
|
|
56
|
+
this.#wake = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Stops taking messages; returns those still queued. */
|
|
60
|
+
close() {
|
|
61
|
+
this.#closed = true;
|
|
62
|
+
const dropped = this.items.splice(0);
|
|
63
|
+
this.#wake?.();
|
|
64
|
+
return dropped;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
31
67
|
/** Cells remembered for tracebacks: a frame can be in a function any earlier cell defined. */
|
|
32
68
|
const MAX_CELLS = 1000;
|
|
69
|
+
/** The error reply for a subshell that doesn't exist (#417). */
|
|
70
|
+
function subshellNotFound(evalue) {
|
|
71
|
+
return { status: "error", ename: "SubshellNotFound", evalue, traceback: [`SubshellNotFound: ${evalue}`] };
|
|
72
|
+
}
|
|
33
73
|
/** Shell requests other than execute_request, and their reply types. */
|
|
34
74
|
const SHELL_REPLIES = {
|
|
35
75
|
kernel_info_request: "kernel_info_reply",
|
|
@@ -84,13 +124,16 @@ export class KernelCore {
|
|
|
84
124
|
#executionCount = 0;
|
|
85
125
|
#parent = {};
|
|
86
126
|
#allowStdin = false;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#abortQueued = 0;
|
|
127
|
+
/** Cells and comm handlers running in the target. */
|
|
128
|
+
#running = 0;
|
|
129
|
+
/** A cell is running: its parent header and allow_stdin hold until it ends (#417). */
|
|
130
|
+
#cellRunning = false;
|
|
131
|
+
/** stop_on_error (#123): the message being handled was queued behind a failed cell. */
|
|
93
132
|
#aborting = false;
|
|
133
|
+
/** Messages without a subshell_id, in the order they came. */
|
|
134
|
+
#mainShell = new ShellQueue();
|
|
135
|
+
/** Kernel subshells (JEP 91, #417): subshell_id -> its queue. */
|
|
136
|
+
#subshells = new Map();
|
|
94
137
|
/** The input_request waiting for its input_reply, if any. */
|
|
95
138
|
#input = null;
|
|
96
139
|
/** An interrupt is waiting on the target. */
|
|
@@ -117,7 +160,7 @@ export class KernelCore {
|
|
|
117
160
|
// Transforms are synchronous, so the stripper loads before any cell.
|
|
118
161
|
if (typeof this.#typeStrip === "string")
|
|
119
162
|
await loadTypeStripper(this.#typeStrip);
|
|
120
|
-
await Promise.all([this.#
|
|
163
|
+
await Promise.all([this.#readShell(), this.#runShell(this.#mainShell),
|
|
121
164
|
this.#loop("control", (m, p) => this.#handleControl(m, p)),
|
|
122
165
|
this.#loop("stdin", (m) => this.#handleStdin(m))]);
|
|
123
166
|
}
|
|
@@ -141,8 +184,9 @@ export class KernelCore {
|
|
|
141
184
|
targetState = await target.diagnose().catch((err) => `diagnose failed: ${err}`);
|
|
142
185
|
this.#log(`state: ${JSON.stringify({
|
|
143
186
|
shell: handling && { msgType: handling.msgType, msgId: handling.msgId, ms: Date.now() - handling.since },
|
|
144
|
-
queued: this.#channels.shellQueued(),
|
|
145
|
-
|
|
187
|
+
queued: this.#mainShell.items.length + this.#channels.shellQueued(),
|
|
188
|
+
subshells: this.#subshells.size,
|
|
189
|
+
running: this.#running,
|
|
146
190
|
interrupting: this.#interrupting,
|
|
147
191
|
awaitingInput: this.#input !== null,
|
|
148
192
|
target: targetState,
|
|
@@ -175,16 +219,21 @@ export class KernelCore {
|
|
|
175
219
|
const type = msg.header.msg_type;
|
|
176
220
|
const commId = String(msg.content.comm_id ?? "");
|
|
177
221
|
const target = await this.#ensureTarget();
|
|
178
|
-
// Kernel messages the handlers send are replies to this message
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
this.#
|
|
222
|
+
// Kernel messages the handlers send are replies to this message, unless
|
|
223
|
+
// it came on a subshell while a cell runs (#417): the target's output
|
|
224
|
+
// doesn't say which it came from, so it stays the cell's.
|
|
225
|
+
if (!this.#cellRunning) {
|
|
226
|
+
this.#parent = msg.header;
|
|
227
|
+
this.#allowStdin = false;
|
|
228
|
+
}
|
|
229
|
+
const parent = msg.header;
|
|
182
230
|
let handled = false;
|
|
231
|
+
this.#running++;
|
|
183
232
|
try {
|
|
184
233
|
const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
|
|
185
234
|
if ("error" in result) {
|
|
186
235
|
const { name, message, stack } = result.error;
|
|
187
|
-
this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` });
|
|
236
|
+
this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` }, { parent });
|
|
188
237
|
}
|
|
189
238
|
else {
|
|
190
239
|
handled = result.handled;
|
|
@@ -193,10 +242,10 @@ export class KernelCore {
|
|
|
193
242
|
catch (err) {
|
|
194
243
|
if (!(err instanceof Interrupted))
|
|
195
244
|
throw err;
|
|
196
|
-
this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` });
|
|
245
|
+
this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` }, { parent });
|
|
197
246
|
}
|
|
198
247
|
finally {
|
|
199
|
-
this.#
|
|
248
|
+
this.#running--;
|
|
200
249
|
}
|
|
201
250
|
await target.flush().catch(() => { });
|
|
202
251
|
if (type === "comm_open") {
|
|
@@ -206,7 +255,7 @@ export class KernelCore {
|
|
|
206
255
|
else {
|
|
207
256
|
// An unknown target, or a handler that failed: the protocol asks the
|
|
208
257
|
// kernel to close the comm so the frontend doesn't wait on it.
|
|
209
|
-
this.#publish("comm_close", { comm_id: commId, data: {} });
|
|
258
|
+
this.#publish("comm_close", { comm_id: commId, data: {} }, { parent });
|
|
210
259
|
}
|
|
211
260
|
}
|
|
212
261
|
else if (type === "comm_close") {
|
|
@@ -228,6 +277,10 @@ export class KernelCore {
|
|
|
228
277
|
return target;
|
|
229
278
|
}, (err) => {
|
|
230
279
|
this.#targetStarting = null;
|
|
280
|
+
// Usually the user's environment (a missing runtime, a bad config),
|
|
281
|
+
// not a kernel fault, and its message quotes the child's stderr.
|
|
282
|
+
if (typeof err === "object" && err !== null)
|
|
283
|
+
launchFailures.add(err);
|
|
231
284
|
throw err;
|
|
232
285
|
});
|
|
233
286
|
return this.#targetStarting;
|
|
@@ -249,10 +302,19 @@ export class KernelCore {
|
|
|
249
302
|
this.#interruptStart = null;
|
|
250
303
|
}
|
|
251
304
|
}
|
|
305
|
+
/** Tells the host of a fault in the kernel's own code; a target that failed to launch isn't one (#422). */
|
|
306
|
+
#fault(err, where) {
|
|
307
|
+
if (typeof err === "object" && err !== null && launchFailures.has(err))
|
|
308
|
+
return;
|
|
309
|
+
this.#host.fault?.(err, where);
|
|
310
|
+
}
|
|
252
311
|
#targetEvents() {
|
|
253
312
|
return {
|
|
254
313
|
stream: (name, text) => this.#publish("stream", { name, text }),
|
|
255
|
-
message: (msg) => void this.#onTargetMessage(msg).catch((err) =>
|
|
314
|
+
message: (msg) => void this.#onTargetMessage(msg).catch((err) => {
|
|
315
|
+
this.#log(`error handling target message: ${err}`);
|
|
316
|
+
this.#fault(err, "target message");
|
|
317
|
+
}),
|
|
256
318
|
exit: (code, note) => {
|
|
257
319
|
this.#target = null;
|
|
258
320
|
// What its cells declared died with it.
|
|
@@ -302,7 +364,7 @@ export class KernelCore {
|
|
|
302
364
|
this.#interruptStart();
|
|
303
365
|
return;
|
|
304
366
|
}
|
|
305
|
-
if (!this.#target ||
|
|
367
|
+
if (!this.#target || this.#running === 0)
|
|
306
368
|
return;
|
|
307
369
|
this.#interrupting = true;
|
|
308
370
|
try {
|
|
@@ -334,27 +396,85 @@ export class KernelCore {
|
|
|
334
396
|
async #loop(channel, handle) {
|
|
335
397
|
while (!this.#shuttingDown) {
|
|
336
398
|
const received = await this.#channels.recv(channel);
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
399
|
+
// Malformed, or on ZeroMQ unsigned or forged: dropped, as in Deno.
|
|
400
|
+
if (received === null)
|
|
401
|
+
continue;
|
|
402
|
+
try {
|
|
403
|
+
await handle(received.msg, received.peer);
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
|
|
407
|
+
this.#fault(err, `${channel} handler`);
|
|
343
408
|
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Takes every shell message as it comes and queues it for the main shell
|
|
413
|
+
* or its subshell, so a subshell's message isn't stuck behind a running
|
|
414
|
+
* cell (#417).
|
|
415
|
+
*/
|
|
416
|
+
async #readShell() {
|
|
417
|
+
while (!this.#shuttingDown) {
|
|
418
|
+
const received = await this.#channels.recv("shell");
|
|
344
419
|
// Malformed, or on ZeroMQ unsigned or forged: dropped, as in Deno.
|
|
345
420
|
if (received === null)
|
|
346
421
|
continue;
|
|
347
|
-
const
|
|
422
|
+
const subshellId = received.msg.header.subshell_id;
|
|
423
|
+
if (typeof subshellId !== "string") {
|
|
424
|
+
this.#mainShell.push({ ...received, abort: false });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const queue = this.#subshells.get(subshellId);
|
|
428
|
+
if (queue === undefined) {
|
|
429
|
+
this.#refuseShell(received, `No subshell with id ${subshellId}`);
|
|
430
|
+
}
|
|
431
|
+
else if (received.msg.header.msg_type === "execute_request") {
|
|
432
|
+
// Cells share one target and one parent header, so a cell sent on
|
|
433
|
+
// a subshell waits its turn on the main shell.
|
|
434
|
+
this.#mainShell.push({ ...received, abort: false });
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
queue.push({ ...received, abort: false });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Answers a shell message for a subshell that doesn't exist, or was
|
|
443
|
+
* deleted with the message queued, so the frontend doesn't wait on it.
|
|
444
|
+
*/
|
|
445
|
+
#refuseShell({ peer, msg }, evalue) {
|
|
446
|
+
const type = msg.header.msg_type;
|
|
447
|
+
this.#log(`refusing ${type}: ${evalue}`);
|
|
448
|
+
this.#status("busy", msg.header);
|
|
449
|
+
const replyType = type === "execute_request" ? "execute_reply" : SHELL_REPLIES[type];
|
|
450
|
+
if (replyType !== undefined)
|
|
451
|
+
this.#reply("shell", peer, msg, replyType, subshellNotFound(evalue));
|
|
452
|
+
// As for an unknown target: the frontend shouldn't wait on the comm.
|
|
453
|
+
if (type === "comm_open")
|
|
454
|
+
this.#publish("comm_close", { comm_id: String(msg.content.comm_id ?? ""), data: {} }, { parent: msg.header });
|
|
455
|
+
this.#status("idle", msg.header);
|
|
456
|
+
}
|
|
457
|
+
/** Handles one queue's messages in turn. */
|
|
458
|
+
async #runShell(queue) {
|
|
459
|
+
const main = queue === this.#mainShell;
|
|
460
|
+
while (!this.#shuttingDown) {
|
|
461
|
+
const queued = await queue.next();
|
|
462
|
+
if (queued === null)
|
|
463
|
+
return;
|
|
464
|
+
const { peer, msg } = queued;
|
|
348
465
|
try {
|
|
349
|
-
if (
|
|
466
|
+
if (main) {
|
|
467
|
+
this.#aborting = queued.abort;
|
|
350
468
|
this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
|
|
351
|
-
|
|
469
|
+
}
|
|
470
|
+
await this.#handleShell(msg, peer);
|
|
352
471
|
}
|
|
353
472
|
catch (err) {
|
|
354
473
|
this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
|
|
474
|
+
this.#fault(err, "shell handler");
|
|
355
475
|
}
|
|
356
476
|
finally {
|
|
357
|
-
if (
|
|
477
|
+
if (main)
|
|
358
478
|
this.#handling = null;
|
|
359
479
|
}
|
|
360
480
|
}
|
|
@@ -369,7 +489,12 @@ export class KernelCore {
|
|
|
369
489
|
language_info: {
|
|
370
490
|
name: "typescript",
|
|
371
491
|
version: this.#host.languageVersion ?? TYPESCRIPT_VERSION,
|
|
372
|
-
|
|
492
|
+
// JupyterLab's LSP adapter picks the language server by a language
|
|
493
|
+
// it derives from this MIME type, not from name: it drops the type
|
|
494
|
+
// and an "x-" prefix, so "text/x.typescript" became "x.typescript"
|
|
495
|
+
// and matched no server (#400). This is CodeMirror's MIME type for
|
|
496
|
+
// TypeScript and in jupyter-lsp's typescript-language-server spec.
|
|
497
|
+
mimetype: "application/typescript",
|
|
373
498
|
file_extension: ".ts",
|
|
374
499
|
pygments_lexer: "typescript",
|
|
375
500
|
codemirror_mode: "typescript",
|
|
@@ -381,12 +506,22 @@ export class KernelCore {
|
|
|
381
506
|
debugger: this.#debugger !== null,
|
|
382
507
|
// What current ipykernel advertises; JupyterLab itself goes by the
|
|
383
508
|
// kernelspec's metadata.debugger.
|
|
384
|
-
supported_features: this.#debugger !== null ? ["debugger"] : [],
|
|
509
|
+
supported_features: [...(this.#debugger !== null ? ["debugger"] : []), "kernel subshells"],
|
|
385
510
|
...(this.#host.capabilities ? { capabilities: this.#host.capabilities } : {}),
|
|
386
511
|
};
|
|
387
512
|
}
|
|
388
513
|
// --- control channel ------------------------------------------------------
|
|
389
514
|
async #handleControl(msg, peer) {
|
|
515
|
+
// A frontend polls usage, and measuring can take a moment (`ps` on
|
|
516
|
+
// macOS): answer it beside the queue, so an interrupt or shutdown
|
|
517
|
+
// never waits behind a poll (#409).
|
|
518
|
+
if (msg.header.msg_type === "usage_request") {
|
|
519
|
+
void this.#handleControlRequest(msg, peer).catch((err) => this.#log(`usage_request failed: ${err}`));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
return this.#handleControlRequest(msg, peer);
|
|
523
|
+
}
|
|
524
|
+
async #handleControlRequest(msg, peer) {
|
|
390
525
|
// Every request is wrapped in busy/idle, as on shell (#124). JupyterLab
|
|
391
526
|
// treats a debug request as done only once the kernel reports idle for
|
|
392
527
|
// it. (Jupyter Server leaves debug_request out of the kernel's status.)
|
|
@@ -412,6 +547,42 @@ export class KernelCore {
|
|
|
412
547
|
]);
|
|
413
548
|
this.#reply(channel, peer, msg, "interrupt_reply", { status: "ok" });
|
|
414
549
|
break;
|
|
550
|
+
case "create_subshell_request": {
|
|
551
|
+
const subshellId = crypto.randomUUID();
|
|
552
|
+
const queue = new ShellQueue();
|
|
553
|
+
this.#subshells.set(subshellId, queue);
|
|
554
|
+
void this.#runShell(queue);
|
|
555
|
+
this.#reply(channel, peer, msg, "create_subshell_reply", { status: "ok", subshell_id: subshellId });
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
case "delete_subshell_request": {
|
|
559
|
+
const subshellId = String(msg.content.subshell_id ?? "");
|
|
560
|
+
const queue = this.#subshells.get(subshellId);
|
|
561
|
+
if (queue === undefined) {
|
|
562
|
+
this.#reply(channel, peer, msg, "delete_subshell_reply", subshellNotFound(`No subshell with id ${subshellId}`));
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
// A message it is handling finishes; what's queued is refused.
|
|
566
|
+
this.#subshells.delete(subshellId);
|
|
567
|
+
for (const dropped of queue.close())
|
|
568
|
+
this.#refuseShell(dropped, `Subshell ${subshellId} was deleted`);
|
|
569
|
+
this.#reply(channel, peer, msg, "delete_subshell_reply", { status: "ok" });
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case "list_subshell_request":
|
|
573
|
+
this.#reply(channel, peer, msg, "list_subshell_reply", { status: "ok", subshell_id: [...this.#subshells.keys()] });
|
|
574
|
+
break;
|
|
575
|
+
case "usage_request": {
|
|
576
|
+
// Without starting a target: a frontend polls this (#409). A host
|
|
577
|
+
// that can't measure anything still answers, so the frontend
|
|
578
|
+
// doesn't wait out its timeout.
|
|
579
|
+
const usage = this.#host.usage?.(this.#target).catch((err) => {
|
|
580
|
+
this.#log(`usage_request failed: ${err}`);
|
|
581
|
+
return {};
|
|
582
|
+
});
|
|
583
|
+
this.#reply(channel, peer, msg, "usage_reply", (await usage) ?? {});
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
415
586
|
case "debug_request": {
|
|
416
587
|
let reply;
|
|
417
588
|
if (this.#debugger === null) {
|
|
@@ -745,7 +916,8 @@ export class KernelCore {
|
|
|
745
916
|
let userExpressions = {};
|
|
746
917
|
// nteract's structured traceback, sent as the error message's metadata (#322).
|
|
747
918
|
let structured = null;
|
|
748
|
-
this.#
|
|
919
|
+
this.#running++;
|
|
920
|
+
this.#cellRunning = true;
|
|
749
921
|
try {
|
|
750
922
|
const target = await this.#targetForCell();
|
|
751
923
|
if (target.notice !== undefined && this.#noticeShownFor !== target) {
|
|
@@ -796,7 +968,8 @@ export class KernelCore {
|
|
|
796
968
|
}
|
|
797
969
|
}
|
|
798
970
|
finally {
|
|
799
|
-
this.#
|
|
971
|
+
this.#running--;
|
|
972
|
+
this.#cellRunning = false;
|
|
800
973
|
}
|
|
801
974
|
// The result and error are shown: nothing refers to them any more (#286).
|
|
802
975
|
await this.#target?.releaseResults?.();
|
|
@@ -805,10 +978,14 @@ export class KernelCore {
|
|
|
805
978
|
// Abort what was queued behind this cell (JupyterLab's Run All sends
|
|
806
979
|
// every cell at once), but nothing sent after the client sees the
|
|
807
980
|
// error. A macrotask first lets bytes already received be parsed.
|
|
981
|
+
// Shell messages are queued as they're read, so by then each is.
|
|
808
982
|
await new Promise((r) => setTimeout(r, 0));
|
|
809
|
-
|
|
983
|
+
for (const queued of this.#mainShell.items)
|
|
984
|
+
queued.abort = true;
|
|
810
985
|
}
|
|
811
|
-
|
|
986
|
+
// With its parent: a subshell's comm handler may have taken #parent
|
|
987
|
+
// since the cell ended (#417).
|
|
988
|
+
this.#publish("error", error, { parent, ...(structured ? { metadata: { [TRACEBACK_MIME]: structured } } : {}) });
|
|
812
989
|
this.#reply("shell", peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
|
|
813
990
|
}
|
|
814
991
|
else {
|
package/dist/kernel.js
CHANGED
|
@@ -18,6 +18,8 @@ import { cacheRoot, VERSION } from "./package.js";
|
|
|
18
18
|
import { watchParent } from "./parent.js";
|
|
19
19
|
import { KernelCore } from "./kernel-core.js";
|
|
20
20
|
import { ZmqChannels } from "./zmq-channels.js";
|
|
21
|
+
import { UsageMeter } from "./usage.js";
|
|
22
|
+
import { crashReporter, reportUncaught } from "./crash-reports.js";
|
|
21
23
|
/** The installed TypeScript's version, for kernel_info's language_info (#340). */
|
|
22
24
|
function typescriptVersion() {
|
|
23
25
|
try {
|
|
@@ -141,6 +143,12 @@ export class Kernel {
|
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
const denoCompat = options.denoCompat ?? false;
|
|
146
|
+
const usage = new UsageMeter();
|
|
147
|
+
// Opt-in reports of the kernel's own faults (#422): null, and nothing
|
|
148
|
+
// installed, unless CDP_KERNEL_CRASH_REPORTS=1.
|
|
149
|
+
const reporter = crashReporter({ target: runtime });
|
|
150
|
+
if (reporter)
|
|
151
|
+
reportUncaught(reporter);
|
|
144
152
|
this.#core = new KernelCore({
|
|
145
153
|
runtime,
|
|
146
154
|
version: VERSION,
|
|
@@ -164,9 +172,11 @@ export class Kernel {
|
|
|
164
172
|
createDebugger: runtime === "node" || runtime === "deno"
|
|
165
173
|
? (publish, transformOptions) => new Debugger(publish, transformOptions)
|
|
166
174
|
: undefined,
|
|
175
|
+
usage: (target) => usage.reply(target),
|
|
167
176
|
readSource,
|
|
168
177
|
kernelFiles: kernelFiles(),
|
|
169
178
|
log,
|
|
179
|
+
fault: reporter ? (err, where) => void reporter.report(err, where) : undefined,
|
|
170
180
|
exit: () => process.exit(0),
|
|
171
181
|
});
|
|
172
182
|
}
|