@tlockney/cdp-kernel 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -478
- package/THIRD-PARTY-NOTICES +62 -0
- package/dist/bun-target.d.ts +2 -2
- package/dist/bun-target.js +30 -8
- package/dist/cdp.d.ts +8 -1
- package/dist/cdp.js +31 -11
- package/dist/celld-target.d.ts +1 -1
- package/dist/celld-target.js +54 -19
- package/dist/children.d.ts +29 -0
- package/dist/children.js +114 -0
- package/dist/debugger.d.ts +1 -1
- package/dist/debugger.js +21 -10
- package/dist/deno-types.d.ts +30 -0
- package/dist/deno-types.js +93 -0
- package/dist/globals.d.ts +3 -3
- package/dist/globals.js +3 -3
- package/dist/kernel.d.ts +7 -1
- package/dist/kernel.js +348 -68
- package/dist/language-service-worker.d.ts +1 -0
- package/dist/language-service-worker.js +88 -0
- package/dist/language-service.d.ts +64 -0
- package/dist/language-service.js +128 -0
- package/dist/main.js +102 -8
- package/dist/mod.d.ts +6 -3
- package/dist/mod.js +6 -3
- package/dist/notebook-types.d.ts +62 -0
- package/dist/notebook-types.js +310 -0
- package/dist/package.d.ts +2 -2
- package/dist/package.js +3 -3
- package/dist/parent.d.ts +12 -0
- package/dist/parent.js +39 -0
- package/dist/port-holder.d.ts +13 -0
- package/dist/port-holder.js +105 -0
- package/dist/sourcemap.d.ts +7 -2
- package/dist/sourcemap.js +9 -3
- package/dist/strip-esbuild.d.ts +2 -0
- package/dist/strip-esbuild.js +44 -0
- package/dist/strip-sucrase.d.ts +2 -0
- package/dist/strip-sucrase.js +78 -0
- package/dist/target.d.ts +58 -6
- package/dist/target.js +412 -54
- package/dist/text.d.ts +5 -1
- package/dist/text.js +55 -2
- package/dist/transform.d.ts +30 -1
- package/dist/transform.js +68 -44
- package/dist/wire.d.ts +8 -1
- package/dist/wire.js +3 -1
- package/dist/workerd-target.d.ts +7 -1
- package/dist/workerd-target.js +102 -8
- package/dist/workers-project.js +13 -2
- package/dist/ws-client.d.ts +5 -1
- package/dist/ws-client.js +46 -13
- package/dist/zmtp.d.ts +9 -1
- package/dist/zmtp.js +42 -1
- package/package.json +17 -6
- package/runtime/bootstrap.mjs +233 -42
- package/runtime/bun-channel.d.mts +3 -0
- package/runtime/bun-channel.mjs +2 -2
- package/runtime/comm.mjs +11 -1
- package/runtime/deno-compat.d.mts +2 -0
- package/runtime/deno-compat.mjs +14 -1
- package/runtime/display.mjs +8 -1
- package/runtime/introspect.mjs +1 -1
- package/runtime/node-hooks.d.mts +48 -0
- package/runtime/node-hooks.mjs +257 -41
- package/runtime/workerd-worker.mjs +77 -6
package/dist/bun-target.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
|
|
2
2
|
import { transformCellForEval } from "./transform.ts";
|
|
3
3
|
import { type KernelTarget, type TargetEvents } from "./target.ts";
|
|
4
4
|
/**
|
|
@@ -55,6 +55,6 @@ export declare class BunTarget implements KernelTarget {
|
|
|
55
55
|
inspect(path: string, detail: number): Promise<string | null>;
|
|
56
56
|
/** None: top-level declarations here are properties of globalThis. */
|
|
57
57
|
lexicalNames(): Promise<string[]>;
|
|
58
|
-
replyInput(value: string | null): void;
|
|
58
|
+
replyInput(value: string | null, interrupted?: boolean): void;
|
|
59
59
|
kill(): void;
|
|
60
60
|
}
|
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,13 +29,20 @@ 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;
|
|
36
35
|
send(method: string, params?: CdpParams): Promise<CdpParams>;
|
|
36
|
+
/** In-flight calls, oldest first, with how long each has waited: for a state dump (#237). */
|
|
37
|
+
pendingCalls(): string[];
|
|
37
38
|
/** Reject every in-flight call; their late responses are then ignored. */
|
|
38
39
|
abandonPending(): void;
|
|
39
40
|
close(): void;
|
|
40
41
|
}
|
|
42
|
+
export declare function isRecord(value: unknown): value is CdpParams;
|
|
43
|
+
export declare function isRemoteObject(value: unknown): value is RemoteObject;
|
|
44
|
+
/**
|
|
45
|
+
* A Runtime.evaluate-shaped reply, checked: a value, or exceptionDetails.
|
|
46
|
+
* The Bun and workerd drivers send no `result` with a thrown exception.
|
|
47
|
+
*/
|
|
41
48
|
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,12 +63,21 @@ 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
|
-
this.#pending.set(id, { method, resolve, reject });
|
|
72
|
+
this.#pending.set(id, { method, sent: Date.now(), resolve, reject });
|
|
73
73
|
this.#ws.send(JSON.stringify({ id, method, params }));
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
+
/** In-flight calls, oldest first, with how long each has waited: for a state dump (#237). */
|
|
77
|
+
pendingCalls() {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
return [...this.#pending.values()].map((p) => `${p.method} (${now - p.sent} ms)`);
|
|
80
|
+
}
|
|
76
81
|
/** Reject every in-flight call; their late responses are then ignored. */
|
|
77
82
|
abandonPending() {
|
|
78
83
|
for (const pending of this.#pending.values()) {
|
|
@@ -84,6 +89,21 @@ export class CdpSession {
|
|
|
84
89
|
this.#ws.close();
|
|
85
90
|
}
|
|
86
91
|
}
|
|
92
|
+
export function isRecord(value) {
|
|
93
|
+
return typeof value === "object" && value !== null;
|
|
94
|
+
}
|
|
95
|
+
export function isRemoteObject(value) {
|
|
96
|
+
return isRecord(value) && typeof value.type === "string";
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A Runtime.evaluate-shaped reply, checked: a value, or exceptionDetails.
|
|
100
|
+
* The Bun and workerd drivers send no `result` with a thrown exception.
|
|
101
|
+
*/
|
|
87
102
|
export function asEvaluateResult(raw) {
|
|
88
|
-
|
|
103
|
+
const details = raw.exceptionDetails;
|
|
104
|
+
if (details !== undefined ? !isRecord(details) || typeof details.text !== "string" : !isRemoteObject(raw.result)) {
|
|
105
|
+
throw new Error(`the target sent a malformed evaluate result: ${JSON.stringify(raw).slice(0, 200)}`);
|
|
106
|
+
}
|
|
107
|
+
const result = isRemoteObject(raw.result) ? raw.result : { type: "undefined" };
|
|
108
|
+
return details === undefined ? { result } : { result, exceptionDetails: details };
|
|
89
109
|
}
|
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 { childPids, killOnExit, reapWith } 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. */
|
|
@@ -120,16 +122,10 @@ function channelOf(ws) {
|
|
|
120
122
|
},
|
|
121
123
|
};
|
|
122
124
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return []; // none
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/** Kills celld dev's node, then the supervisor itself. */
|
|
125
|
+
/**
|
|
126
|
+
* Kills celld dev's own children, which run in their own process groups,
|
|
127
|
+
* then the supervisor itself.
|
|
128
|
+
*/
|
|
133
129
|
function killTree(child) {
|
|
134
130
|
for (const pid of childPids(child.pid ?? -1)) {
|
|
135
131
|
try {
|
|
@@ -141,6 +137,8 @@ function killTree(child) {
|
|
|
141
137
|
}
|
|
142
138
|
child.kill("SIGKILL");
|
|
143
139
|
}
|
|
140
|
+
/** Starts tried when a port is taken (see CelldHost.start). */
|
|
141
|
+
const PORT_ATTEMPTS = 3;
|
|
144
142
|
/** The driver on `celld dev`. */
|
|
145
143
|
class CelldHost {
|
|
146
144
|
runtime;
|
|
@@ -153,7 +151,23 @@ class CelldHost {
|
|
|
153
151
|
this.runtime = runtime;
|
|
154
152
|
this.project = project;
|
|
155
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* celld dev, retried when a port is taken: both its --port (from
|
|
156
|
+
* freePort) and its node's own listener are picked free and bound later,
|
|
157
|
+
* so another process can take one in between (#54).
|
|
158
|
+
*/
|
|
156
159
|
static async start(cwd, projectConfig, executable) {
|
|
160
|
+
for (let attempt = 1;; attempt++) {
|
|
161
|
+
try {
|
|
162
|
+
return await CelldHost.#start(cwd, projectConfig, executable);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
if (attempt >= PORT_ATTEMPTS || !(err instanceof Error && /Address already in use/.test(err.message)))
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
static async #start(cwd, projectConfig, executable) {
|
|
157
171
|
const celld = executable ?? findCelld();
|
|
158
172
|
const version = celldVersionOf(execFileSync(celld, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
|
|
159
173
|
const source = await runtimeDir(new URL("../runtime/", import.meta.url).href);
|
|
@@ -163,7 +177,7 @@ class CelldHost {
|
|
|
163
177
|
if (projectConfig.endsWith(".toml"))
|
|
164
178
|
throw new Error("celld reads wrangler.jsonc or wrangler.json, not wrangler.toml");
|
|
165
179
|
const parsed = parseJsonc(readFileSync(projectConfig, "utf8"));
|
|
166
|
-
if (
|
|
180
|
+
if (!isRecord(parsed) || Array.isArray(parsed))
|
|
167
181
|
throw new Error(`${projectConfig} is not an object`);
|
|
168
182
|
project = parsed;
|
|
169
183
|
dir = join(dirname(projectConfig), ".celld", "cdp-kernel");
|
|
@@ -173,9 +187,14 @@ class CelldHost {
|
|
|
173
187
|
dir = mkdtempSync(join(tmpdir(), "cdpk-celld-"));
|
|
174
188
|
}
|
|
175
189
|
const main = project && typeof project.main === "string" ? resolve(dirname(projectConfig ?? cwd), project.main) : undefined;
|
|
176
|
-
const define =
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
const define = {};
|
|
191
|
+
if (project && isRecord(project.define)) {
|
|
192
|
+
for (const [name, value] of Object.entries(project.define)) {
|
|
193
|
+
if (typeof value !== "string")
|
|
194
|
+
throw new Error(`define.${name} in ${projectConfig} must be a string`);
|
|
195
|
+
define[name] = value;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
179
198
|
const token = randomBytes(32).toString("hex");
|
|
180
199
|
// 0600: the config carries this launch's token.
|
|
181
200
|
writeFileSync(join(dir, "driver.mjs"), await bundleDriver(source, main, define));
|
|
@@ -186,25 +205,35 @@ class CelldHost {
|
|
|
186
205
|
const child = spawn(celld, ["dev", dir, "--host", "127.0.0.1", "--port", String(port), "--no-watch"], {
|
|
187
206
|
stdio: ["ignore", "pipe", "pipe"],
|
|
188
207
|
});
|
|
189
|
-
|
|
208
|
+
const removeFiles = () => {
|
|
190
209
|
if (projectConfig === null)
|
|
191
210
|
rmSync(dir, { recursive: true, force: true });
|
|
192
211
|
else
|
|
193
212
|
for (const name of ["driver.mjs", "wrangler.jsonc"])
|
|
194
213
|
rmSync(join(dir, name), { force: true });
|
|
195
|
-
}
|
|
214
|
+
};
|
|
215
|
+
child.once("exit", removeFiles);
|
|
216
|
+
killOnExit(child, () => {
|
|
217
|
+
killTree(child);
|
|
218
|
+
removeFiles();
|
|
219
|
+
}, { dir: projectConfig === null ? dir : undefined });
|
|
196
220
|
// Cell output is captured in the driver; celld's own lines go to the
|
|
197
221
|
// server log.
|
|
222
|
+
// `early` keeps that output for a startup error only, and stops growing
|
|
223
|
+
// once the session is open.
|
|
198
224
|
let early = "";
|
|
225
|
+
let starting = true;
|
|
199
226
|
const log = (chunk) => {
|
|
200
|
-
|
|
227
|
+
if (starting)
|
|
228
|
+
early += chunk.toString();
|
|
201
229
|
process.stderr.write(`[cdp-kernel celld] ${chunk}`);
|
|
202
230
|
};
|
|
203
231
|
child.stdout?.on("data", log);
|
|
204
232
|
child.stderr?.on("data", log);
|
|
205
233
|
const exited = new Promise((_, reject) => {
|
|
206
234
|
child.once("error", reject);
|
|
207
|
-
|
|
235
|
+
// On close, not exit, so its last output is in the message.
|
|
236
|
+
child.once("close", (code) => reject(new Error(`celld exited (${code}) before it was ready:\n${early}`)));
|
|
208
237
|
});
|
|
209
238
|
exited.catch(() => { });
|
|
210
239
|
const url = `127.0.0.1:${port}/?token=${token}`;
|
|
@@ -226,7 +255,13 @@ class CelldHost {
|
|
|
226
255
|
return channelOf(ws);
|
|
227
256
|
})();
|
|
228
257
|
try {
|
|
229
|
-
|
|
258
|
+
const host = new CelldHost(child, await Promise.race([ready, exited]), `celld ${version}`.trim(), projectConfig ?? undefined);
|
|
259
|
+
// Its server, in its own process group, outlives a SIGKILLed celld dev.
|
|
260
|
+
for (const pid of childPids(child.pid ?? -1))
|
|
261
|
+
reapWith(pid, child);
|
|
262
|
+
starting = false;
|
|
263
|
+
early = "";
|
|
264
|
+
return host;
|
|
230
265
|
}
|
|
231
266
|
catch (err) {
|
|
232
267
|
killTree(child);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* The helper's script. Lines on stdin: `+pid` registers a pid, `-pid`
|
|
4
|
+
* forgets it (it exited, and the pid may be reused), `d<path>` registers a
|
|
5
|
+
* directory. In its own session, so a killpg of this process's group (as
|
|
6
|
+
* Jupyter's kill does) doesn't take it down first.
|
|
7
|
+
*/
|
|
8
|
+
export declare const REAPER_SCRIPT = "trap '' HUP INT\npids=\" \"\ndirs=\"\"\nwhile IFS= read -r line; do\n case $line in\n +*) pids=\"$pids${line#?} \" ;;\n -*) pids=$(printf '%s' \"$pids\" | sed \"s/ ${line#?} / /\") ;;\n d*) dirs=\"$dirs${line#?}\n\" ;;\n esac\ndone\nkill -9 $pids 2>/dev/null\nprintf '%s' \"$dirs\" | while IFS= read -r dir; do rm -rf -- \"$dir\"; done\n";
|
|
9
|
+
/**
|
|
10
|
+
* Kills `pid` if this process dies, until the returned function is called
|
|
11
|
+
* (when the process is known to have exited: its pid may be reused).
|
|
12
|
+
*/
|
|
13
|
+
export declare function reap(pid: number): () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Kills `pid`, which isn't this process's child (a runtime's own child in
|
|
16
|
+
* its own process group), if this process dies while `owner` runs.
|
|
17
|
+
*/
|
|
18
|
+
export declare function reapWith(pid: number, owner: ChildProcess): void;
|
|
19
|
+
/** `pid`'s children whose command line matches `pattern` (pgrep's -f). */
|
|
20
|
+
export declare function childPids(pid: number, pattern?: string): number[];
|
|
21
|
+
/**
|
|
22
|
+
* Kills `child` if it's still running when this process exits. `cleanup`
|
|
23
|
+
* replaces the default SIGKILL; it must be synchronous, as exit hooks are,
|
|
24
|
+
* and do whatever the child's own exit listener would have done. If this
|
|
25
|
+
* process is SIGKILLed, the helper kills `child` and removes `dir`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function killOnExit(child: ChildProcess, cleanup?: () => void, { dir }?: {
|
|
28
|
+
dir?: string;
|
|
29
|
+
}): void;
|
package/dist/children.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
//
|
|
5
|
+
// SIGKILL runs no exit hook, so a helper outside this process also knows the
|
|
6
|
+
// runtimes' pids (#240): a detached sh reading this process's pipe, which
|
|
7
|
+
// kills whatever is still registered, and removes the registered temp
|
|
8
|
+
// directories, once the pipe closes. That covers what nothing in a runtime
|
|
9
|
+
// can notice: celld dev and its server, and a Node or Deno runtime stuck in
|
|
10
|
+
// a cell's synchronous loop, where the #219 watchdog never gets to run.
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
13
|
+
const cleanups = new Set();
|
|
14
|
+
function runAll() {
|
|
15
|
+
for (const cleanup of cleanups) {
|
|
16
|
+
try {
|
|
17
|
+
cleanup();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Best effort: the process is exiting.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
cleanups.clear();
|
|
24
|
+
}
|
|
25
|
+
let hooked = false;
|
|
26
|
+
/**
|
|
27
|
+
* The helper's script. Lines on stdin: `+pid` registers a pid, `-pid`
|
|
28
|
+
* forgets it (it exited, and the pid may be reused), `d<path>` registers a
|
|
29
|
+
* directory. In its own session, so a killpg of this process's group (as
|
|
30
|
+
* Jupyter's kill does) doesn't take it down first.
|
|
31
|
+
*/
|
|
32
|
+
export const REAPER_SCRIPT = `trap '' HUP INT
|
|
33
|
+
pids=" "
|
|
34
|
+
dirs=""
|
|
35
|
+
while IFS= read -r line; do
|
|
36
|
+
case $line in
|
|
37
|
+
+*) pids="$pids\${line#?} " ;;
|
|
38
|
+
-*) pids=$(printf '%s' "$pids" | sed "s/ \${line#?} / /") ;;
|
|
39
|
+
d*) dirs="$dirs\${line#?}
|
|
40
|
+
" ;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
kill -9 $pids 2>/dev/null
|
|
44
|
+
printf '%s' "$dirs" | while IFS= read -r dir; do rm -rf -- "$dir"; done
|
|
45
|
+
`;
|
|
46
|
+
let reaper;
|
|
47
|
+
/** Sends `line` to the helper, starting it first; a no-op on Windows. */
|
|
48
|
+
function tellReaper(line) {
|
|
49
|
+
if (reaper === undefined) {
|
|
50
|
+
reaper = null;
|
|
51
|
+
if (process.platform === "win32")
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
const child = spawn("/bin/sh", ["-c", REAPER_SCRIPT], { cwd: "/", detached: true, stdio: ["pipe", "ignore", "ignore"] });
|
|
55
|
+
child.once("error", () => (reaper = null));
|
|
56
|
+
child.stdin?.on("error", () => (reaper = null));
|
|
57
|
+
// Neither the helper nor its pipe keeps this process running.
|
|
58
|
+
child.unref();
|
|
59
|
+
child.stdin?.unref?.();
|
|
60
|
+
reaper = child;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
reaper?.stdin?.write(`${line}\n`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Kills `pid` if this process dies, until the returned function is called
|
|
70
|
+
* (when the process is known to have exited: its pid may be reused).
|
|
71
|
+
*/
|
|
72
|
+
export function reap(pid) {
|
|
73
|
+
tellReaper(`+${pid}`);
|
|
74
|
+
return () => tellReaper(`-${pid}`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Kills `pid`, which isn't this process's child (a runtime's own child in
|
|
78
|
+
* its own process group), if this process dies while `owner` runs.
|
|
79
|
+
*/
|
|
80
|
+
export function reapWith(pid, owner) {
|
|
81
|
+
if (owner.exitCode !== null || owner.signalCode !== null)
|
|
82
|
+
return;
|
|
83
|
+
owner.once("exit", reap(pid));
|
|
84
|
+
}
|
|
85
|
+
/** `pid`'s children whose command line matches `pattern` (pgrep's -f). */
|
|
86
|
+
export function childPids(pid, pattern) {
|
|
87
|
+
try {
|
|
88
|
+
const args = ["-P", String(pid), ...(pattern === undefined ? [] : ["-f", pattern])];
|
|
89
|
+
return execFileSync("pgrep", args, { encoding: "utf8" }).split(/\s+/).filter(Boolean).map(Number);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return []; // none, or no pgrep
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Kills `child` if it's still running when this process exits. `cleanup`
|
|
97
|
+
* replaces the default SIGKILL; it must be synchronous, as exit hooks are,
|
|
98
|
+
* and do whatever the child's own exit listener would have done. If this
|
|
99
|
+
* process is SIGKILLed, the helper kills `child` and removes `dir`.
|
|
100
|
+
*/
|
|
101
|
+
export function killOnExit(child, cleanup = () => child.kill("SIGKILL"), { dir } = {}) {
|
|
102
|
+
if (!hooked) {
|
|
103
|
+
hooked = true;
|
|
104
|
+
process.on("exit", runAll);
|
|
105
|
+
}
|
|
106
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
107
|
+
return;
|
|
108
|
+
cleanups.add(cleanup);
|
|
109
|
+
child.once("exit", () => cleanups.delete(cleanup));
|
|
110
|
+
if (dir !== undefined)
|
|
111
|
+
tellReaper(`d${dir}`);
|
|
112
|
+
if (child.pid !== undefined)
|
|
113
|
+
reapWith(child.pid, child);
|
|
114
|
+
}
|
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
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type DenoResolution = {
|
|
2
|
+
kind: "npm";
|
|
3
|
+
dir: string;
|
|
4
|
+
subpath: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "remote";
|
|
7
|
+
path: string;
|
|
8
|
+
};
|
|
9
|
+
export type RunDeno = (args: string[]) => {
|
|
10
|
+
status: number | null;
|
|
11
|
+
stdout: string;
|
|
12
|
+
};
|
|
13
|
+
export declare class DenoResolver {
|
|
14
|
+
#private;
|
|
15
|
+
/** Where remote modules appear to the language service. */
|
|
16
|
+
readonly root: string;
|
|
17
|
+
constructor({ deno, config, root }: {
|
|
18
|
+
deno?: string;
|
|
19
|
+
config?: string | null;
|
|
20
|
+
root: string;
|
|
21
|
+
}, run?: RunDeno);
|
|
22
|
+
/** The virtual path for a remote module's URL, e.g. <root>/https/jsr.io/@std/path/1.1.6/mod.ts. */
|
|
23
|
+
pathFor(url: string): string | null;
|
|
24
|
+
/** The cached file behind a virtual path, if a lookup has found it. */
|
|
25
|
+
localFile(path: string): string | undefined;
|
|
26
|
+
/** Where `specifier` resolves for Deno, from what's already in its cache. */
|
|
27
|
+
lookup(specifier: string): DenoResolution | null;
|
|
28
|
+
}
|
|
29
|
+
/** `deno types`: declarations for the Deno namespace and its web globals. */
|
|
30
|
+
export declare function denoTypes(run?: RunDeno): string | null;
|