@tlockney/cdp-kernel 0.4.0 → 0.5.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 +60 -481
- 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 +6 -1
- package/dist/cdp.js +25 -10
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +46 -8
- package/dist/children.d.ts +7 -0
- package/dist/children.js +32 -0
- package/dist/debugger.d.ts +1 -1
- package/dist/debugger.js +21 -10
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.js +163 -41
- package/dist/mod.d.ts +5 -3
- package/dist/mod.js +5 -3
- package/dist/package.d.ts +2 -2
- package/dist/package.js +3 -3
- package/dist/target.d.ts +31 -3
- package/dist/target.js +233 -32
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +9 -0
- package/dist/transform.js +37 -9
- 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/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +7 -1
- package/dist/zmtp.js +30 -1
- package/package.json +9 -3
- package/runtime/bootstrap.mjs +121 -10
- 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/node-hooks.d.mts +42 -0
- package/runtime/node-hooks.mjs +246 -41
- package/runtime/workerd-worker.mjs +77 -6
package/dist/bun-target.js
CHANGED
|
@@ -9,9 +9,11 @@ import { Buffer } from "node:buffer";
|
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
import { asEvaluateResult } from "./cdp.js";
|
|
12
13
|
import { transformCellForEval } from "./transform.js";
|
|
13
14
|
import { runtimeDir } from "./package.js";
|
|
14
|
-
import {
|
|
15
|
+
import { killOnExit } from "./children.js";
|
|
16
|
+
import { Gates, Interrupted, isTargetMessage, parseJson, PipeDemux } from "./target.js";
|
|
15
17
|
/**
|
|
16
18
|
* How long an interrupted target has to answer a ping. A cell waiting on a
|
|
17
19
|
* promise leaves the event loop free, so the answer is near-instant; a busy
|
|
@@ -45,6 +47,9 @@ export class BunTarget {
|
|
|
45
47
|
#gates = new Gates();
|
|
46
48
|
#pending = new Map();
|
|
47
49
|
#nextId = 0;
|
|
50
|
+
/** Ids of requests interrupt() may cut short: eval and comm. */
|
|
51
|
+
#interruptibleIds = new Set();
|
|
52
|
+
#exitedOnce = false;
|
|
48
53
|
#messages = Promise.resolve();
|
|
49
54
|
#interrupted = false;
|
|
50
55
|
#stopNote;
|
|
@@ -60,6 +65,7 @@ export class BunTarget {
|
|
|
60
65
|
const env = { ...process.env, FORCE_COLOR: "1" };
|
|
61
66
|
delete env.NO_COLOR;
|
|
62
67
|
const child = spawn(cmd, args, { cwd: options.cwd, stdio: ["pipe", "pipe", "pipe", "pipe"], env });
|
|
68
|
+
killOnExit(child);
|
|
63
69
|
const channel = child.stdio[3];
|
|
64
70
|
if (!child.stdout || !child.stderr || !isDuplex(channel)) {
|
|
65
71
|
child.kill();
|
|
@@ -97,7 +103,8 @@ export class BunTarget {
|
|
|
97
103
|
this.#channel.on("data", (chunk) => {
|
|
98
104
|
buffer += chunk;
|
|
99
105
|
for (let nl = buffer.indexOf("\n"); nl !== -1; nl = buffer.indexOf("\n")) {
|
|
100
|
-
|
|
106
|
+
// Cells can write here through __cdpk_send too (#122).
|
|
107
|
+
const msg = parseJson(buffer.slice(0, nl));
|
|
101
108
|
buffer = buffer.slice(nl + 1);
|
|
102
109
|
if (isTargetMessage(msg))
|
|
103
110
|
this.#deliver(msg, events);
|
|
@@ -116,12 +123,14 @@ export class BunTarget {
|
|
|
116
123
|
if (!pending)
|
|
117
124
|
return; // abandoned
|
|
118
125
|
this.#pending.delete(id);
|
|
126
|
+
this.#interruptibleIds.delete(id);
|
|
119
127
|
if (typeof reply.failure === "string")
|
|
120
128
|
pending.reject(new Error(reply.failure));
|
|
121
129
|
else
|
|
122
130
|
pending.resolve(reply);
|
|
123
131
|
}
|
|
124
132
|
#exited(code, events) {
|
|
133
|
+
this.#exitedOnce = true;
|
|
125
134
|
this.#gates.openAll();
|
|
126
135
|
this.#stdout.close();
|
|
127
136
|
this.#stderr.close();
|
|
@@ -132,6 +141,7 @@ export class BunTarget {
|
|
|
132
141
|
for (const pending of this.#pending.values())
|
|
133
142
|
pending.reject(new Abandoned(reason));
|
|
134
143
|
this.#pending.clear();
|
|
144
|
+
this.#interruptibleIds.clear();
|
|
135
145
|
}
|
|
136
146
|
// Binding messages wait until both pipes have caught up to their marker,
|
|
137
147
|
// and are delivered strictly in order (as in Target).
|
|
@@ -142,10 +152,15 @@ export class BunTarget {
|
|
|
142
152
|
this.#gates.open(msg.barrier);
|
|
143
153
|
});
|
|
144
154
|
}
|
|
145
|
-
#request(op, params = {}) {
|
|
155
|
+
#request(op, params = {}, interruptible = false) {
|
|
156
|
+
// Bun is gone; a write to its channel would never be answered.
|
|
157
|
+
if (this.#exitedOnce)
|
|
158
|
+
return Promise.reject(new Abandoned("the bun session is closed"));
|
|
146
159
|
const id = ++this.#nextId;
|
|
147
160
|
return new Promise((resolve, reject) => {
|
|
148
161
|
this.#pending.set(id, { resolve, reject });
|
|
162
|
+
if (interruptible)
|
|
163
|
+
this.#interruptibleIds.add(id);
|
|
149
164
|
this.#channel.write(JSON.stringify({ id, op, ...params }) + "\n");
|
|
150
165
|
});
|
|
151
166
|
}
|
|
@@ -153,7 +168,7 @@ export class BunTarget {
|
|
|
153
168
|
async #interruptible(op, params, what) {
|
|
154
169
|
this.#interrupted = false;
|
|
155
170
|
try {
|
|
156
|
-
return await this.#request(op, params);
|
|
171
|
+
return await this.#request(op, params, true);
|
|
157
172
|
}
|
|
158
173
|
catch (err) {
|
|
159
174
|
if (err instanceof Abandoned || this.#interrupted)
|
|
@@ -162,7 +177,7 @@ export class BunTarget {
|
|
|
162
177
|
}
|
|
163
178
|
}
|
|
164
179
|
async evaluate(script) {
|
|
165
|
-
return (await this.#interruptible("eval", { code: script }, "Execution"));
|
|
180
|
+
return asEvaluateResult(await this.#interruptible("eval", { code: script }, "Execution"));
|
|
166
181
|
}
|
|
167
182
|
/**
|
|
168
183
|
* There's no terminateExecution, but a cell that's waiting on a promise
|
|
@@ -173,12 +188,19 @@ export class BunTarget {
|
|
|
173
188
|
*/
|
|
174
189
|
async interrupt() {
|
|
175
190
|
this.#interrupted = true;
|
|
191
|
+
const running = [...this.#interruptibleIds];
|
|
176
192
|
const answered = await Promise.race([
|
|
177
193
|
this.#request("ping").then(() => true, () => false),
|
|
178
194
|
new Promise((resolve) => setTimeout(() => resolve(false), INTERRUPT_GRACE_MS)),
|
|
179
195
|
]);
|
|
180
196
|
if (answered) {
|
|
181
|
-
|
|
197
|
+
// Only the cell or comm handler: a barrier or broadcast request in
|
|
198
|
+
// flight would otherwise fail as a KernelError (#135).
|
|
199
|
+
for (const id of running) {
|
|
200
|
+
this.#pending.get(id)?.reject(new Abandoned("interrupted"));
|
|
201
|
+
this.#pending.delete(id);
|
|
202
|
+
this.#interruptibleIds.delete(id);
|
|
203
|
+
}
|
|
182
204
|
return;
|
|
183
205
|
}
|
|
184
206
|
this.#stopNote = RESTART_NOTE;
|
|
@@ -227,8 +249,8 @@ export class BunTarget {
|
|
|
227
249
|
lexicalNames() {
|
|
228
250
|
return Promise.resolve([]);
|
|
229
251
|
}
|
|
230
|
-
replyInput(value) {
|
|
231
|
-
this.#child.stdin?.write(JSON.stringify({ value }) + "\n");
|
|
252
|
+
replyInput(value, interrupted = false) {
|
|
253
|
+
this.#child.stdin?.write(JSON.stringify({ value, interrupted }) + "\n");
|
|
232
254
|
}
|
|
233
255
|
kill() {
|
|
234
256
|
this.#child.kill();
|
package/dist/cdp.d.ts
CHANGED
|
@@ -29,7 +29,6 @@ export declare class CdpAbandoned extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
export declare class CdpSession {
|
|
31
31
|
#private;
|
|
32
|
-
readonly closed: Promise<void>;
|
|
33
32
|
private constructor();
|
|
34
33
|
static connect(url: string): Promise<CdpSession>;
|
|
35
34
|
on(method: string, listener: (params: CdpParams) => void): void;
|
|
@@ -38,4 +37,10 @@ export declare class CdpSession {
|
|
|
38
37
|
abandonPending(): void;
|
|
39
38
|
close(): void;
|
|
40
39
|
}
|
|
40
|
+
export declare function isRecord(value: unknown): value is CdpParams;
|
|
41
|
+
export declare function isRemoteObject(value: unknown): value is RemoteObject;
|
|
42
|
+
/**
|
|
43
|
+
* A Runtime.evaluate-shaped reply, checked: a value, or exceptionDetails.
|
|
44
|
+
* The Bun and workerd drivers send no `result` with a thrown exception.
|
|
45
|
+
*/
|
|
41
46
|
export declare function asEvaluateResult(raw: CdpParams): EvaluateResult;
|
package/dist/cdp.js
CHANGED
|
@@ -21,7 +21,6 @@ export class CdpSession {
|
|
|
21
21
|
#nextId = 0;
|
|
22
22
|
#pending = new Map();
|
|
23
23
|
#listeners = new Map();
|
|
24
|
-
closed;
|
|
25
24
|
constructor(ws) {
|
|
26
25
|
this.#ws = ws;
|
|
27
26
|
ws.addEventListener("message", (event) => {
|
|
@@ -43,14 +42,11 @@ export class CdpSession {
|
|
|
43
42
|
listener(msg.params);
|
|
44
43
|
}
|
|
45
44
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.#pending.clear();
|
|
52
|
-
resolve();
|
|
53
|
-
});
|
|
45
|
+
ws.addEventListener("close", () => {
|
|
46
|
+
for (const pending of this.#pending.values()) {
|
|
47
|
+
pending.reject(new Error("CDP connection closed"));
|
|
48
|
+
}
|
|
49
|
+
this.#pending.clear();
|
|
54
50
|
});
|
|
55
51
|
}
|
|
56
52
|
static async connect(url) {
|
|
@@ -67,6 +63,10 @@ export class CdpSession {
|
|
|
67
63
|
this.#listeners.set(method, list);
|
|
68
64
|
}
|
|
69
65
|
send(method, params = {}) {
|
|
66
|
+
// send() on a closing or closed socket drops the frame silently, so
|
|
67
|
+
// nothing would ever settle the call (#133).
|
|
68
|
+
if (this.#ws.readyState !== WebSocket.OPEN)
|
|
69
|
+
return Promise.reject(new Error("CDP connection closed"));
|
|
70
70
|
const id = ++this.#nextId;
|
|
71
71
|
return new Promise((resolve, reject) => {
|
|
72
72
|
this.#pending.set(id, { method, resolve, reject });
|
|
@@ -84,6 +84,21 @@ export class CdpSession {
|
|
|
84
84
|
this.#ws.close();
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
export function isRecord(value) {
|
|
88
|
+
return typeof value === "object" && value !== null;
|
|
89
|
+
}
|
|
90
|
+
export function isRemoteObject(value) {
|
|
91
|
+
return isRecord(value) && typeof value.type === "string";
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A Runtime.evaluate-shaped reply, checked: a value, or exceptionDetails.
|
|
95
|
+
* The Bun and workerd drivers send no `result` with a thrown exception.
|
|
96
|
+
*/
|
|
87
97
|
export function asEvaluateResult(raw) {
|
|
88
|
-
|
|
98
|
+
const details = raw.exceptionDetails;
|
|
99
|
+
if (details !== undefined ? !isRecord(details) || typeof details.text !== "string" : !isRemoteObject(raw.result)) {
|
|
100
|
+
throw new Error(`the target sent a malformed evaluate result: ${JSON.stringify(raw).slice(0, 200)}`);
|
|
101
|
+
}
|
|
102
|
+
const result = isRemoteObject(raw.result) ? raw.result : { type: "undefined" };
|
|
103
|
+
return details === undefined ? { result } : { result, exceptionDetails: details };
|
|
89
104
|
}
|
package/dist/celld-target.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CdpParams } from "./cdp.ts";
|
|
2
2
|
import type { TargetEvents } from "./target.ts";
|
|
3
3
|
import { WorkerdTarget } from "./workerd-target.ts";
|
|
4
4
|
/** The project keys the generated config keeps: bindings and compatibility. */
|
package/dist/celld-target.js
CHANGED
|
@@ -21,8 +21,10 @@ import { createServer } from "node:net";
|
|
|
21
21
|
import { homedir, tmpdir } from "node:os";
|
|
22
22
|
import { dirname, join, resolve } from "node:path";
|
|
23
23
|
import { build } from "esbuild";
|
|
24
|
+
import { isRecord } from "./cdp.js";
|
|
24
25
|
import { parseJsonc } from "./deno-config.js";
|
|
25
26
|
import { runtimeDir } from "./package.js";
|
|
27
|
+
import { killOnExit } from "./children.js";
|
|
26
28
|
import { cellMeta, WorkerdTarget } from "./workerd-target.js";
|
|
27
29
|
import { builtinAliases, findWranglerConfig } from "./workers-project.js";
|
|
28
30
|
/** The project keys the generated config keeps: bindings and compatibility. */
|
|
@@ -141,6 +143,8 @@ function killTree(child) {
|
|
|
141
143
|
}
|
|
142
144
|
child.kill("SIGKILL");
|
|
143
145
|
}
|
|
146
|
+
/** Starts tried when a port is taken (see CelldHost.start). */
|
|
147
|
+
const PORT_ATTEMPTS = 3;
|
|
144
148
|
/** The driver on `celld dev`. */
|
|
145
149
|
class CelldHost {
|
|
146
150
|
runtime;
|
|
@@ -153,7 +157,23 @@ class CelldHost {
|
|
|
153
157
|
this.runtime = runtime;
|
|
154
158
|
this.project = project;
|
|
155
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* celld dev, retried when a port is taken: both its --port (from
|
|
162
|
+
* freePort) and its node's own listener are picked free and bound later,
|
|
163
|
+
* so another process can take one in between (#54).
|
|
164
|
+
*/
|
|
156
165
|
static async start(cwd, projectConfig, executable) {
|
|
166
|
+
for (let attempt = 1;; attempt++) {
|
|
167
|
+
try {
|
|
168
|
+
return await CelldHost.#start(cwd, projectConfig, executable);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (attempt >= PORT_ATTEMPTS || !(err instanceof Error && /Address already in use/.test(err.message)))
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
static async #start(cwd, projectConfig, executable) {
|
|
157
177
|
const celld = executable ?? findCelld();
|
|
158
178
|
const version = celldVersionOf(execFileSync(celld, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
|
|
159
179
|
const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
|
|
@@ -163,7 +183,7 @@ class CelldHost {
|
|
|
163
183
|
if (projectConfig.endsWith(".toml"))
|
|
164
184
|
throw new Error("celld reads wrangler.jsonc or wrangler.json, not wrangler.toml");
|
|
165
185
|
const parsed = parseJsonc(readFileSync(projectConfig, "utf8"));
|
|
166
|
-
if (
|
|
186
|
+
if (!isRecord(parsed) || Array.isArray(parsed))
|
|
167
187
|
throw new Error(`${projectConfig} is not an object`);
|
|
168
188
|
project = parsed;
|
|
169
189
|
dir = join(dirname(projectConfig), ".celld", "cdp-kernel");
|
|
@@ -173,9 +193,14 @@ class CelldHost {
|
|
|
173
193
|
dir = mkdtempSync(join(tmpdir(), "cdpk-celld-"));
|
|
174
194
|
}
|
|
175
195
|
const main = project && typeof project.main === "string" ? resolve(dirname(projectConfig ?? cwd), project.main) : undefined;
|
|
176
|
-
const define =
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
const define = {};
|
|
197
|
+
if (project && isRecord(project.define)) {
|
|
198
|
+
for (const [name, value] of Object.entries(project.define)) {
|
|
199
|
+
if (typeof value !== "string")
|
|
200
|
+
throw new Error(`define.${name} in ${projectConfig} must be a string`);
|
|
201
|
+
define[name] = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
179
204
|
const token = randomBytes(32).toString("hex");
|
|
180
205
|
// 0600: the config carries this launch's token.
|
|
181
206
|
writeFileSync(join(dir, "driver.mjs"), await bundleDriver(source, main, define));
|
|
@@ -186,25 +211,35 @@ class CelldHost {
|
|
|
186
211
|
const child = spawn(celld, ["dev", dir, "--host", "127.0.0.1", "--port", String(port), "--no-watch"], {
|
|
187
212
|
stdio: ["ignore", "pipe", "pipe"],
|
|
188
213
|
});
|
|
189
|
-
|
|
214
|
+
const removeFiles = () => {
|
|
190
215
|
if (projectConfig === null)
|
|
191
216
|
rmSync(dir, { recursive: true, force: true });
|
|
192
217
|
else
|
|
193
218
|
for (const name of ["driver.mjs", "wrangler.jsonc"])
|
|
194
219
|
rmSync(join(dir, name), { force: true });
|
|
220
|
+
};
|
|
221
|
+
child.once("exit", removeFiles);
|
|
222
|
+
killOnExit(child, () => {
|
|
223
|
+
killTree(child);
|
|
224
|
+
removeFiles();
|
|
195
225
|
});
|
|
196
226
|
// Cell output is captured in the driver; celld's own lines go to the
|
|
197
227
|
// server log.
|
|
228
|
+
// `early` keeps that output for a startup error only, and stops growing
|
|
229
|
+
// once the session is open.
|
|
198
230
|
let early = "";
|
|
231
|
+
let starting = true;
|
|
199
232
|
const log = (chunk) => {
|
|
200
|
-
|
|
233
|
+
if (starting)
|
|
234
|
+
early += chunk.toString();
|
|
201
235
|
process.stderr.write(`[cdp-kernel celld] ${chunk}`);
|
|
202
236
|
};
|
|
203
237
|
child.stdout?.on("data", log);
|
|
204
238
|
child.stderr?.on("data", log);
|
|
205
239
|
const exited = new Promise((_, reject) => {
|
|
206
240
|
child.once("error", reject);
|
|
207
|
-
|
|
241
|
+
// On close, not exit, so its last output is in the message.
|
|
242
|
+
child.once("close", (code) => reject(new Error(`celld exited (${code}) before it was ready:\n${early}`)));
|
|
208
243
|
});
|
|
209
244
|
exited.catch(() => { });
|
|
210
245
|
const url = `127.0.0.1:${port}/?token=${token}`;
|
|
@@ -226,7 +261,10 @@ class CelldHost {
|
|
|
226
261
|
return channelOf(ws);
|
|
227
262
|
})();
|
|
228
263
|
try {
|
|
229
|
-
|
|
264
|
+
const host = new CelldHost(child, await Promise.race([ready, exited]), `celld ${version}`.trim(), projectConfig ?? undefined);
|
|
265
|
+
starting = false;
|
|
266
|
+
early = "";
|
|
267
|
+
return host;
|
|
230
268
|
}
|
|
231
269
|
catch (err) {
|
|
232
270
|
killTree(child);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Kills `child` if it's still running when this process exits. `cleanup`
|
|
4
|
+
* replaces the default SIGKILL; it must be synchronous, as exit hooks are,
|
|
5
|
+
* and do whatever the child's own exit listener would have done.
|
|
6
|
+
*/
|
|
7
|
+
export declare function killOnExit(child: ChildProcess, cleanup?: () => void): void;
|
package/dist/children.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Runtimes this process started, killed when it exits however it exits
|
|
2
|
+
// short of SIGKILL: a target left behind keeps running, reparented to pid 1,
|
|
3
|
+
// with whatever temp directory it uses (#127, #218).
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
const cleanups = new Set();
|
|
6
|
+
function runAll() {
|
|
7
|
+
for (const cleanup of cleanups) {
|
|
8
|
+
try {
|
|
9
|
+
cleanup();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Best effort: the process is exiting.
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
cleanups.clear();
|
|
16
|
+
}
|
|
17
|
+
let hooked = false;
|
|
18
|
+
/**
|
|
19
|
+
* Kills `child` if it's still running when this process exits. `cleanup`
|
|
20
|
+
* replaces the default SIGKILL; it must be synchronous, as exit hooks are,
|
|
21
|
+
* and do whatever the child's own exit listener would have done.
|
|
22
|
+
*/
|
|
23
|
+
export function killOnExit(child, cleanup = () => child.kill("SIGKILL")) {
|
|
24
|
+
if (!hooked) {
|
|
25
|
+
hooked = true;
|
|
26
|
+
process.on("exit", runAll);
|
|
27
|
+
}
|
|
28
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
29
|
+
return;
|
|
30
|
+
cleanups.add(cleanup);
|
|
31
|
+
child.once("exit", () => cleanups.delete(cleanup));
|
|
32
|
+
}
|
package/dist/debugger.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CdpParams, type RemoteObject } from "./cdp.ts";
|
|
2
2
|
import { type CellTransformOptions } from "./transform.ts";
|
|
3
3
|
/** What the debugger needs from a CDP target: its session, with the Debugger domain. */
|
|
4
4
|
export interface DebugChannel {
|
package/dist/debugger.js
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
// as their //# sourceURL, so breakpoints bind and frames point at the cell.
|
|
9
9
|
// Lines go through the cell's source map (src/sourcemap.ts), since esbuild
|
|
10
10
|
// rewrites TypeScript.
|
|
11
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join, sep } from "node:path";
|
|
14
14
|
import process from "node:process";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { isRecord, isRemoteObject } from "./cdp.js";
|
|
16
17
|
import { murmur2 } from "./murmur2.js";
|
|
17
|
-
import { transformCellWithLines } from "./transform.js";
|
|
18
|
+
import { SETTLE, transformCellWithLines } from "./transform.js";
|
|
18
19
|
/** ipykernel's seed, so a kernel switch doesn't move JupyterLab's breakpoints. */
|
|
19
20
|
export const HASH_SEED = 0xc70f6907;
|
|
20
21
|
export const THREAD_ID = 1;
|
|
@@ -35,9 +36,6 @@ const SCOPE_NAMES = {
|
|
|
35
36
|
eval: "Eval",
|
|
36
37
|
global: "Globals",
|
|
37
38
|
};
|
|
38
|
-
function isRecord(value) {
|
|
39
|
-
return typeof value === "object" && value !== null;
|
|
40
|
-
}
|
|
41
39
|
/**
|
|
42
40
|
* CDPK_DEBUG_TRACE=1 logs every debug request, reply and event to stderr
|
|
43
41
|
* (the Jupyter server's log), for diagnosing a frontend.
|
|
@@ -48,8 +46,11 @@ function trace(kind, ...parts) {
|
|
|
48
46
|
const text = parts.map((p) => JSON.stringify(p)).join(" -> ");
|
|
49
47
|
process.stderr.write(`[cdp-kernel debugger] ${kind} ${text.length > 600 ? `${text.slice(0, 600)}…` : text}\n`);
|
|
50
48
|
}
|
|
51
|
-
function
|
|
52
|
-
return isRecord(value) && typeof value.
|
|
49
|
+
function isCallFrame(value) {
|
|
50
|
+
return isRecord(value) && typeof value.callFrameId === "string" && typeof value.functionName === "string" &&
|
|
51
|
+
isRecord(value.location) && typeof value.location.scriptId === "string" &&
|
|
52
|
+
typeof value.location.lineNumber === "number" && Array.isArray(value.scopeChain) &&
|
|
53
|
+
value.scopeChain.every((scope) => isRecord(scope) && typeof scope.type === "string" && isRemoteObject(scope.object));
|
|
53
54
|
}
|
|
54
55
|
/** A DAP value string for a CDP RemoteObject. */
|
|
55
56
|
export function describeRemote(object) {
|
|
@@ -88,6 +89,7 @@ export class Debugger {
|
|
|
88
89
|
#pauseWaiters = [];
|
|
89
90
|
#emit;
|
|
90
91
|
#transformOptions;
|
|
92
|
+
#cleanupRegistered = false;
|
|
91
93
|
constructor(emit, transformOptions = () => ({})) {
|
|
92
94
|
this.#emit = emit;
|
|
93
95
|
this.#transformOptions = transformOptions;
|
|
@@ -330,7 +332,7 @@ export class Debugger {
|
|
|
330
332
|
}
|
|
331
333
|
// --- stopped state ----------------------------------------------------------
|
|
332
334
|
#onPaused(params) {
|
|
333
|
-
this.#frames = Array.isArray(params.callFrames) ? params.callFrames : [];
|
|
335
|
+
this.#frames = Array.isArray(params.callFrames) ? params.callFrames.filter(isCallFrame) : [];
|
|
334
336
|
this.#stopped = true;
|
|
335
337
|
for (const notify of this.#pauseWaiters.splice(0))
|
|
336
338
|
notify();
|
|
@@ -453,7 +455,6 @@ export class Debugger {
|
|
|
453
455
|
const res = await this.#unlessPaused(channel.send("Runtime.evaluate", {
|
|
454
456
|
expression: name,
|
|
455
457
|
contextId: channel.contextId,
|
|
456
|
-
replMode: true,
|
|
457
458
|
throwOnSideEffect: true,
|
|
458
459
|
}));
|
|
459
460
|
if (res === null)
|
|
@@ -470,9 +471,14 @@ export class Debugger {
|
|
|
470
471
|
throw new Error("no target to debug");
|
|
471
472
|
// While paused, only a frame can evaluate: use the top one if none was named.
|
|
472
473
|
const frame = typeof frameId === "number" ? this.#frames[frameId] : this.#stopped ? this.#frames[0] : undefined;
|
|
474
|
+
// replMode (for the console's `await`) returns a promise the inspector
|
|
475
|
+
// holds only weakly: SETTLE closes that window, as for cells (#55). A
|
|
476
|
+
// side-effect-free lookup can't await, so it uses a plain evaluate.
|
|
473
477
|
const res = frame
|
|
474
478
|
? await channel.send("Debugger.evaluateOnCallFrame", { callFrameId: frame.callFrameId, expression, throwOnSideEffect: pure })
|
|
475
|
-
:
|
|
479
|
+
: pure
|
|
480
|
+
? await channel.send("Runtime.evaluate", { expression, contextId: channel.contextId, throwOnSideEffect: true })
|
|
481
|
+
: await channel.send("Runtime.evaluate", { expression: expression + SETTLE, contextId: channel.contextId, replMode: true });
|
|
476
482
|
if (isRecord(res.exceptionDetails)) {
|
|
477
483
|
const exception = res.exceptionDetails.exception;
|
|
478
484
|
throw new Error(exception ? describeRemote(exception) : String(res.exceptionDetails.text ?? "evaluation failed"));
|
|
@@ -519,6 +525,11 @@ export class Debugger {
|
|
|
519
525
|
}
|
|
520
526
|
#writeCell(path, code) {
|
|
521
527
|
try {
|
|
528
|
+
if (!this.#cleanupRegistered) {
|
|
529
|
+
// The files only serve this kernel's frontend: remove them with it.
|
|
530
|
+
process.once("exit", () => rmSync(this.prefix, { recursive: true, force: true }));
|
|
531
|
+
this.#cleanupRegistered = true;
|
|
532
|
+
}
|
|
522
533
|
mkdirSync(this.prefix, { recursive: true });
|
|
523
534
|
writeFileSync(path, code);
|
|
524
535
|
}
|
package/dist/globals.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Ambient types for notebook cells run by cdp-kernel: the `jupyter` display
|
|
3
3
|
* namespace and the stdin-backed `prompt`/`confirm`.
|
|
4
4
|
*
|
|
5
|
-
* Load it once per project, for example
|
|
6
|
-
* `/// <reference types="@tlockney/cdp-kernel/globals"
|
|
7
|
-
*
|
|
5
|
+
* Load it once per project, for example with
|
|
6
|
+
* `/// <reference types="@tlockney/cdp-kernel/globals" />`. It ships in the
|
|
7
|
+
* npm package only: Deno notebooks use `Deno.jupyter`, which Deno types.
|
|
8
8
|
*
|
|
9
9
|
* A custom display is a class with a `[jupyter.$display]()` method returning
|
|
10
10
|
* a media bundle. At runtime that key is `Symbol.for("Jupyter.display")`, but
|
package/dist/globals.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Ambient types for notebook cells run by cdp-kernel: the `jupyter` display
|
|
3
3
|
* namespace and the stdin-backed `prompt`/`confirm`.
|
|
4
4
|
*
|
|
5
|
-
* Load it once per project, for example
|
|
6
|
-
* `/// <reference types="@tlockney/cdp-kernel/globals"
|
|
7
|
-
*
|
|
5
|
+
* Load it once per project, for example with
|
|
6
|
+
* `/// <reference types="@tlockney/cdp-kernel/globals" />`. It ships in the
|
|
7
|
+
* npm package only: Deno notebooks use `Deno.jupyter`, which Deno types.
|
|
8
8
|
*
|
|
9
9
|
* A custom display is a class with a `[jupyter.$display]()` method returning
|
|
10
10
|
* a media bundle. At runtime that key is `Symbol.for("Jupyter.display")`, but
|