@ai-ecoverse/slicc-kernel 1.22.0 → 1.23.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.
@@ -224,8 +224,51 @@ function pinScript(url) {
224
224
  }
225
225
  }
226
226
 
227
+ // src/cdp/port.ts
228
+ function portCdpHook(ask, gone) {
229
+ return (request2) => new Promise((resolve, reject) => {
230
+ const { port1, port2 } = new MessageChannel();
231
+ let closed = false;
232
+ const conn = {
233
+ send: (m) => {
234
+ if (!closed) port1.postMessage({ m });
235
+ },
236
+ onmessage: null,
237
+ onclose: null,
238
+ close: () => {
239
+ if (closed) return;
240
+ closed = true;
241
+ port1.postMessage({ close: true });
242
+ port1.close();
243
+ }
244
+ };
245
+ port1.addEventListener("message", ({ data }) => {
246
+ if ("m" in data) conn.onmessage?.(data.m);
247
+ else if ("open" in data) resolve(conn);
248
+ else if ("error" in data) {
249
+ closed = true;
250
+ port1.close();
251
+ reject(new Error(data.error));
252
+ } else if (!closed) {
253
+ closed = true;
254
+ port1.close();
255
+ conn.onclose?.(data.close ?? void 0);
256
+ }
257
+ });
258
+ port1.start();
259
+ void gone?.then(() => {
260
+ if (closed) return;
261
+ closed = true;
262
+ port1.close();
263
+ reject(new Error("the CDP host is gone"));
264
+ conn.onclose?.("the CDP host is gone");
265
+ });
266
+ ask(request2, port2);
267
+ });
268
+ }
269
+
227
270
  // src/client/protocol.ts
228
- var PROTOCOL = [1, 4];
271
+ var PROTOCOL = [1, 5];
229
272
  var FS_METHODS = [
230
273
  "readFile",
231
274
  "writeFile",
@@ -887,7 +930,12 @@ function keyOf(addr) {
887
930
  var LoopbackNet = class {
888
931
  bound = /* @__PURE__ */ new Map();
889
932
  activators = /* @__PURE__ */ new Map();
933
+ internal = /* @__PURE__ */ new Set();
890
934
  nextEphemeral = EPHEMERAL_FIRST;
935
+ kernelOnly(port, reserve = false) {
936
+ if (reserve) this.internal.add(port);
937
+ return this.internal.has(port);
938
+ }
891
939
  activate(addr, start) {
892
940
  this.activators.set(keyOf(addr), start);
893
941
  }
@@ -948,6 +996,14 @@ var LOOPBACK_NAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "
948
996
  function dialSocket(net, port, host = "127.0.0.1") {
949
997
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new KernelError("EINVAL");
950
998
  const target = LOOPBACK_NAMES.has(host.toLowerCase()) ? "127.0.0.1" : host;
999
+ if (net.kernelOnly(port)) {
1000
+ throw Object.assign(
1001
+ new Error(`ECONNREFUSED: port ${port} only takes connections from inside the kernel`),
1002
+ {
1003
+ code: "ECONNREFUSED"
1004
+ }
1005
+ );
1006
+ }
951
1007
  return net.connect({ family: "inet", host: target, port });
952
1008
  }
953
1009
  var codeOf = (err) => {
@@ -1236,6 +1292,43 @@ function joinable(l, pgid, ours) {
1236
1292
  if (sid === void 0) throw fsError("ESRCH", `no process group ${pgid}`);
1237
1293
  if (!ours(sid)) throw fsError("EPERM", `process group ${pgid} is in another session`);
1238
1294
  }
1295
+ function cdpRegistry(send, gone) {
1296
+ const registered = /* @__PURE__ */ new Map();
1297
+ const drop = (id) => {
1298
+ registered.get(id)?.();
1299
+ return registered.delete(id);
1300
+ };
1301
+ return {
1302
+ serve(l, req) {
1303
+ const hook = portCdpHook(
1304
+ (request2, cdp) => send({ cdpOpen: { ...request2, registration: req.id }, port: cdp }, [
1305
+ cdp
1306
+ ]),
1307
+ gone
1308
+ );
1309
+ registered.set(req.id, l.cdp.register(hook, req.runtime));
1310
+ return req.id;
1311
+ },
1312
+ drop,
1313
+ close: () => {
1314
+ for (const id of [...registered.keys()]) drop(id);
1315
+ }
1316
+ };
1317
+ }
1318
+ function watchOp(l, req, watches, send) {
1319
+ if (req.op === "unwatch") {
1320
+ watches.get(req.watch)?.();
1321
+ watches.delete(req.watch);
1322
+ return { result: true };
1323
+ }
1324
+ const unwatch = l.watchers.watch(
1325
+ req.paths,
1326
+ { recursive: req.recursive },
1327
+ (change) => send({ watch: req.id, change })
1328
+ );
1329
+ watches.set(req.id, unwatch);
1330
+ return { result: req.id };
1331
+ }
1239
1332
  function serveClient(port, host) {
1240
1333
  let state = "new";
1241
1334
  const terminals = /* @__PURE__ */ new Map();
@@ -1248,6 +1341,7 @@ function serveClient(port, host) {
1248
1341
  if (state !== "closed") port.postMessage(message2, transfer);
1249
1342
  };
1250
1343
  const reply = (id, body, transfer) => send({ id, ...body }, transfer);
1344
+ const cdps = cdpRegistry(send, ended.promise);
1251
1345
  function detach(kill = false, bye = "the slicc-kernel detached this client") {
1252
1346
  if (state === "closed") return;
1253
1347
  send({ bye });
@@ -1255,6 +1349,7 @@ function serveClient(port, host) {
1255
1349
  net?.close();
1256
1350
  for (const unwatch of watches.values()) unwatch();
1257
1351
  watches.clear();
1352
+ cdps.close();
1258
1353
  for (const session of terminals.values()) session.close();
1259
1354
  terminals.clear();
1260
1355
  if (kill) killGroups(host, [...groups]);
@@ -1349,19 +1444,9 @@ function serveClient(port, host) {
1349
1444
  throw new Error(`unknown file system call ${req.method}`);
1350
1445
  }
1351
1446
  return fsCall(l.fs, req.method, req.args);
1352
- case "watch": {
1353
- const unwatch = l.watchers.watch(
1354
- req.paths,
1355
- { recursive: req.recursive },
1356
- (change) => send({ watch: req.id, change })
1357
- );
1358
- watches.set(req.id, unwatch);
1359
- return { result: req.id };
1360
- }
1447
+ case "watch":
1361
1448
  case "unwatch":
1362
- watches.get(req.watch)?.();
1363
- watches.delete(req.watch);
1364
- return { result: true };
1449
+ return watchOp(l, req, watches, send);
1365
1450
  case "mount":
1366
1451
  return { result: await l.mount(req.spec) };
1367
1452
  case "umount":
@@ -1371,6 +1456,10 @@ function serveClient(port, host) {
1371
1456
  return { result: l.mounts.list() };
1372
1457
  case "dial":
1373
1458
  return dialFor(l, req, watches);
1459
+ case "serve-cdp":
1460
+ return { result: cdps.serve(l, req) };
1461
+ case "unserve-cdp":
1462
+ return { result: cdps.drop(req.registration) };
1374
1463
  case "detach":
1375
1464
  return { result: true };
1376
1465
  default:
@@ -1934,6 +2023,8 @@ var REASON = {
1934
2023
  200: "OK",
1935
2024
  400: "Bad Request",
1936
2025
  403: "Forbidden",
2026
+ 404: "Not Found",
2027
+ 405: "Method Not Allowed",
1937
2028
  408: "Request Timeout",
1938
2029
  411: "Length Required",
1939
2030
  413: "Content Too Large",
@@ -1943,6 +2034,7 @@ var REASON = {
1943
2034
  501: "Not Implemented",
1944
2035
  502: "Bad Gateway",
1945
2036
  503: "Service Unavailable",
2037
+ 504: "Gateway Timeout",
1946
2038
  505: "HTTP Version Not Supported"
1947
2039
  };
1948
2040
  var Incoming = class {
@@ -3388,6 +3480,465 @@ var RemoteTransport = class {
3388
3480
  }
3389
3481
  };
3390
3482
 
3483
+ // src/cdp/types.ts
3484
+ var CDP_PORT = 9222;
3485
+ var NO_CDP_HOST = "slicc-kernel: no CDP host is attached (createKernel({ cdp }) or client.serveCdp)";
3486
+
3487
+ // src/cdp/websocket.ts
3488
+ var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
3489
+ var MAX_MESSAGE = 256 * 1024 * 1024;
3490
+ var CLOSE = {
3491
+ normal: 1e3,
3492
+ protocol: 1002,
3493
+ unsupported: 1003,
3494
+ invalid: 1007,
3495
+ tooBig: 1009,
3496
+ error: 1011
3497
+ };
3498
+ var OP_TEXT = 1;
3499
+ var OP = { continuation: 0, text: OP_TEXT, binary: 2, close: 8, ping: 9, pong: 10 };
3500
+ var WsError = class extends Error {
3501
+ code;
3502
+ constructor(code, message2) {
3503
+ super(message2);
3504
+ this.code = code;
3505
+ }
3506
+ };
3507
+ function field(headers, name2) {
3508
+ return headers.find(([n]) => n.toLowerCase() === name2)?.[1];
3509
+ }
3510
+ async function acceptKey(headers) {
3511
+ const tokens = (name2) => (field(headers, name2) ?? "").split(",").map((t) => t.trim().toLowerCase());
3512
+ if (!tokens("upgrade").includes("websocket") || !tokens("connection").includes("upgrade")) {
3513
+ throw new HttpError(400, "expected a WebSocket upgrade");
3514
+ }
3515
+ if (field(headers, "sec-websocket-version") !== "13") {
3516
+ throw new HttpError(400, "Sec-WebSocket-Version must be 13");
3517
+ }
3518
+ const key = field(headers, "sec-websocket-key") ?? "";
3519
+ if (!/^[A-Za-z0-9+/]{22}==$/.test(key)) throw new HttpError(400, "bad Sec-WebSocket-Key");
3520
+ const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(key + GUID));
3521
+ return btoa(String.fromCharCode(...new Uint8Array(digest)));
3522
+ }
3523
+ function isUpgrade(headers) {
3524
+ return (field(headers, "upgrade") ?? "").toLowerCase().includes("websocket");
3525
+ }
3526
+ function frame(opcode, payload) {
3527
+ const n = payload.length;
3528
+ const size = n < 126 ? 2 : n < 65536 ? 4 : 10;
3529
+ const out = new Uint8Array(size + n);
3530
+ const view = new DataView(out.buffer);
3531
+ out[0] = 128 | opcode;
3532
+ if (n < 126) out[1] = n;
3533
+ else if (n < 65536) {
3534
+ out[1] = 126;
3535
+ view.setUint16(2, n);
3536
+ } else {
3537
+ out[1] = 127;
3538
+ view.setBigUint64(2, BigInt(n));
3539
+ }
3540
+ out.set(payload, size);
3541
+ return out;
3542
+ }
3543
+ function closeFrame(code, reason = "") {
3544
+ let bytes = new TextEncoder().encode(reason);
3545
+ if (bytes.length > 123) bytes = bytes.subarray(0, 123);
3546
+ const payload = new Uint8Array(2 + bytes.length);
3547
+ new DataView(payload.buffer).setUint16(0, code);
3548
+ payload.set(bytes, 2);
3549
+ return frame(OP.close, payload);
3550
+ }
3551
+ function pongFrame(payload) {
3552
+ return frame(OP.pong, payload);
3553
+ }
3554
+ async function fill(incoming, out, signal) {
3555
+ for (let at = 0; at < out.length; ) {
3556
+ const piece = await incoming.some(out.length - at, signal);
3557
+ if (piece.length === 0) throw new WsError(CLOSE.protocol, "the connection closed mid-frame");
3558
+ out.set(piece, at);
3559
+ at += piece.length;
3560
+ }
3561
+ }
3562
+ var fatal = new TextDecoder("utf-8", { fatal: true });
3563
+ function decode(bytes) {
3564
+ try {
3565
+ return fatal.decode(bytes);
3566
+ } catch {
3567
+ throw new WsError(CLOSE.invalid, "a text message is not UTF-8");
3568
+ }
3569
+ }
3570
+ var WsReader = class {
3571
+ incoming;
3572
+ max;
3573
+ parts = [];
3574
+ size = 0;
3575
+ continuing = false;
3576
+ constructor(incoming, max = MAX_MESSAGE) {
3577
+ this.incoming = incoming;
3578
+ this.max = max;
3579
+ }
3580
+ async next(signal) {
3581
+ for (; ; ) {
3582
+ const first = await this.incoming.some(1, signal);
3583
+ if (first.length === 0) return null;
3584
+ const head = new Uint8Array(2);
3585
+ head[0] = first[0];
3586
+ await fill(this.incoming, head.subarray(1), signal);
3587
+ const fin = (head[0] & 128) !== 0;
3588
+ const opcode = head[0] & 15;
3589
+ if ((head[0] & 112) !== 0) throw new WsError(CLOSE.protocol, "reserved bits are set");
3590
+ if ((head[1] & 128) === 0) throw new WsError(CLOSE.protocol, "client frames must be masked");
3591
+ const length2 = await this.length(head[1] & 127, signal);
3592
+ const control = opcode >= 8;
3593
+ if (control && (length2 > 125 || !fin)) {
3594
+ throw new WsError(CLOSE.protocol, "a control frame is too long or fragmented");
3595
+ }
3596
+ if (!control && this.size + length2 > this.max) {
3597
+ throw new WsError(CLOSE.tooBig, `a message is over ${this.max} bytes`);
3598
+ }
3599
+ const mask = new Uint8Array(4);
3600
+ await fill(this.incoming, mask, signal);
3601
+ const payload = new Uint8Array(length2);
3602
+ await fill(this.incoming, payload, signal);
3603
+ for (let i = 0; i < length2; i++) payload[i] ^= mask[i & 3];
3604
+ const event = this.take(opcode, fin, payload);
3605
+ if (event) return event;
3606
+ }
3607
+ }
3608
+ async length(short, signal) {
3609
+ if (short < 126) return short;
3610
+ const ext = new Uint8Array(short === 126 ? 2 : 8);
3611
+ await fill(this.incoming, ext, signal);
3612
+ const view = new DataView(ext.buffer);
3613
+ if (short === 126) return view.getUint16(0);
3614
+ const long = view.getBigUint64(0);
3615
+ return long > BigInt(this.max) ? this.max + 1 : Number(long);
3616
+ }
3617
+ take(opcode, fin, payload) {
3618
+ switch (opcode) {
3619
+ case OP.ping:
3620
+ return { kind: "ping", payload };
3621
+ case OP.pong:
3622
+ return void 0;
3623
+ case OP.close: {
3624
+ if (payload.length === 1) throw new WsError(CLOSE.protocol, "a close frame is malformed");
3625
+ const code = payload.length >= 2 ? new DataView(payload.buffer).getUint16(0) : 1005;
3626
+ return { kind: "close", code, reason: decode(payload.subarray(2)) };
3627
+ }
3628
+ case OP.text:
3629
+ case OP.continuation: {
3630
+ if (opcode === OP.continuation !== this.continuing) {
3631
+ throw new WsError(CLOSE.protocol, "unexpected continuation frame");
3632
+ }
3633
+ this.parts.push(payload);
3634
+ this.size += payload.length;
3635
+ this.continuing = !fin;
3636
+ if (!fin) return void 0;
3637
+ const whole = this.parts.length === 1 ? payload : concat(this.parts, this.size);
3638
+ this.parts = [];
3639
+ this.size = 0;
3640
+ return { kind: "text", text: decode(whole) };
3641
+ }
3642
+ case OP.binary:
3643
+ throw new WsError(CLOSE.unsupported, "CDP messages are text");
3644
+ default:
3645
+ throw new WsError(CLOSE.protocol, `unknown opcode ${opcode}`);
3646
+ }
3647
+ }
3648
+ };
3649
+ function switchingHead(accept2) {
3650
+ return latin1Bytes(
3651
+ `HTTP/1.1 101 Switching Protocols\r
3652
+ Upgrade: websocket\r
3653
+ Connection: Upgrade\r
3654
+ Sec-WebSocket-Accept: ${accept2}\r
3655
+ \r
3656
+ `
3657
+ );
3658
+ }
3659
+
3660
+ // src/cdp/facade.ts
3661
+ var MAX_HEAD = 64 * 1024;
3662
+ var LIST_TIMEOUT = 1e4;
3663
+ function respond(status, body, type = "text/plain; charset=utf-8") {
3664
+ const bytes = new TextEncoder().encode(body);
3665
+ const head = latin1Bytes(
3666
+ `HTTP/1.1 ${status} ${REASON[status]}\r
3667
+ Content-Type: ${type}\r
3668
+ Content-Length: ${bytes.length}\r
3669
+ Connection: close\r
3670
+ \r
3671
+ `
3672
+ );
3673
+ const out = new Uint8Array(head.length + bytes.length);
3674
+ out.set(head);
3675
+ out.set(bytes, head.length);
3676
+ return out;
3677
+ }
3678
+ function json(value) {
3679
+ return respond(200, `${JSON.stringify(value, null, 2)}
3680
+ `, "application/json; charset=UTF-8");
3681
+ }
3682
+ var CdpFacade = class {
3683
+ options;
3684
+ listener;
3685
+ stop = new AbortController();
3686
+ connections = /* @__PURE__ */ new Set();
3687
+ closed;
3688
+ constructor(options) {
3689
+ this.options = options;
3690
+ this.listener = options.net.listen({
3691
+ family: "inet",
3692
+ host: "127.0.0.1",
3693
+ port: options.port ?? CDP_PORT
3694
+ });
3695
+ this.closed = this.acceptLoop();
3696
+ }
3697
+ close() {
3698
+ if (this.stop.signal.aborted) return;
3699
+ this.stop.abort();
3700
+ this.listener.close();
3701
+ for (const conn of this.connections) conn.close();
3702
+ }
3703
+ async acceptLoop() {
3704
+ const served = /* @__PURE__ */ new Set();
3705
+ for (; ; ) {
3706
+ let conn;
3707
+ try {
3708
+ conn = await this.listener.accept(this.stop.signal);
3709
+ } catch {
3710
+ break;
3711
+ }
3712
+ const done4 = this.serve(conn).finally(() => served.delete(done4));
3713
+ served.add(done4);
3714
+ }
3715
+ await Promise.allSettled([...served]);
3716
+ }
3717
+ async serve(conn) {
3718
+ this.connections.add(conn);
3719
+ const incoming = new Incoming(conn);
3720
+ try {
3721
+ const head = await incoming.head(MAX_HEAD, this.stop.signal);
3722
+ if (head) await this.route(conn, incoming, parseRequestHead(head));
3723
+ } catch (e) {
3724
+ if (e instanceof HttpError) await this.write(conn, respond(e.status, `${e.message}
3725
+ `));
3726
+ } finally {
3727
+ this.connections.delete(conn);
3728
+ conn.close();
3729
+ }
3730
+ }
3731
+ async write(conn, bytes) {
3732
+ try {
3733
+ await conn.write(bytes, this.stop.signal);
3734
+ } catch {
3735
+ }
3736
+ }
3737
+ async route(conn, incoming, req) {
3738
+ const url = new URL(req.target, "http://127.0.0.1");
3739
+ const runtime = url.searchParams.get("runtime") ?? void 0;
3740
+ const host = req.headers.find(([n]) => n.toLowerCase() === "host")?.[1] ?? "127.0.0.1";
3741
+ if (req.method !== "GET") throw new HttpError(405, `${req.method} is not supported here`);
3742
+ const { hosts } = this.options;
3743
+ if (url.pathname === hosts.path && isUpgrade(req.headers)) {
3744
+ return this.upgrade(conn, incoming, req, runtime);
3745
+ }
3746
+ const path = url.pathname.replace(/\/+$/, "");
3747
+ if (path !== "/json/version" && path !== "/json/list" && path !== "/json") {
3748
+ throw new HttpError(404, "not found: try /json/version");
3749
+ }
3750
+ if (!this.options.hosts.available(runtime)) {
3751
+ throw new HttpError(503, NO_CDP_HOST);
3752
+ }
3753
+ if (path === "/json/version") {
3754
+ await this.write(
3755
+ conn,
3756
+ json({
3757
+ Browser: "slicc-kernel",
3758
+ "Protocol-Version": "1.3",
3759
+ webSocketDebuggerUrl: `ws://${host}${hosts.path}${url.search}`
3760
+ })
3761
+ );
3762
+ return;
3763
+ }
3764
+ await this.write(conn, json(await this.list(runtime)));
3765
+ }
3766
+ async list(runtime) {
3767
+ const conn = await this.host(runtime);
3768
+ try {
3769
+ const reply = await new Promise(
3770
+ (resolve, reject) => {
3771
+ const timer = setTimeout(
3772
+ () => reject(new HttpError(504, "the CDP host did not answer Target.getTargets")),
3773
+ this.options.listTimeout ?? LIST_TIMEOUT
3774
+ );
3775
+ conn.onclose = (reason) => {
3776
+ clearTimeout(timer);
3777
+ reject(new HttpError(502, `the CDP host closed: ${reason ?? "no reason"}`));
3778
+ };
3779
+ conn.onmessage = (message2) => {
3780
+ const parsed = JSON.parse(message2);
3781
+ if (parsed.id !== 1) return;
3782
+ clearTimeout(timer);
3783
+ resolve(parsed);
3784
+ };
3785
+ conn.send(JSON.stringify({ id: 1, method: "Target.getTargets" }));
3786
+ }
3787
+ );
3788
+ if (!reply.result) {
3789
+ throw new HttpError(502, `Target.getTargets failed: ${JSON.stringify(reply.error)}`);
3790
+ }
3791
+ return (reply.result.targetInfos ?? []).map((t) => ({
3792
+ description: "",
3793
+ id: t.targetId,
3794
+ title: t.title,
3795
+ type: t.type,
3796
+ url: t.url
3797
+ }));
3798
+ } finally {
3799
+ conn.close();
3800
+ }
3801
+ }
3802
+ host(runtime) {
3803
+ return this.options.hosts.connect(runtime ? { runtime } : {});
3804
+ }
3805
+ async upgrade(conn, incoming, req, runtime) {
3806
+ const accept2 = await acceptKey(req.headers);
3807
+ const host = await this.host(runtime);
3808
+ await new Relay(conn, incoming, host, this.options.maxMessage ?? MAX_MESSAGE, this.stop.signal).start(switchingHead(accept2)).finally(() => host.close());
3809
+ }
3810
+ };
3811
+ var Relay = class {
3812
+ writes = Promise.resolve();
3813
+ ended = false;
3814
+ conn;
3815
+ incoming;
3816
+ host;
3817
+ max;
3818
+ stop;
3819
+ constructor(conn, incoming, host, max, stop) {
3820
+ this.conn = conn;
3821
+ this.incoming = incoming;
3822
+ this.host = host;
3823
+ this.max = max;
3824
+ this.stop = stop;
3825
+ }
3826
+ send(bytes) {
3827
+ this.writes = this.writes.then(() => this.conn.write(bytes, this.stop)).catch(() => void 0);
3828
+ return this.writes;
3829
+ }
3830
+ end(code, reason) {
3831
+ if (this.ended) return this.writes;
3832
+ this.ended = true;
3833
+ return this.send(closeFrame(code, reason));
3834
+ }
3835
+ async start(head) {
3836
+ await this.send(head);
3837
+ const hostGone = new Promise((resolve) => {
3838
+ this.host.onclose = (reason) => {
3839
+ void this.end(reason ? CLOSE.error : CLOSE.normal, reason ?? "").then(() => resolve());
3840
+ };
3841
+ });
3842
+ this.host.onmessage = (message2) => {
3843
+ if (this.ended) return;
3844
+ const bytes = new TextEncoder().encode(message2);
3845
+ if (bytes.length > this.max)
3846
+ void this.end(CLOSE.tooBig, `a message is over ${this.max} bytes`);
3847
+ else void this.send(frame(OP_TEXT, bytes));
3848
+ };
3849
+ await Promise.race([this.pump(), hostGone]);
3850
+ await this.writes;
3851
+ }
3852
+ async pump() {
3853
+ const reader = new WsReader(this.incoming, this.max);
3854
+ try {
3855
+ for (; ; ) {
3856
+ const event = await reader.next(this.stop);
3857
+ if (!event) return;
3858
+ if (event.kind === "text") this.host.send(event.text);
3859
+ else if (event.kind === "ping") void this.send(pongFrame(event.payload));
3860
+ else {
3861
+ await this.end(event.code === 1005 ? CLOSE.normal : event.code, "");
3862
+ return;
3863
+ }
3864
+ }
3865
+ } catch (e) {
3866
+ if (e instanceof WsError) await this.end(e.code, e.message);
3867
+ }
3868
+ }
3869
+ };
3870
+ function enableCdp(net, hosts) {
3871
+ let facade;
3872
+ net.kernelOnly(CDP_PORT, true);
3873
+ net.activate({ family: "inet", host: "127.0.0.1", port: CDP_PORT }, () => {
3874
+ const started = new CdpFacade({ net, hosts });
3875
+ facade = started;
3876
+ void started.closed.then(() => {
3877
+ if (facade === started) facade = void 0;
3878
+ });
3879
+ });
3880
+ return () => facade;
3881
+ }
3882
+
3883
+ // src/cdp/hosts.ts
3884
+ var CdpHosts = class {
3885
+ embedder;
3886
+ clients = [];
3887
+ closed = false;
3888
+ path = `/devtools/browser/${crypto.randomUUID()}`;
3889
+ url = `ws://127.0.0.1:${CDP_PORT}${this.path}`;
3890
+ constructor(hook) {
3891
+ this.embedder = hook ? { hook, runtime: void 0, open: /* @__PURE__ */ new Set() } : void 0;
3892
+ }
3893
+ register(hook, runtime) {
3894
+ const entry = { hook, runtime, open: /* @__PURE__ */ new Set() };
3895
+ this.clients.push(entry);
3896
+ return () => {
3897
+ const at = this.clients.indexOf(entry);
3898
+ if (at >= 0) this.clients.splice(at, 1);
3899
+ closeAll(entry);
3900
+ };
3901
+ }
3902
+ pick(runtime) {
3903
+ if (this.closed) return void 0;
3904
+ const named = runtime ? this.clients.findLast((c) => c.runtime === runtime) : void 0;
3905
+ return named ?? this.embedder ?? this.clients.at(-1);
3906
+ }
3907
+ available(runtime) {
3908
+ return this.pick(runtime) !== void 0;
3909
+ }
3910
+ async connect(request2) {
3911
+ const entry = this.pick(request2.runtime);
3912
+ if (!entry) throw new HttpError(503, NO_CDP_HOST);
3913
+ let conn;
3914
+ try {
3915
+ conn = await entry.hook(request2.runtime ? { runtime: request2.runtime } : {});
3916
+ } catch (e) {
3917
+ throw new HttpError(502, `CDP host: ${e instanceof Error ? e.message : String(e)}`);
3918
+ }
3919
+ if (this.closed || !(entry === this.embedder || this.clients.includes(entry))) {
3920
+ conn.close();
3921
+ throw new HttpError(503, NO_CDP_HOST);
3922
+ }
3923
+ entry.open.add(conn);
3924
+ const close = conn.close.bind(conn);
3925
+ conn.close = () => {
3926
+ if (entry.open.delete(conn)) close();
3927
+ };
3928
+ return conn;
3929
+ }
3930
+ close() {
3931
+ this.closed = true;
3932
+ for (const entry of [this.embedder, ...this.clients]) if (entry) closeAll(entry);
3933
+ }
3934
+ };
3935
+ function closeAll(entry) {
3936
+ for (const conn of [...entry.open]) {
3937
+ conn.close();
3938
+ conn.onclose?.("the CDP host is gone");
3939
+ }
3940
+ }
3941
+
3391
3942
  // src/commands.ts
3392
3943
  var NAME = /^[A-Za-z0-9._+-]+$/;
3393
3944
  var ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -4128,6 +4679,7 @@ var MountTable = class {
4128
4679
  mounts = /* @__PURE__ */ new Map();
4129
4680
  devices = 0;
4130
4681
  pending = /* @__PURE__ */ new Set();
4682
+ notes = /* @__PURE__ */ new Map();
4131
4683
  deps;
4132
4684
  constructor(deps) {
4133
4685
  this.deps = deps;
@@ -4136,7 +4688,7 @@ var MountTable = class {
4136
4688
  return this.mounts.get(target);
4137
4689
  }
4138
4690
  list() {
4139
- return [...this.mounts.values()].map(
4691
+ const mounted = [...this.mounts.values()].map(
4140
4692
  ({ type, source, target, options, state, error, present }) => ({
4141
4693
  type,
4142
4694
  source,
@@ -4146,6 +4698,19 @@ var MountTable = class {
4146
4698
  ...error ? { error } : {}
4147
4699
  })
4148
4700
  );
4701
+ const noted = [...this.notes.values()].filter((note) => !this.mounts.has(note.target));
4702
+ return [...mounted, ...noted.map((note) => ({ ...note, options: { ...note.options } }))];
4703
+ }
4704
+ note(spec, state, error) {
4705
+ const target = normalizePath(spec.target);
4706
+ this.notes.set(target, {
4707
+ type: spec.type,
4708
+ source: spec.source,
4709
+ target,
4710
+ options: { ...spec.options },
4711
+ state,
4712
+ ...error ? { error } : {}
4713
+ });
4149
4714
  }
4150
4715
  locate(path) {
4151
4716
  let best;
@@ -4222,13 +4787,19 @@ var MountTable = class {
4222
4787
  };
4223
4788
  mounted = mount;
4224
4789
  this.mounts.set(target, mount);
4790
+ this.notes.delete(target);
4225
4791
  return this.list().find((m) => m.target === target);
4226
4792
  }
4793
+ unnote(target) {
4794
+ this.notes.delete(normalizePath(target));
4795
+ }
4227
4796
  umount(target, detach = false) {
4228
4797
  const at = normalizePath(target);
4229
4798
  const mount = this.mounts.get(at);
4799
+ if (!mount && this.notes.delete(at)) return;
4230
4800
  if (!mount) throw errnoError("EINVAL", `${at} is not mounted`);
4231
4801
  if (!detach && this.deps.busy(at)) throw errnoError("EBUSY", `${at} has open files`);
4802
+ this.notes.delete(at);
4232
4803
  this.mounts.delete(at);
4233
4804
  mount.conn.close();
4234
4805
  mount.dispose();
@@ -4691,7 +5262,7 @@ async function storageUsage() {
4691
5262
  return { quota: estimate.quota, usage: estimate.usage ?? 0 };
4692
5263
  }
4693
5264
  var done = { ok: true, kind: "void" };
4694
- function json(value) {
5265
+ function json2(value) {
4695
5266
  return { ok: true, kind: "json", json: value };
4696
5267
  }
4697
5268
  async function run(entry, path, req) {
@@ -4703,15 +5274,15 @@ async function run(entry, path, req) {
4703
5274
  await fs.writeFile(path, req.body ?? new Uint8Array(0));
4704
5275
  return done;
4705
5276
  case "exists":
4706
- return json(await fs.exists(path));
5277
+ return json2(await fs.exists(path));
4707
5278
  case "stat":
4708
- return json(statJson(await fs.stat(path)));
5279
+ return json2(statJson(await fs.stat(path)));
4709
5280
  case "lstat":
4710
- return json(statJson(await fs.lstat(path)));
5281
+ return json2(statJson(await fs.lstat(path)));
4711
5282
  case "readdir":
4712
- return json(await fs.readdir(path));
5283
+ return json2(await fs.readdir(path));
4713
5284
  case "readdir-stat":
4714
- return json(await readdirStat(fs, path));
5285
+ return json2(await readdirStat(fs, path));
4715
5286
  case "mkdir":
4716
5287
  await fs.mkdir(path, { recursive: true });
4717
5288
  return done;
@@ -4737,7 +5308,7 @@ async function run(entry, path, req) {
4737
5308
  await fs.symlink(req.arg2 ?? "", path);
4738
5309
  return done;
4739
5310
  case "readlink":
4740
- return json(await fs.readlink(path));
5311
+ return json2(await fs.readlink(path));
4741
5312
  case "chmod":
4742
5313
  await fs.chmod(path, req.mode ?? 0);
4743
5314
  return done;
@@ -4768,7 +5339,7 @@ async function dispatchSyncFs(req) {
4768
5339
  const { fs, cwd } = entry;
4769
5340
  try {
4770
5341
  const path = fs.resolvePath(cwd, req.path);
4771
- if (req.op === "statfs") return json(await entry.statfs?.(path) ?? await storageUsage());
5342
+ if (req.op === "statfs") return json2(await entry.statfs?.(path) ?? await storageUsage());
4772
5343
  if (req.op === "hold") {
4773
5344
  entry.hold?.(path, req.mode === 1);
4774
5345
  return done;
@@ -4941,7 +5512,7 @@ var HOST_OPS = [
4941
5512
  "async-cancel",
4942
5513
  "async-close"
4943
5514
  ];
4944
- var json2 = (value) => ({ ok: true, kind: "json", json: value });
5515
+ var json3 = (value) => ({ ok: true, kind: "json", json: value });
4945
5516
  var done2 = { ok: true, kind: "void" };
4946
5517
  var LockTable = class {
4947
5518
  held = /* @__PURE__ */ new Map();
@@ -4990,7 +5561,7 @@ var AsyncOps = class {
4990
5561
  }
4991
5562
  resolve(value) {
4992
5563
  const id = this.hold();
4993
- this.complete(id, json2(value));
5564
+ this.complete(id, json3(value));
4994
5565
  return id;
4995
5566
  }
4996
5567
  hold() {
@@ -5072,15 +5643,15 @@ async function hostSyscall(req, ctx) {
5072
5643
  return done2;
5073
5644
  case "async-submit":
5074
5645
  if (!ctx.valid(req.req)) throw new KernelError("EINVAL");
5075
- return json2(ctx.ops.submit(req.req));
5646
+ return json3(ctx.ops.submit(req.req));
5076
5647
  case "async-wait":
5077
- return json2(await ctx.ops.wait(req.timeoutMs, ctx.blocking()));
5648
+ return json3(await ctx.ops.wait(req.timeoutMs, ctx.blocking()));
5078
5649
  case "async-take":
5079
5650
  return ctx.ops.take(req.id);
5080
5651
  case "async-resolve":
5081
- return json2(ctx.ops.resolve(req.value));
5652
+ return json3(ctx.ops.resolve(req.value));
5082
5653
  case "async-hold":
5083
- return json2(ctx.ops.hold());
5654
+ return json3(ctx.ops.hold());
5084
5655
  case "async-cancel":
5085
5656
  ctx.ops.cancel(req.id);
5086
5657
  return done2;
@@ -5607,23 +6178,23 @@ function terminalOf(ctx, fd) {
5607
6178
  if (!tty) throw new KernelError("ENOTTY");
5608
6179
  return tty;
5609
6180
  }
5610
- var json3 = (value) => ({ ok: true, kind: "json", json: value });
6181
+ var json4 = (value) => ({ ok: true, kind: "json", json: value });
5611
6182
  var done3 = { ok: true, kind: "void" };
5612
6183
  function ptySyscall(req, ctx) {
5613
6184
  switch (req.op) {
5614
6185
  case "pty-open": {
5615
6186
  if (!ctx.ptys) throw new KernelError("ENOENT");
5616
- return json3(ctx.fds.install(ctx.ptys.open().master, 3));
6187
+ return json4(ctx.fds.install(ctx.ptys.open().master, 3));
5617
6188
  }
5618
6189
  case "pty-slave-open": {
5619
6190
  if (!ctx.ptys) throw new KernelError("ENOENT");
5620
6191
  const pair = ctx.ptys.get(req.n);
5621
6192
  const fd = ctx.fds.install(pair.openSlave(), 3);
5622
6193
  if (!req.noctty) ctx.jobs?.acquireTerminal(ctx.pid, pair.slave);
5623
- return json3(fd);
6194
+ return json4(fd);
5624
6195
  }
5625
6196
  case "pty-number":
5626
- return json3(masterOf(ctx, req.fd).index);
6197
+ return json4(masterOf(ctx, req.fd).index);
5627
6198
  case "pty-lock":
5628
6199
  masterOf(ctx, req.fd).locked = req.lock;
5629
6200
  return done3;
@@ -5635,7 +6206,7 @@ function ptySyscall(req, ctx) {
5635
6206
  return done3;
5636
6207
  }
5637
6208
  case "pty-list":
5638
- return json3(ctx.ptys?.numbers() ?? []);
6209
+ return json4(ctx.ptys?.numbers() ?? []);
5639
6210
  case "pty-packet":
5640
6211
  masterOf(ctx, req.fd).packet = req.on;
5641
6212
  return done3;
@@ -5713,7 +6284,7 @@ function socketAt(fds, fd) {
5713
6284
  if (!(file instanceof KernelSocket)) throw new KernelError("ENOTSOCK");
5714
6285
  return file;
5715
6286
  }
5716
- var ok = (json4) => json4 === void 0 ? { ok: true, kind: "void" } : { ok: true, kind: "json", json: json4 };
6287
+ var ok = (json5) => json5 === void 0 ? { ok: true, kind: "void" } : { ok: true, kind: "json", json: json5 };
5717
6288
  async function socketSyscall(req, proc) {
5718
6289
  const { fds, net } = proc;
5719
6290
  switch (req.op) {
@@ -7677,8 +8248,8 @@ function umountCall(target, flags) {
7677
8248
  var FSTAB_PATH = "/etc/fstab";
7678
8249
  var FSTAB_RETRIES = [1e3, 4e3, 16e3];
7679
8250
  var FINAL = /* @__PURE__ */ new Set(["EINVAL", "EBUSY", "EPERM", "EACCES"]);
7680
- function unescape(field) {
7681
- return field.replace(
8251
+ function unescape(field2) {
8252
+ return field2.replace(
7682
8253
  /\\([0-7]{3})/g,
7683
8254
  (_, octal) => String.fromCharCode(Number.parseInt(octal, 8))
7684
8255
  );
@@ -7717,28 +8288,34 @@ function wait(ms, signal) {
7717
8288
  });
7718
8289
  }
7719
8290
  async function mountFstab(lines, mount, signal, retries = FSTAB_RETRIES, discard = () => {
8291
+ }, settled = () => {
7720
8292
  }) {
7721
8293
  return Promise.all(
7722
- lines.map(async ({ line, spec, invalid }) => {
7723
- let failed = invalid;
7724
- for (let attempt = 0; !invalid && attempt <= retries.length && !signal.aborted; attempt++) {
7725
- try {
7726
- const entry = await mount(spec);
7727
- if (!signal.aborted) return { line, target: spec.target, entry };
7728
- discard(entry);
7729
- failed = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
7730
- break;
7731
- } catch (err) {
7732
- failed = err;
7733
- if (FINAL.has(codeOf3(err)) || attempt === retries.length) break;
7734
- await wait(retries[attempt], signal);
7735
- }
7736
- }
7737
- const code = codeOf3(failed);
7738
- return { line, target: spec.target, code, error: String(failed?.message ?? code) };
8294
+ lines.map(async (each) => {
8295
+ const result = await mountLine(each, mount, signal, retries, discard);
8296
+ settled(each, result);
8297
+ return result;
7739
8298
  })
7740
8299
  );
7741
8300
  }
8301
+ async function mountLine({ line, spec, invalid }, mount, signal, retries, discard) {
8302
+ let failed = invalid;
8303
+ for (let attempt = 0; !invalid && attempt <= retries.length && !signal.aborted; attempt++) {
8304
+ try {
8305
+ const entry = await mount(spec);
8306
+ if (!signal.aborted) return { line, target: spec.target, entry };
8307
+ discard(entry);
8308
+ failed = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
8309
+ break;
8310
+ } catch (err) {
8311
+ failed = err;
8312
+ if (FINAL.has(codeOf3(err)) || attempt === retries.length) break;
8313
+ await wait(retries[attempt], signal);
8314
+ }
8315
+ }
8316
+ const code = codeOf3(failed);
8317
+ return { line, target: spec.target, code, error: String(failed?.message ?? code) };
8318
+ }
7742
8319
 
7743
8320
  // src/mount/hostfs.ts
7744
8321
  var HOSTFS_IO = 16 * 1024 * 1024;
@@ -8320,7 +8897,7 @@ function importedMemory(bytes) {
8320
8897
  const count = u32();
8321
8898
  for (let i = 0; i < count; i++) {
8322
8899
  const module = name2();
8323
- const field = name2();
8900
+ const field2 = name2();
8324
8901
  const kind = bytes[at++];
8325
8902
  if (kind === 0) u32();
8326
8903
  else if (kind === 1) {
@@ -8328,7 +8905,7 @@ function importedMemory(bytes) {
8328
8905
  limits();
8329
8906
  } else if (kind === 2) {
8330
8907
  const l = limits();
8331
- return { module, name: field, ...l };
8908
+ return { module, name: field2, ...l };
8332
8909
  } else if (kind === 3) at += 2;
8333
8910
  else if (kind === 4) {
8334
8911
  at++;
@@ -8419,12 +8996,12 @@ function readImports(r, types) {
8419
8996
  const foreign = {};
8420
8997
  for (let i = r.u32(); i > 0; i--) {
8421
8998
  const module = r.name();
8422
- const field = r.name();
8999
+ const field2 = r.name();
8423
9000
  const kind = r.byte();
8424
9001
  if (kind !== 0) skipImport(r, kind);
8425
9002
  else {
8426
9003
  const type = types[r.u32()] ?? "other";
8427
- if (!RESERVED_NAMESPACES.has(module)) (foreign[module] ??= {})[field] = type;
9004
+ if (!RESERVED_NAMESPACES.has(module)) (foreign[module] ??= {})[field2] = type;
8428
9005
  }
8429
9006
  }
8430
9007
  return foreign;
@@ -8531,6 +9108,7 @@ var Launcher = class {
8531
9108
  locks = new LockTable();
8532
9109
  ptys = new PtyTable((tty, sig) => this.jobs.signalOwnedForeground(tty, sig));
8533
9110
  net = new LoopbackNet();
9111
+ cdp;
8534
9112
  ca;
8535
9113
  transport;
8536
9114
  watchers = new FsWatchers();
@@ -8541,6 +9119,7 @@ var Launcher = class {
8541
9119
  onMountPending;
8542
9120
  removable = /* @__PURE__ */ new Map();
8543
9121
  hostfs;
9122
+ fstabRetries;
8544
9123
  inserted = /* @__PURE__ */ new Map();
8545
9124
  openFiles = /* @__PURE__ */ new Set();
8546
9125
  held = /* @__PURE__ */ new Set();
@@ -8562,10 +9141,12 @@ var Launcher = class {
8562
9141
  this.media = options.media ?? memoryMedia();
8563
9142
  this.onMountPending = options.onMountPending;
8564
9143
  this.hostfs = options;
9144
+ this.fstabRetries = options.fstabRetries ?? FSTAB_RETRIES;
8565
9145
  this.processMounts = options.processMounts ?? true;
8566
9146
  this.pnpmHome = options.env?.PNPM_HOME ?? PNPM_HOME;
8567
9147
  this.modulesDir = options.modules ?? "/node_modules";
8568
- this.env = { ...networkEnv(), ...options.env };
9148
+ this.cdp = new CdpHosts(options.cdp);
9149
+ this.env = { ...networkEnv(), SLICC_CDP_URL: this.cdp.url, ...options.env };
8569
9150
  this.ca = kernelCa(options.caStore ?? memoryCaStore());
8570
9151
  this.transport = options.transport ?? missingTransport();
8571
9152
  enableNetwork(this.net, {
@@ -8573,6 +9154,7 @@ var Launcher = class {
8573
9154
  engine: kernelTlsEngine(packageTlsEngine(options.fs, this.modulesDir)),
8574
9155
  ca: this.ca
8575
9156
  });
9157
+ enableCdp(this.net, this.cdp);
8576
9158
  this.fs = withCommandDirs(this.base, async () => new Set((await this.commands()).keys()));
8577
9159
  this.watchers.watch([this.modulesDir, this.pnpmHome], { recursive: true }, () => {
8578
9160
  this.catalog = void 0;
@@ -8995,12 +9577,22 @@ var Launcher = class {
8995
9577
  }
8996
9578
  async mountFstab() {
8997
9579
  const text = await this.base.readFile(FSTAB_PATH).catch(() => "");
9580
+ const lines = parseFstab(text);
9581
+ for (const { spec } of lines) this.mounts.note(spec, "pending");
8998
9582
  return mountFstab(
8999
- parseFstab(text),
9000
- (spec) => this.mount(spec),
9583
+ lines,
9584
+ (spec) => this.mount(spec).catch((err) => {
9585
+ this.mounts.note(spec, "pending", err?.message ?? String(err));
9586
+ throw err;
9587
+ }),
9001
9588
  this.booting.signal,
9002
- FSTAB_RETRIES,
9003
- (entry) => this.umount(entry.target, true)
9589
+ this.fstabRetries,
9590
+ (entry) => this.umount(entry.target, true),
9591
+ ({ spec }, result) => {
9592
+ if (!result.code || result.code === "ECANCELED" || this.booting.signal.aborted) {
9593
+ this.mounts.unnote(spec.target);
9594
+ } else this.mounts.note(spec, "failed", result.error);
9595
+ }
9004
9596
  );
9005
9597
  }
9006
9598
  environment(cwd, extra) {
@@ -9121,6 +9713,14 @@ async function stores(deps, name2, media) {
9121
9713
  ...handles === false ? {} : { media: await deps.media?.(handles) }
9122
9714
  };
9123
9715
  }
9716
+ function cdpOption(req, port) {
9717
+ if (!req.cdp) return {};
9718
+ return {
9719
+ cdp: portCdpHook(
9720
+ (request2, cdp) => port.postMessage({ cdpOpen: request2, port: cdp }, [cdp])
9721
+ )
9722
+ };
9723
+ }
9124
9724
  function serveKernel(port, deps) {
9125
9725
  let launcher;
9126
9726
  let remote;
@@ -9192,6 +9792,7 @@ function serveKernel(port, deps) {
9192
9792
  ...media ? { media } : {},
9193
9793
  onMountPending: (medium) => port.postMessage({ medium }),
9194
9794
  ...req.hostfs ? { hostfs: askGrant } : {},
9795
+ ...cdpOption(req, port),
9195
9796
  ...mountPolicy(req.processMounts),
9196
9797
  caStore: caStore(deps.metadata ? caDb(req) : false)
9197
9798
  });