@ai-ecoverse/slicc-kernel 1.19.0 → 1.20.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.
@@ -225,7 +225,7 @@ function pinScript(url) {
225
225
  }
226
226
 
227
227
  // src/client/protocol.ts
228
- var PROTOCOL = [1, 3];
228
+ var PROTOCOL = [1, 4];
229
229
  var FS_METHODS = [
230
230
  "readFile",
231
231
  "writeFile",
@@ -280,1232 +280,1638 @@ function inodeOf(path) {
280
280
  return 4294967296 * (2097151 & h2) + (h1 >>> 0) || 1;
281
281
  }
282
282
 
283
- // src/kernel/signals.ts
284
- var SIG = {
285
- HUP: 1,
286
- INT: 2,
287
- QUIT: 3,
288
- ILL: 4,
289
- TRAP: 5,
290
- ABRT: 6,
291
- BUS: 7,
292
- FPE: 8,
293
- KILL: 9,
294
- USR1: 10,
295
- SEGV: 11,
296
- USR2: 12,
297
- PIPE: 13,
298
- ALRM: 14,
299
- TERM: 15,
300
- STKFLT: 16,
301
- CHLD: 17,
302
- CONT: 18,
303
- STOP: 19,
304
- TSTP: 20,
305
- TTIN: 21,
306
- TTOU: 22,
307
- URG: 23,
308
- XCPU: 24,
309
- XFSZ: 25,
310
- VTALRM: 26,
311
- PROF: 27,
312
- WINCH: 28,
313
- POLL: 29,
314
- PWR: 30,
315
- SYS: 31
316
- };
317
- var IGNORED_BY_DEFAULT = /* @__PURE__ */ new Set([SIG.CHLD, SIG.URG, SIG.WINCH]);
318
- var STOPPING = /* @__PURE__ */ new Set([SIG.STOP, SIG.TSTP, SIG.TTIN, SIG.TTOU]);
319
- function defaultAction(sig) {
320
- if (sig === SIG.CONT) return "continue";
321
- if (STOPPING.has(sig)) return "stop";
322
- return IGNORED_BY_DEFAULT.has(sig) ? "ignore" : "terminate";
323
- }
324
- function isSignal(sig) {
325
- return Number.isInteger(sig) && sig >= 1 && sig <= 31;
326
- }
327
- function sigbit(sig) {
328
- return 1 << sig;
329
- }
330
- var SIGNAL_BY_NAME = {
331
- SIGINT: SIG.INT,
332
- SIGTERM: SIG.TERM,
333
- SIGKILL: SIG.KILL,
334
- SIGSTOP: SIG.STOP,
335
- SIGCONT: SIG.CONT
283
+ // src/kernel/pipe.ts
284
+ var PIPE_CAPACITY = 64 * 1024;
285
+ var PipeError = class extends Error {
286
+ code;
287
+ constructor(code) {
288
+ super(code);
289
+ this.code = code;
290
+ }
336
291
  };
337
-
338
- // src/body-idle.ts
339
- var BODY_IDLE_MS = 5 * 60 * 1e3;
340
-
341
- // src/transport.ts
342
- var MAX_REQUEST_BODY = 64 * 1024 * 1024;
343
- function serveTransport(peer, transport) {
344
- const open = /* @__PURE__ */ new Map();
345
- const fail = (nid, e) => {
346
- open.delete(nid);
347
- const { status, code } = e;
348
- peer.postMessage({
349
- net: "error",
350
- nid,
351
- message: e instanceof Error ? e.message : String(e),
352
- ...typeof status === "number" ? { status } : {},
353
- ...typeof code === "string" ? { code } : {}
354
- });
355
- };
356
- const start = async (call) => {
357
- const abort = new AbortController();
358
- const entry = { abort };
359
- open.set(call.nid, entry);
360
- try {
361
- const response = await transport.fetch({
362
- url: call.url,
363
- method: call.method,
364
- headers: call.headers,
365
- ...call.body ? { body: call.body } : {},
366
- signal: abort.signal
367
- });
368
- if (!open.has(call.nid)) {
369
- await response.cancel();
370
- return;
371
- }
372
- open.set(call.nid, { abort, body: response.body[Symbol.asyncIterator](), response });
373
- peer.postMessage({
374
- net: "head",
375
- nid: call.nid,
376
- status: response.status,
377
- statusText: response.statusText,
378
- headers: response.headers
379
- });
380
- } catch (e) {
381
- fail(call.nid, e);
292
+ var KernelPipe = class {
293
+ chunks = [];
294
+ size = 0;
295
+ readers = 0;
296
+ writers = 0;
297
+ waiters = [];
298
+ capacity;
299
+ constructor(capacity = PIPE_CAPACITY) {
300
+ this.capacity = capacity;
301
+ }
302
+ get buffered() {
303
+ return this.size;
304
+ }
305
+ get readReady() {
306
+ return this.size > 0 || this.writers === 0;
307
+ }
308
+ get writeReady() {
309
+ return this.size < this.capacity || this.readers === 0;
310
+ }
311
+ get writersGone() {
312
+ return this.writers === 0;
313
+ }
314
+ get readersGone() {
315
+ return this.readers === 0;
316
+ }
317
+ openRead() {
318
+ this.readers += 1;
319
+ }
320
+ openWrite() {
321
+ this.writers += 1;
322
+ }
323
+ closeRead() {
324
+ if (this.readers > 0) this.readers -= 1;
325
+ if (this.readers === 0) {
326
+ this.chunks = [];
327
+ this.size = 0;
382
328
  }
383
- };
384
- const read = async (nid) => {
385
- const body = open.get(nid)?.body;
386
- if (!body) return;
387
- try {
388
- const next = await body.next();
389
- if (next.done) {
390
- open.delete(nid);
391
- peer.postMessage({ net: "end", nid });
392
- } else {
393
- const bytes = next.value.slice();
394
- peer.postMessage({ net: "chunk", nid, bytes }, [bytes.buffer]);
329
+ this.wake();
330
+ }
331
+ closeWrite() {
332
+ if (this.writers > 0) this.writers -= 1;
333
+ this.wake();
334
+ }
335
+ async read(max, signal) {
336
+ while (this.size === 0) {
337
+ if (this.writers === 0) return new Uint8Array(0);
338
+ await this.changed(signal);
339
+ }
340
+ const out = this.take(Math.min(max, this.size));
341
+ this.wake();
342
+ return out;
343
+ }
344
+ async peek(max, signal) {
345
+ while (this.size === 0) {
346
+ if (this.writers === 0) return new Uint8Array(0);
347
+ await this.changed(signal);
348
+ }
349
+ const out = new Uint8Array(Math.min(max, this.size));
350
+ let filled = 0;
351
+ for (const chunk2 of this.chunks) {
352
+ if (filled === out.length) break;
353
+ const count = Math.min(chunk2.length, out.length - filled);
354
+ out.set(chunk2.subarray(0, count), filled);
355
+ filled += count;
356
+ }
357
+ return out;
358
+ }
359
+ async write(bytes, signal) {
360
+ let offset = 0;
361
+ while (offset < bytes.length) {
362
+ if (this.readers === 0) throw new PipeError("EPIPE");
363
+ const room = this.capacity - this.size;
364
+ if (room === 0) {
365
+ try {
366
+ await this.changed(signal);
367
+ } catch (e) {
368
+ if (offset > 0) return offset;
369
+ throw e;
370
+ }
371
+ continue;
395
372
  }
396
- } catch (e) {
397
- fail(nid, e);
373
+ const n = Math.min(room, bytes.length - offset);
374
+ this.chunks.push(bytes.slice(offset, offset + n));
375
+ this.size += n;
376
+ offset += n;
377
+ this.wake();
398
378
  }
399
- };
400
- const drop = (nid) => {
401
- const entry = open.get(nid);
402
- open.delete(nid);
403
- entry?.abort.abort();
404
- void entry?.response?.cancel().catch(() => void 0);
405
- };
406
- return {
407
- answer(call) {
408
- if (call.net === "fetch") void start(call);
409
- else if (call.net === "read") void read(call.nid);
410
- else drop(call.nid);
411
- },
412
- close() {
413
- for (const nid of [...open.keys()]) drop(nid);
379
+ return bytes.length;
380
+ }
381
+ take(n) {
382
+ const out = new Uint8Array(n);
383
+ let filled = 0;
384
+ while (filled < n) {
385
+ const head = this.chunks[0];
386
+ const count = Math.min(head.length, n - filled);
387
+ out.set(head.subarray(0, count), filled);
388
+ filled += count;
389
+ if (count === head.length) this.chunks.shift();
390
+ else this.chunks[0] = head.subarray(count);
414
391
  }
415
- };
416
- }
392
+ this.size -= n;
393
+ return out;
394
+ }
395
+ changed(signal) {
396
+ if (signal?.aborted) return Promise.reject(new PipeError("EINTR"));
397
+ return new Promise((resolve, reject) => {
398
+ const onAbort = () => {
399
+ this.waiters = this.waiters.filter((w) => w !== waiter);
400
+ reject(new PipeError("EINTR"));
401
+ };
402
+ const waiter = () => {
403
+ signal?.removeEventListener("abort", onAbort);
404
+ resolve();
405
+ };
406
+ this.waiters.push(waiter);
407
+ signal?.addEventListener("abort", onAbort, { once: true });
408
+ });
409
+ }
410
+ wake() {
411
+ const waiters = this.waiters;
412
+ this.waiters = [];
413
+ for (const resolve of waiters) resolve();
414
+ }
415
+ };
417
416
 
418
- // src/client/serve-client.ts
419
- var MAX_LINKS = 40;
420
- function act(session, req, signal) {
421
- if (req.action === "write") session.write(req.bytes);
422
- else if (req.action === "resize") session.resize(req.cols, req.rows);
423
- else if (req.action === "signal") session.signal(signal(req.signal));
424
- else session.close();
425
- }
426
- async function realpath(fs, path) {
427
- let parts = fs.resolvePath("/", path).split("/").filter(Boolean);
428
- let resolved = "";
429
- for (let hops = 0; parts.length > 0; ) {
430
- const [name2, ...rest] = parts;
431
- const next = `${resolved}/${name2}`;
432
- if (!(await fs.lstat(next)).isSymbolicLink) {
433
- resolved = next;
434
- parts = rest;
435
- continue;
436
- }
437
- if (++hops > MAX_LINKS) throw fsError("ELOOP", path);
438
- const target = await fs.readlink(next);
439
- const base = target.startsWith("/") ? "/" : resolved || "/";
440
- parts = [...fs.resolvePath(base, target).split("/").filter(Boolean), ...rest];
441
- resolved = "";
417
+ // src/kernel/fd-table.ts
418
+ var KernelError = class extends Error {
419
+ code;
420
+ constructor(code) {
421
+ super(code);
422
+ this.code = code;
442
423
  }
443
- return resolved || "/";
424
+ };
425
+ function pollFile(file) {
426
+ return file.poll?.() ?? { readable: !!file.read, writable: !!file.write, hangup: false };
444
427
  }
445
- async function fsCall(fs, method, args) {
446
- const path = (i) => fs.resolvePath("/", String(args[i]));
447
- switch (method) {
448
- case "readFile": {
449
- const bytes = (await fs.readFileBuffer(path(0))).slice();
450
- return { result: bytes, transfer: [bytes.buffer] };
451
- }
452
- case "writeFile":
453
- await fs.writeFile(path(0), args[1]);
454
- return { result: true };
455
- case "stat":
456
- return { result: await fs.stat(path(0)) };
457
- case "lstat":
458
- return { result: await fs.lstat(path(0)) };
459
- case "readdir":
460
- return { result: await fs.readdir(path(0)) };
461
- case "mkdir":
462
- await fs.mkdir(path(0), { recursive: true });
463
- return { result: true };
464
- case "rm":
465
- await fs.rm(path(0), { recursive: true, force: args[1] === true });
466
- return { result: true };
467
- case "rename":
468
- await fs.rename(path(0), path(1));
469
- return { result: true };
470
- case "realpath":
471
- return { result: await realpath(fs, String(args[0])) };
472
- case "symlink":
473
- await fs.symlink(String(args[0]), path(1));
474
- return { result: true };
475
- case "readlink":
476
- return { result: await fs.readlink(path(0)) };
477
- default:
478
- return { result: await fs.exists(path(0)) };
428
+ var OpenFile = class {
429
+ refs = 1;
430
+ file;
431
+ constructor(file) {
432
+ this.file = file;
479
433
  }
434
+ retain() {
435
+ this.refs += 1;
436
+ return this;
437
+ }
438
+ release() {
439
+ this.refs -= 1;
440
+ if (this.refs === 0) return this.file.close();
441
+ }
442
+ };
443
+ function openPipe(capacity) {
444
+ const pipe = new KernelPipe(capacity);
445
+ pipe.openRead();
446
+ pipe.openWrite();
447
+ return {
448
+ read: new OpenFile({
449
+ read: async (max, signal) => {
450
+ try {
451
+ return await pipe.read(max, signal);
452
+ } catch (e) {
453
+ throw new KernelError(e.code);
454
+ }
455
+ },
456
+ close: () => pipe.closeRead(),
457
+ poll: () => ({ readable: pipe.readReady, writable: false, hangup: pipe.writersGone }),
458
+ changed: (signal) => pipe.changed(signal)
459
+ }),
460
+ write: new OpenFile({
461
+ write: async (bytes, signal) => {
462
+ try {
463
+ return await pipe.write(bytes, signal);
464
+ } catch (e) {
465
+ throw new KernelError(e.code);
466
+ }
467
+ },
468
+ close: () => pipe.closeWrite(),
469
+ poll: () => ({ readable: false, writable: pipe.writeReady, hangup: pipe.readersGone }),
470
+ changed: (signal) => pipe.changed(signal)
471
+ })
472
+ };
480
473
  }
481
- function signalOf(host) {
482
- return host.signal ?? ((name2) => {
483
- throw fsError("EINVAL", `this kernel names no signal ${name2}`);
474
+ function bytesSource(data) {
475
+ let offset = 0;
476
+ return new OpenFile({
477
+ read: async (max) => {
478
+ const out = data.subarray(offset, offset + max);
479
+ offset += out.length;
480
+ return out;
481
+ },
482
+ close: () => {
483
+ }
484
484
  });
485
485
  }
486
- function killGroups(host, pgids) {
487
- if (pgids.length === 0) return;
488
- void host.launcher().then((launcher) => {
489
- for (const pgid of pgids) launcher.kill(-pgid, SIG.KILL);
486
+ function sinkFile(onData) {
487
+ return new OpenFile({
488
+ write: async (bytes) => {
489
+ onData(bytes.slice());
490
+ return bytes.length;
491
+ },
492
+ close: () => {
493
+ }
490
494
  });
491
495
  }
492
- function joinable(l, pgid, ours) {
493
- const sid = l.groupSession(pgid);
494
- if (sid === void 0) throw fsError("ESRCH", `no process group ${pgid}`);
495
- if (!ours(sid)) throw fsError("EPERM", `process group ${pgid} is in another session`);
496
+ function nullFile() {
497
+ return new OpenFile({
498
+ read: async () => new Uint8Array(0),
499
+ write: async (bytes) => bytes.length,
500
+ close: () => {
501
+ }
502
+ });
496
503
  }
497
- function serveClient(port, host) {
498
- let state = "new";
499
- const terminals = /* @__PURE__ */ new Map();
500
- const groups = /* @__PURE__ */ new Set();
501
- const sessions = /* @__PURE__ */ new Set();
502
- const watches = /* @__PURE__ */ new Map();
503
- let net;
504
- const ended = Promise.withResolvers();
505
- const send = (message2, transfer = []) => {
506
- if (state !== "closed") port.postMessage(message2, transfer);
504
+ function deviceFile(device, access) {
505
+ const read = async (max) => {
506
+ if (device === "null") return new Uint8Array(0);
507
+ const out = new Uint8Array(max);
508
+ if (device === "urandom") {
509
+ for (let at = 0; at < max; at += 65536) {
510
+ crypto.getRandomValues(out.subarray(at, Math.min(max, at + 65536)));
511
+ }
512
+ }
513
+ return out;
507
514
  };
508
- const reply = (id, body, transfer) => send({ id, ...body }, transfer);
509
- function detach(kill = false, bye = "the slicc-kernel detached this client") {
510
- if (state === "closed") return;
511
- send({ bye });
512
- state = "closed";
513
- net?.close();
514
- for (const unwatch of watches.values()) unwatch();
515
- watches.clear();
516
- for (const session of terminals.values()) session.close();
517
- terminals.clear();
518
- if (kill) killGroups(host, [...groups]);
519
- groups.clear();
520
- port.close?.();
521
- ended.resolve();
515
+ return new OpenFile({
516
+ ...access !== "write" ? { read } : {},
517
+ ...access !== "read" ? { write: async (bytes) => bytes.length } : {},
518
+ seek: async () => 0,
519
+ heldMeta: { device, ...access ? { access } : {} },
520
+ close: () => {
521
+ }
522
+ });
523
+ }
524
+ function heldFile(meta) {
525
+ return new OpenFile({ held: true, ...meta ? { heldMeta: meta } : {}, close: () => {
526
+ } });
527
+ }
528
+ function kernelFdKind(file) {
529
+ if (file.held) return "held";
530
+ if (file.heldMeta && "device" in file.heldMeta) return "device";
531
+ if (file.tty) return "tty";
532
+ return file.seek ? "file" : "stream";
533
+ }
534
+ var FdTable = class _FdTable {
535
+ static MAX_FDS = 1024;
536
+ fds = /* @__PURE__ */ new Map();
537
+ cloexec = /* @__PURE__ */ new Set();
538
+ status = /* @__PURE__ */ new Map();
539
+ get(fd) {
540
+ const file = this.fds.get(fd);
541
+ if (!file) throw new KernelError("EBADF");
542
+ return file;
522
543
  }
523
- async function hello(message2) {
524
- const refused = versionError(message2.protocol);
525
- if (refused) {
526
- send({ hello: { protocol: PROTOCOL, error: refused } });
527
- detach();
528
- return;
544
+ stdioTerminal() {
545
+ for (const fd of [0, 1, 2]) {
546
+ const tty = this.fds.get(fd)?.file.tty;
547
+ if (tty) return tty;
529
548
  }
530
- const launcher = await host.launcher();
531
- const transport = host.transport?.() ?? launcher.transport;
532
- net = serveTransport({ postMessage: (m, t) => send(m, t) }, transport);
533
- state = "open";
534
- if (message2.lock && host.locks) void host.locks.request(message2.lock, () => detach());
535
- send({
536
- hello: {
537
- protocol: PROTOCOL,
538
- traits: transport.traits,
539
- ...host.lock ? { lock: host.lock } : {}
540
- }
541
- });
549
+ return void 0;
542
550
  }
543
- async function terminal(req, l) {
544
- const session = await l.openTerminal(req.argv, {
545
- ...req.options,
546
- onData: (bytes) => reply(req.id, { fd: 1, bytes: bytes.slice() })
547
- });
548
- if (state === "closed") {
549
- session.close();
550
- } else {
551
- terminals.set(req.id, session);
552
- sessions.add(session.pid);
553
- reply(req.id, { started: session.pid });
554
- }
555
- try {
556
- return { result: await session.exited };
557
- } finally {
558
- terminals.delete(req.id);
559
- }
551
+ numbers() {
552
+ return [...this.fds.keys()].sort((a, b) => a - b);
560
553
  }
561
- async function spawn(req, l) {
562
- const out = (fd) => (bytes) => reply(req.id, { fd, bytes: bytes.slice() });
563
- if (req.options.pgid !== void 0) joinable(l, req.options.pgid, (sid) => sessions.has(sid));
564
- let leader;
565
- const result = await l.run(req.argv, {
566
- ...req.options,
567
- collect: false,
568
- onStdout: out(1),
569
- onStderr: out(2),
570
- onStarted: (pid) => {
571
- leader = pid;
572
- if (state === "closed") return;
573
- groups.add(req.options.pgid ?? pid);
574
- if (req.options.pgid === void 0) sessions.add(pid);
575
- reply(req.id, { started: pid });
576
- }
577
- });
578
- if (leader === void 0) throw fsError("ENOENT", `${req.argv[0] ?? ""}: command not found`);
579
- if (!l.list().some((p) => p.pgid === leader)) groups.delete(leader);
580
- return { result: result.status };
554
+ has(fd) {
555
+ return this.fds.has(fd);
581
556
  }
582
- async function handle(req) {
583
- if (host.scope === "transport" && req.op !== "detach") {
584
- throw fsError("EPERM", `${req.op}: this client may only use the network transport`);
585
- }
586
- const l = await host.launcher();
587
- switch (req.op) {
588
- case "spawn":
589
- return spawn(req, l);
590
- case "open-terminal":
591
- return terminal(req, l);
592
- case "terminal": {
593
- const session = terminals.get(req.terminal);
594
- if (!session) throw new Error(`no terminal ${req.terminal}`);
595
- act(session, req, signalOf(host));
596
- return { result: true };
597
- }
598
- case "kill":
599
- if (!l.kill(req.pid, signalOf(host)(req.signal))) throw fsError("ESRCH", `pid ${req.pid}`);
600
- return { result: true };
601
- case "ps":
602
- return {
603
- result: l.list().map(({ tid: _, ...entry }) => entry)
604
- };
605
- case "fs":
606
- if (!FS_METHODS.includes(req.method)) {
607
- throw new Error(`unknown file system call ${req.method}`);
608
- }
609
- return fsCall(l.fs, req.method, req.args);
610
- case "watch": {
611
- const unwatch = l.watchers.watch(
612
- req.paths,
613
- { recursive: req.recursive },
614
- (change) => send({ watch: req.id, change })
615
- );
616
- watches.set(req.id, unwatch);
617
- return { result: req.id };
618
- }
619
- case "unwatch":
620
- watches.get(req.watch)?.();
621
- watches.delete(req.watch);
622
- return { result: true };
623
- case "mount":
624
- return { result: await l.mount(req.spec) };
625
- case "umount":
626
- l.umount(req.target);
627
- return { result: true };
628
- case "mounts":
629
- return { result: l.mounts.list() };
630
- case "detach":
631
- return { result: true };
632
- default:
633
- throw new Error(`unknown op ${req.op}`);
634
- }
557
+ setCloseOnExec(fd) {
558
+ this.get(fd);
559
+ this.cloexec.add(fd);
635
560
  }
636
- port.addEventListener("message", (event) => {
637
- const data = event.data;
638
- if (state === "closed") return;
639
- if (data.hello) {
640
- if (state === "new") {
641
- hello(data.hello).catch((err) => {
642
- send({ hello: { protocol: PROTOCOL, error: String(err?.message ?? err) } });
643
- detach();
644
- });
561
+ clearCloseOnExec(fd) {
562
+ this.get(fd);
563
+ this.cloexec.delete(fd);
564
+ }
565
+ closesOnExec(fd) {
566
+ return this.cloexec.has(fd);
567
+ }
568
+ setStatusFlags(fd, flags) {
569
+ this.get(fd);
570
+ this.status.set(fd, flags);
571
+ }
572
+ statusFlags(fd) {
573
+ return this.status.get(fd);
574
+ }
575
+ install(file, min = 0) {
576
+ for (let fd = min; fd < _FdTable.MAX_FDS; fd++) {
577
+ if (!this.fds.has(fd)) {
578
+ this.fds.set(fd, file);
579
+ return fd;
645
580
  }
646
- return;
647
581
  }
648
- if (state !== "open") return;
649
- if (data.net !== void 0) {
650
- net?.answer(data);
651
- return;
582
+ void Promise.resolve(file.release()).catch(() => void 0);
583
+ throw new KernelError("EMFILE");
584
+ }
585
+ installAt(fd, file) {
586
+ if (fd < 0 || fd >= _FdTable.MAX_FDS) {
587
+ void Promise.resolve(file.release()).catch(() => void 0);
588
+ throw new KernelError("EBADF");
652
589
  }
653
- const req = data;
654
- handle(req).then(
655
- ({ result, transfer }) => {
656
- reply(req.id, { result }, transfer);
657
- if (req.op === "detach") detach(req.kill === true);
658
- },
659
- (err) => {
660
- const code = err?.code;
661
- reply(req.id, {
662
- error: err instanceof Error ? err.message : String(err),
663
- ...typeof code === "string" ? { code } : {}
664
- });
665
- }
666
- );
667
- });
668
- port.addEventListener("close", () => detach());
669
- port.start?.();
670
- return { detach, closed: ended.promise };
671
- }
672
-
673
- // src/fs/opfs.ts
674
- var S_IFDIR = 16384;
675
- var S_IFREG = 32768;
676
- var S_IFLNK = 40960;
677
- var PERM_MASK = 4095;
678
- var READ_ATTEMPTS = 3;
679
- var MAX_LINKS2 = 40;
680
- var CHECK_MS = 1e3;
681
- var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
682
- var ERRNO_BY_DOM_ERROR = {
683
- NotFoundError: "ENOENT",
684
- TypeMismatchError: "ENOTDIR",
685
- InvalidModificationError: "ENOTEMPTY",
686
- NoModificationAllowedError: "EBUSY",
687
- TypeError: "EINVAL",
688
- NotAllowedError: "EACCES"
590
+ const previous = this.fds.get(fd);
591
+ this.fds.set(fd, file);
592
+ this.cloexec.delete(fd);
593
+ this.status.delete(fd);
594
+ void Promise.resolve(previous?.release()).catch(() => void 0);
595
+ }
596
+ dup(fd, min = 0) {
597
+ return this.install(this.get(fd).retain(), min);
598
+ }
599
+ dup2(oldFd, newFd) {
600
+ const file = this.get(oldFd);
601
+ if (oldFd !== newFd) this.installAt(newFd, file.retain());
602
+ return newFd;
603
+ }
604
+ close(fd) {
605
+ const file = this.get(fd);
606
+ this.fds.delete(fd);
607
+ this.cloexec.delete(fd);
608
+ this.status.delete(fd);
609
+ return file.release();
610
+ }
611
+ fork() {
612
+ const child = new _FdTable();
613
+ for (const [fd, file] of this.fds) child.fds.set(fd, file.retain());
614
+ for (const fd of this.cloexec) child.cloexec.add(fd);
615
+ for (const [fd, flags] of this.status) child.status.set(fd, flags);
616
+ return child;
617
+ }
618
+ async closeAll() {
619
+ const files = [...this.fds.values()];
620
+ this.fds.clear();
621
+ this.cloexec.clear();
622
+ this.status.clear();
623
+ await Promise.allSettled(files.map((file) => Promise.resolve().then(() => file.release())));
624
+ }
689
625
  };
690
- function translate(err, path) {
691
- if (typeof err?.code === "string") return err;
692
- const name2 = err?.name;
693
- return fsError(typeof name2 === "string" && ERRNO_BY_DOM_ERROR[name2] || "EIO", path);
694
- }
695
- function missing(err) {
696
- const code = err?.code;
697
- return code === "ENOENT" || code === "ENOTDIR";
698
- }
699
- function split(path) {
700
- return path.split("/").filter(Boolean);
701
- }
702
- function parentOf2(path) {
703
- return path.slice(0, path.lastIndexOf("/")) || "/";
704
- }
705
- function baseOf(path) {
706
- return path.slice(path.lastIndexOf("/") + 1);
707
- }
708
- function prefixes(path) {
709
- const parts = split(path);
710
- return parts.map((_, i) => `/${parts.slice(0, i + 1).join("/")}`);
711
- }
712
- function within(path, root) {
713
- return path === root || path.startsWith(`${root}/`);
626
+
627
+ // src/kernel/socket.ts
628
+ var SOMAXCONN = 4096;
629
+ var EPHEMERAL_FIRST = 32768;
630
+ var EPHEMERAL_LAST = 60999;
631
+ var SOL_SOCKET = 1;
632
+ var SO_TYPE = 3;
633
+ var SO_ERROR = 4;
634
+ var SO_SNDBUF = 7;
635
+ var SO_RCVBUF = 8;
636
+ var SO_ACCEPTCONN = 30;
637
+ var SO_DOMAIN = 39;
638
+ var SOCK_STREAM = 1;
639
+ var AF_UNIX = 1;
640
+ var AF_INET = 2;
641
+ var SHUT_RD = 0;
642
+ var SHUT_WR = 1;
643
+ var SHUT_RDWR = 2;
644
+ function isLoopback(host) {
645
+ return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
714
646
  }
715
- function isHandle(child) {
716
- return "kind" in child;
647
+ async function pipeCall(op) {
648
+ try {
649
+ return await op();
650
+ } catch (e) {
651
+ if (e instanceof PipeError) throw new KernelError(e.code);
652
+ throw e;
653
+ }
717
654
  }
718
- var OpfsFs = class {
719
- root;
720
- meta;
721
- dirs = /* @__PURE__ */ new Map();
722
- checked = /* @__PURE__ */ new WeakMap();
723
- channel;
724
- constructor(root, meta = new MemoryMeta(), channel) {
725
- this.root = root;
726
- this.meta = meta;
727
- this.channel = channel === void 0 ? void 0 : new BroadcastChannel(channel);
728
- if (this.channel) {
729
- this.channel.onmessage = ({ data }) => {
730
- for (const path of data) this.forget(path);
655
+ var Changes = class {
656
+ waiters = [];
657
+ wait(signal) {
658
+ if (signal?.aborted) return Promise.reject(new KernelError("EINTR"));
659
+ return new Promise((resolve, reject) => {
660
+ const onAbort = () => {
661
+ this.waiters = this.waiters.filter((w) => w !== waiter);
662
+ reject(new KernelError("EINTR"));
731
663
  };
732
- this.channel.unref?.();
733
- }
664
+ const waiter = () => {
665
+ signal?.removeEventListener("abort", onAbort);
666
+ resolve();
667
+ };
668
+ this.waiters.push(waiter);
669
+ signal?.addEventListener("abort", onAbort, { once: true });
670
+ });
734
671
  }
735
- changed(...paths) {
736
- for (const path of paths) this.forget(path);
737
- this.channel?.postMessage(paths);
672
+ wake() {
673
+ const waiters = this.waiters;
674
+ this.waiters = [];
675
+ for (const resolve of waiters) resolve();
738
676
  }
739
- resolvePath(base, path) {
740
- return resolvePath(base, path);
677
+ };
678
+ function link(a, b) {
679
+ const ab = new KernelPipe();
680
+ const ba = new KernelPipe();
681
+ for (const pipe of [ab, ba]) {
682
+ pipe.openRead();
683
+ pipe.openWrite();
741
684
  }
742
- async walk(parts, from, start) {
743
- let dir = start;
744
- for (let at = from; at < parts.length; at++) {
745
- dir = await dir.getDirectoryHandle(parts[at]);
746
- this.dirs.set(`/${parts.slice(0, at + 1).join("/")}`, dir);
747
- this.checked.set(dir, performance.now());
748
- }
749
- return dir;
685
+ a.attach({ rx: ba, tx: ab });
686
+ b.attach({ rx: ab, tx: ba });
687
+ }
688
+ var KernelSocket = class _KernelSocket {
689
+ net;
690
+ domain;
691
+ state = "open";
692
+ local;
693
+ peer;
694
+ link;
695
+ readShut = false;
696
+ writeShut = false;
697
+ queue = [];
698
+ backlog = 0;
699
+ bound = false;
700
+ changes = new Changes();
701
+ options = /* @__PURE__ */ new Map();
702
+ constructor(net, domain) {
703
+ this.net = net;
704
+ this.domain = domain;
750
705
  }
751
- async dir(path) {
752
- const parts = split(path);
753
- let from = parts.length;
754
- let cached;
755
- for (; from > 0 && !cached; from--)
756
- cached = this.dirs.get(`/${parts.slice(0, from).join("/")}`);
757
- if (cached) from++;
758
- const at = `/${parts.slice(0, from).join("/")}`;
759
- if (cached && !await this.current(at, cached)) {
760
- this.forget(at);
761
- cached = void 0;
762
- from = 0;
706
+ attach(link2) {
707
+ this.link = link2;
708
+ this.state = "connected";
709
+ this.changes.wake();
710
+ }
711
+ get listening() {
712
+ return this.state === "listening";
713
+ }
714
+ connection() {
715
+ if (!this.link || this.state !== "connected") throw new KernelError("ENOTCONN");
716
+ return this.link;
717
+ }
718
+ async read(max, signal) {
719
+ const { rx } = this.connection();
720
+ if (this.readShut || max <= 0) return new Uint8Array(0);
721
+ return pipeCall(() => rx.read(max, signal));
722
+ }
723
+ async peek(max, signal) {
724
+ const { rx } = this.connection();
725
+ if (this.readShut || max <= 0) return new Uint8Array(0);
726
+ return pipeCall(() => rx.peek(max, signal));
727
+ }
728
+ async write(bytes, signal) {
729
+ const { tx } = this.connection();
730
+ if (this.writeShut) throw new KernelError("EPIPE");
731
+ return pipeCall(() => tx.write(bytes, signal));
732
+ }
733
+ poll() {
734
+ if (this.state === "listening") {
735
+ return { readable: this.queue.length > 0, writable: false, hangup: false };
763
736
  }
764
- try {
765
- return await this.walk(parts, from, cached ?? this.root);
766
- } catch (err) {
767
- throw translate(err, path);
737
+ if (this.state !== "connected" || !this.link) {
738
+ return { readable: false, writable: false, hangup: true };
768
739
  }
740
+ const { rx, tx } = this.link;
741
+ return {
742
+ readable: this.readShut || rx.readReady,
743
+ writable: this.writeShut || tx.writeReady,
744
+ hangup: rx.writersGone && tx.readersGone
745
+ };
769
746
  }
770
- async current(path, dir) {
771
- if (performance.now() - (this.checked.get(dir) ?? -CHECK_MS) < CHECK_MS) return true;
772
- const where = await this.root.resolve(dir).catch(() => null);
773
- if (where === null || `/${where.join("/")}` !== path) return false;
774
- this.checked.set(dir, performance.now());
775
- return true;
747
+ changed(signal) {
748
+ const own = this.changes.wait(signal);
749
+ if (this.state !== "connected" || !this.link) return own;
750
+ const waits = [own, this.link.rx.changed(signal), this.link.tx.changed(signal)];
751
+ for (const wait of waits) wait.catch(() => void 0);
752
+ return pipeCall(() => Promise.race(waits));
776
753
  }
777
- forget(path) {
778
- for (const key of [...this.dirs.keys()]) if (within(key, path)) this.dirs.delete(key);
754
+ bind(addr) {
755
+ if (this.state !== "open" || this.local) throw new KernelError("EINVAL");
756
+ if (addr.family !== this.domain) throw new KernelError("EAFNOSUPPORT");
757
+ this.local = this.net.bind(this, addr);
758
+ this.bound = true;
779
759
  }
780
- async inDir(path, op) {
781
- const dir = await this.dir(path);
782
- try {
783
- return await op(dir);
784
- } catch (err) {
785
- if (err?.name !== "NotFoundError" || await this.current(path, dir))
786
- throw err;
787
- this.forget(path);
788
- return op(await this.dir(path));
760
+ listen(backlog) {
761
+ if (this.state === "connected") throw new KernelError("EISCONN");
762
+ if (this.state !== "open" && this.state !== "listening") throw new KernelError("EINVAL");
763
+ if (!this.local) {
764
+ if (this.domain === "unix") throw new KernelError("EINVAL");
765
+ this.bind({ family: "inet", host: "0.0.0.0", port: 0 });
789
766
  }
767
+ this.backlog = Math.min(Math.max(backlog, 1), SOMAXCONN);
768
+ this.state = "listening";
790
769
  }
791
- async child(parent, name2, kind) {
792
- return kind === "file" ? parent.getFileHandle(name2) : parent.getDirectoryHandle(name2);
770
+ async accept(signal, nonblock = false) {
771
+ for (; ; ) {
772
+ if (this.state !== "listening") throw new KernelError("EINVAL");
773
+ const next = this.queue.shift();
774
+ if (next) return next;
775
+ if (nonblock) throw new KernelError("EAGAIN");
776
+ await this.changes.wait(signal);
777
+ }
793
778
  }
794
- async parent(path) {
795
- const parts = split(path);
796
- const name2 = parts.pop();
797
- if (name2 === void 0) throw fsError("EBUSY", path);
798
- return [`/${parts.join("/")}`, name2];
779
+ offer(server) {
780
+ if (this.state !== "listening" || this.queue.length >= this.backlog) return false;
781
+ this.queue.push(server);
782
+ this.changes.wake();
783
+ return true;
799
784
  }
800
- async handle(path) {
801
- if (split(path).length === 0) return this.root;
802
- const [dir, name2] = await this.parent(path);
803
- const known = this.dirs.has(path);
804
- return this.inDir(dir, async (parent) => {
805
- const [first, second] = known ? ["directory", "file"] : ["file", "directory"];
806
- try {
807
- return await this.child(parent, name2, first);
808
- } catch (err) {
809
- if (err?.name !== "TypeMismatchError") throw err;
810
- }
811
- return this.child(parent, name2, second);
812
- }).catch((err) => {
813
- throw translate(err, path);
814
- });
785
+ connect(addr) {
786
+ if (this.state === "connected") throw new KernelError("EISCONN");
787
+ if (this.state !== "open") throw new KernelError("EINVAL");
788
+ if (addr.family !== this.domain) throw new KernelError("EAFNOSUPPORT");
789
+ const target = this.net.target(addr);
790
+ if (!this.local) {
791
+ if (addr.family === "inet") {
792
+ this.local = this.net.bind(this, { family: "inet", host: "127.0.0.1", port: 0 });
793
+ this.bound = true;
794
+ } else this.local = { family: "unix", path: "" };
795
+ }
796
+ const server = new _KernelSocket(this.net, this.domain);
797
+ server.local = addr.family === "inet" ? { ...addr, host: canonicalHost(addr.host) } : addr;
798
+ server.peer = this.local;
799
+ this.peer = server.local;
800
+ link(this, server);
801
+ if (!target.offer(server)) {
802
+ this.unlink();
803
+ server.close();
804
+ throw new KernelError("ECONNREFUSED");
805
+ }
815
806
  }
816
- real(path) {
817
- return this.handle(path).then(
818
- () => true,
819
- () => false
820
- );
807
+ unlink() {
808
+ const link2 = this.link;
809
+ if (link2) {
810
+ link2.rx.closeRead();
811
+ link2.tx.closeWrite();
812
+ }
813
+ this.link = void 0;
814
+ this.peer = void 0;
815
+ this.state = "open";
821
816
  }
822
- async linkAlong(path) {
823
- for (const entry of await this.meta.get(prefixes(path))) {
824
- if (entry?.link === void 0) continue;
825
- if (!await this.real(entry.path) && await this.real(parentOf2(entry.path))) return entry;
817
+ shutdown(how) {
818
+ const { rx, tx } = this.connection();
819
+ if (how !== SHUT_RD && how !== SHUT_WR && how !== SHUT_RDWR) throw new KernelError("EINVAL");
820
+ if (how !== SHUT_WR && !this.readShut) {
821
+ this.readShut = true;
822
+ rx.closeRead();
826
823
  }
827
- return void 0;
824
+ if (how !== SHUT_RD && !this.writeShut) {
825
+ this.writeShut = true;
826
+ tx.closeWrite();
827
+ }
828
+ this.changes.wake();
828
829
  }
829
- async locate(path, follow) {
830
- let current = path;
831
- for (let hops = 0; hops <= MAX_LINKS2; hops++) {
832
- try {
833
- return { path: current, handle: await this.handle(current) };
834
- } catch (err) {
835
- if (!missing(err)) throw err;
836
- const link2 = await this.linkAlong(current);
837
- if (!link2) throw err;
838
- if (link2.path === current && !follow) return { path: current, link: link2 };
839
- const rest = current.slice(link2.path.length);
840
- current = resolvePath(parentOf2(link2.path), `${link2.link}${rest}`);
830
+ getOption(level, name2) {
831
+ if (level === SOL_SOCKET) {
832
+ switch (name2) {
833
+ case SO_TYPE:
834
+ return SOCK_STREAM;
835
+ case SO_ERROR:
836
+ return 0;
837
+ case SO_ACCEPTCONN:
838
+ return this.state === "listening" ? 1 : 0;
839
+ case SO_DOMAIN:
840
+ return this.domain === "inet" ? AF_INET : AF_UNIX;
841
+ case SO_SNDBUF:
842
+ case SO_RCVBUF:
843
+ return this.options.get(`${level}:${name2}`) ?? PIPE_CAPACITY;
841
844
  }
842
845
  }
843
- throw fsError("ELOOP", path);
846
+ return this.options.get(`${level}:${name2}`) ?? 0;
844
847
  }
845
- mode(path, directory, meta) {
846
- const fallback = directory || EXECUTABLE.test(path) ? 493 : 420;
847
- return (directory ? S_IFDIR : S_IFREG) | (meta?.mode ?? fallback);
848
+ setOption(level, name2, value) {
849
+ this.options.set(`${level}:${name2}`, value);
848
850
  }
849
- async statOf(path, handle, meta) {
850
- const directory = handle.kind === "directory";
851
- const file = directory ? void 0 : await handle.getFile();
852
- const modified = file?.lastModified ?? 0;
853
- const mtime = meta?.mtimeMs !== void 0 && meta.mtimeFor === modified ? meta.mtimeMs : modified;
854
- return {
855
- isFile: !directory,
856
- isDirectory: directory,
857
- isSymbolicLink: false,
858
- size: file?.size ?? 0,
859
- mode: this.mode(path, directory, meta),
860
- mtime: new Date(mtime),
861
- atime: new Date(meta?.atimeMs ?? mtime),
862
- ctime: new Date(Math.max(meta?.ctimeMs ?? mtime, modified)),
863
- ino: meta?.ino ?? inodeOf(path)
864
- };
851
+ close() {
852
+ const was = this.state;
853
+ this.state = "closed";
854
+ if (was === "listening") {
855
+ for (const pending of this.queue.splice(0)) pending.close();
856
+ }
857
+ if (this.link) {
858
+ if (!this.readShut) this.link.rx.closeRead();
859
+ if (!this.writeShut) this.link.tx.closeWrite();
860
+ this.readShut = true;
861
+ this.writeShut = true;
862
+ }
863
+ if (this.bound && this.local) this.net.release(this.local, this);
864
+ this.bound = false;
865
+ this.changes.wake();
865
866
  }
866
- linkStat(entry) {
867
- const time2 = new Date(entry.mtimeMs ?? 0);
868
- return {
869
- isFile: false,
870
- isDirectory: false,
871
- isSymbolicLink: true,
872
- size: new TextEncoder().encode(entry.link).length,
873
- mode: S_IFLNK | 511,
874
- mtime: time2,
875
- atime: time2,
876
- ctime: new Date(entry.ctimeMs ?? 0),
877
- ino: entry.ino ?? inodeOf(entry.path)
878
- };
867
+ static pair(net, domain) {
868
+ const a = new _KernelSocket(net, domain);
869
+ const b = new _KernelSocket(net, domain);
870
+ const name2 = domain === "inet" ? { family: "inet", host: "127.0.0.1", port: 0 } : { family: "unix", path: "" };
871
+ a.local = b.local = a.peer = b.peer = name2;
872
+ link(a, b);
873
+ return [a, b];
879
874
  }
880
- async describe(found) {
881
- if (found.link) return this.linkStat(found.link);
882
- const [meta] = await this.meta.get([found.path]);
883
- return this.statOf(found.path, found.handle, meta);
875
+ };
876
+ function canonicalHost(host) {
877
+ return host === "0.0.0.0" ? "127.0.0.1" : host;
878
+ }
879
+ function keyOf(addr) {
880
+ return addr.family === "inet" ? `inet:${addr.port}` : `unix:${addr.path}`;
881
+ }
882
+ var LoopbackNet = class {
883
+ bound = /* @__PURE__ */ new Map();
884
+ activators = /* @__PURE__ */ new Map();
885
+ nextEphemeral = EPHEMERAL_FIRST;
886
+ activate(addr, start) {
887
+ this.activators.set(keyOf(addr), start);
884
888
  }
885
- async stat(path) {
886
- return this.describe(await this.locate(path, true));
889
+ socket(domain) {
890
+ return new KernelSocket(this, domain);
887
891
  }
888
- async lstat(path) {
889
- return this.describe(await this.locate(path, false));
892
+ listen(addr, backlog = 128) {
893
+ const socket = this.socket(addr.family);
894
+ socket.bind(addr);
895
+ socket.listen(backlog);
896
+ return socket;
890
897
  }
891
- async exists(path) {
892
- return this.locate(path, true).then(
893
- () => true,
894
- () => false
895
- );
898
+ connect(addr) {
899
+ const socket = this.socket(addr.family);
900
+ socket.connect(addr);
901
+ return socket;
896
902
  }
897
- async readFileBuffer(path) {
898
- const handle = (await this.locate(path, true)).handle;
899
- if (handle.kind === "directory") throw fsError("EISDIR", path);
900
- for (let attempt = 1; ; attempt++) {
903
+ bind(socket, addr) {
904
+ if (addr.family === "unix") {
905
+ if (addr.path === "") throw new KernelError("EINVAL");
906
+ } else if (addr.host !== "0.0.0.0" && !isLoopback(addr.host)) {
907
+ throw new KernelError("EADDRNOTAVAIL");
908
+ }
909
+ const named = addr.family === "inet" && addr.port === 0 ? { ...addr, port: this.ephemeral() } : addr;
910
+ const key = keyOf(named);
911
+ if (this.bound.has(key)) throw new KernelError("EADDRINUSE");
912
+ this.bound.set(key, socket);
913
+ return named;
914
+ }
915
+ release(addr, socket) {
916
+ const key = keyOf(addr);
917
+ if (this.bound.get(key) === socket) this.bound.delete(key);
918
+ }
919
+ target(addr) {
920
+ if (addr.family === "inet" && !isLoopback(canonicalHost(addr.host))) {
921
+ throw new KernelError("ENETUNREACH");
922
+ }
923
+ const key = keyOf(addr);
924
+ if (!this.bound.has(key)) this.activators.get(key)?.();
925
+ const socket = this.bound.get(key);
926
+ if (!socket?.listening) throw new KernelError("ECONNREFUSED");
927
+ return socket;
928
+ }
929
+ ephemeral() {
930
+ const span = EPHEMERAL_LAST - EPHEMERAL_FIRST + 1;
931
+ for (let i = 0; i < span; i++) {
932
+ const port = this.nextEphemeral;
933
+ this.nextEphemeral = port === EPHEMERAL_LAST ? EPHEMERAL_FIRST : port + 1;
934
+ if (!this.bound.has(`inet:${port}`)) return port;
935
+ }
936
+ throw new KernelError("EADDRINUSE");
937
+ }
938
+ };
939
+
940
+ // src/kernel/dial.ts
941
+ var CHUNK = 64 * 1024;
942
+ var LOOPBACK_NAMES = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
943
+ function dialSocket(net, port, host = "127.0.0.1") {
944
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new KernelError("EINVAL");
945
+ const target = LOOPBACK_NAMES.has(host.toLowerCase()) ? "127.0.0.1" : host;
946
+ return net.connect({ family: "inet", host: target, port });
947
+ }
948
+ var codeOf = (err) => {
949
+ const code = err?.code;
950
+ return typeof code === "string" ? code : "EIO";
951
+ };
952
+ function serveSocket(socket, port, onClose) {
953
+ let closed = false;
954
+ let writes = Promise.resolve();
955
+ let credit = 0;
956
+ let granted2;
957
+ const send = (message2, transfer) => {
958
+ if (!closed) port.postMessage(message2, transfer);
959
+ };
960
+ const close = (reset = false) => {
961
+ if (closed) return;
962
+ if (reset) send({ error: "ECONNRESET" });
963
+ closed = true;
964
+ granted2?.();
965
+ socket.close();
966
+ port.close();
967
+ onClose?.();
968
+ };
969
+ const write = (data) => {
970
+ writes = writes.then(async () => {
971
+ if (closed) return;
901
972
  try {
902
- return new Uint8Array(await (await handle.getFile()).arrayBuffer());
973
+ await socket.write(data);
974
+ send({ ack: true });
903
975
  } catch (err) {
904
- if (err?.name !== "NotReadableError" || attempt >= READ_ATTEMPTS) {
905
- throw translate(err, path);
906
- }
907
- await new Promise((resolve) => setTimeout(resolve, attempt * 10));
976
+ send({ error: codeOf(err) });
977
+ }
978
+ });
979
+ };
980
+ port.addEventListener("message", ({ data }) => {
981
+ if (data instanceof Uint8Array) write(data);
982
+ else if ("more" in data) {
983
+ credit++;
984
+ granted2?.();
985
+ } else if ("end" in data) {
986
+ writes = writes.then(() => {
987
+ if (!closed) socket.shutdown(SHUT_WR);
988
+ });
989
+ } else if ("close" in data) close();
990
+ });
991
+ port.start?.();
992
+ const turn = () => credit > 0 || closed ? Promise.resolve() : new Promise((r) => granted2 = r);
993
+ void (async () => {
994
+ try {
995
+ for (; ; ) {
996
+ await turn();
997
+ if (closed) return;
998
+ const chunk2 = await socket.read(CHUNK);
999
+ if (chunk2.length === 0) break;
1000
+ credit--;
1001
+ send(chunk2, [chunk2.buffer]);
1002
+ }
1003
+ send({ end: true });
1004
+ } catch (err) {
1005
+ send({ error: codeOf(err) });
1006
+ }
1007
+ })();
1008
+ return () => close(true);
1009
+ }
1010
+
1011
+ // src/kernel/signals.ts
1012
+ var SIG = {
1013
+ HUP: 1,
1014
+ INT: 2,
1015
+ QUIT: 3,
1016
+ ILL: 4,
1017
+ TRAP: 5,
1018
+ ABRT: 6,
1019
+ BUS: 7,
1020
+ FPE: 8,
1021
+ KILL: 9,
1022
+ USR1: 10,
1023
+ SEGV: 11,
1024
+ USR2: 12,
1025
+ PIPE: 13,
1026
+ ALRM: 14,
1027
+ TERM: 15,
1028
+ STKFLT: 16,
1029
+ CHLD: 17,
1030
+ CONT: 18,
1031
+ STOP: 19,
1032
+ TSTP: 20,
1033
+ TTIN: 21,
1034
+ TTOU: 22,
1035
+ URG: 23,
1036
+ XCPU: 24,
1037
+ XFSZ: 25,
1038
+ VTALRM: 26,
1039
+ PROF: 27,
1040
+ WINCH: 28,
1041
+ POLL: 29,
1042
+ PWR: 30,
1043
+ SYS: 31
1044
+ };
1045
+ var IGNORED_BY_DEFAULT = /* @__PURE__ */ new Set([SIG.CHLD, SIG.URG, SIG.WINCH]);
1046
+ var STOPPING = /* @__PURE__ */ new Set([SIG.STOP, SIG.TSTP, SIG.TTIN, SIG.TTOU]);
1047
+ function defaultAction(sig) {
1048
+ if (sig === SIG.CONT) return "continue";
1049
+ if (STOPPING.has(sig)) return "stop";
1050
+ return IGNORED_BY_DEFAULT.has(sig) ? "ignore" : "terminate";
1051
+ }
1052
+ function isSignal(sig) {
1053
+ return Number.isInteger(sig) && sig >= 1 && sig <= 31;
1054
+ }
1055
+ function sigbit(sig) {
1056
+ return 1 << sig;
1057
+ }
1058
+ var SIGNAL_BY_NAME = {
1059
+ SIGINT: SIG.INT,
1060
+ SIGTERM: SIG.TERM,
1061
+ SIGKILL: SIG.KILL,
1062
+ SIGSTOP: SIG.STOP,
1063
+ SIGCONT: SIG.CONT
1064
+ };
1065
+
1066
+ // src/body-idle.ts
1067
+ var BODY_IDLE_MS = 5 * 60 * 1e3;
1068
+
1069
+ // src/transport.ts
1070
+ var MAX_REQUEST_BODY = 64 * 1024 * 1024;
1071
+ function serveTransport(peer, transport) {
1072
+ const open = /* @__PURE__ */ new Map();
1073
+ const fail = (nid, e) => {
1074
+ open.delete(nid);
1075
+ const { status, code } = e;
1076
+ peer.postMessage({
1077
+ net: "error",
1078
+ nid,
1079
+ message: e instanceof Error ? e.message : String(e),
1080
+ ...typeof status === "number" ? { status } : {},
1081
+ ...typeof code === "string" ? { code } : {}
1082
+ });
1083
+ };
1084
+ const start = async (call) => {
1085
+ const abort = new AbortController();
1086
+ const entry = { abort };
1087
+ open.set(call.nid, entry);
1088
+ try {
1089
+ const response = await transport.fetch({
1090
+ url: call.url,
1091
+ method: call.method,
1092
+ headers: call.headers,
1093
+ ...call.body ? { body: call.body } : {},
1094
+ signal: abort.signal
1095
+ });
1096
+ if (!open.has(call.nid)) {
1097
+ await response.cancel();
1098
+ return;
908
1099
  }
1100
+ open.set(call.nid, { abort, body: response.body[Symbol.asyncIterator](), response });
1101
+ peer.postMessage({
1102
+ net: "head",
1103
+ nid: call.nid,
1104
+ status: response.status,
1105
+ statusText: response.statusText,
1106
+ headers: response.headers
1107
+ });
1108
+ } catch (e) {
1109
+ fail(call.nid, e);
909
1110
  }
910
- }
911
- async readFile(path) {
912
- return new TextDecoder().decode(await this.readFileBuffer(path));
913
- }
914
- async writeTarget(path) {
1111
+ };
1112
+ const read = async (nid) => {
1113
+ const body = open.get(nid)?.body;
1114
+ if (!body) return;
915
1115
  try {
916
- return [(await this.locate(path, true)).path, true];
917
- } catch (err) {
918
- if (!missing(err)) throw err;
1116
+ const next = await body.next();
1117
+ if (next.done) {
1118
+ open.delete(nid);
1119
+ peer.postMessage({ net: "end", nid });
1120
+ } else {
1121
+ const bytes = next.value.slice();
1122
+ peer.postMessage({ net: "chunk", nid, bytes }, [bytes.buffer]);
1123
+ }
1124
+ } catch (e) {
1125
+ fail(nid, e);
919
1126
  }
920
- const [entry] = await this.meta.get([path]);
921
- const wanted = entry?.link === void 0 ? path : resolvePath(parentOf2(path), entry.link);
922
- return [resolvePath((await this.locate(parentOf2(wanted), true)).path, baseOf(wanted)), false];
923
- }
924
- async writeFile(path, content) {
925
- const [target, existed] = await this.writeTarget(path);
926
- const [dir, name2] = await this.parent(target);
927
- let file;
928
- try {
929
- file = await this.inDir(dir, (parent) => parent.getFileHandle(name2, { create: true }));
930
- } catch (err) {
931
- const code = err?.name === "TypeMismatchError" ? "EISDIR" : null;
932
- throw code ? fsError(code, path) : translate(err, path);
1127
+ };
1128
+ const drop = (nid) => {
1129
+ const entry = open.get(nid);
1130
+ open.delete(nid);
1131
+ entry?.abort.abort();
1132
+ void entry?.response?.cancel().catch(() => void 0);
1133
+ };
1134
+ return {
1135
+ answer(call) {
1136
+ if (call.net === "fetch") void start(call);
1137
+ else if (call.net === "read") void read(call.nid);
1138
+ else drop(call.nid);
1139
+ },
1140
+ close() {
1141
+ for (const nid of [...open.keys()]) drop(nid);
933
1142
  }
934
- if (!existed && content.length === 0) return;
935
- const writable = await file.createWritable();
936
- await writable.write(content);
937
- await writable.close();
938
- }
939
- async listing(path) {
940
- const found = await this.locate(path, true);
941
- const handle = found.handle;
942
- if (handle.kind !== "directory") throw fsError("ENOTDIR", path);
943
- const children2 = /* @__PURE__ */ new Map();
944
- for await (const child of handle.values()) children2.set(child.name, child);
945
- for (const link2 of await this.meta.links(found.path)) {
946
- const name2 = baseOf(link2.path);
947
- if (!children2.has(name2)) children2.set(name2, link2);
1143
+ };
1144
+ }
1145
+
1146
+ // src/client/serve-client.ts
1147
+ var MAX_LINKS = 40;
1148
+ function act(session, req, signal) {
1149
+ if (req.action === "write") session.write(req.bytes);
1150
+ else if (req.action === "resize") session.resize(req.cols, req.rows);
1151
+ else if (req.action === "signal") session.signal(signal(req.signal));
1152
+ else session.close();
1153
+ }
1154
+ async function realpath(fs, path) {
1155
+ let parts = fs.resolvePath("/", path).split("/").filter(Boolean);
1156
+ let resolved = "";
1157
+ for (let hops = 0; parts.length > 0; ) {
1158
+ const [name2, ...rest] = parts;
1159
+ const next = `${resolved}/${name2}`;
1160
+ if (!(await fs.lstat(next)).isSymbolicLink) {
1161
+ resolved = next;
1162
+ parts = rest;
1163
+ continue;
948
1164
  }
949
- return [found.path, [...children2].sort(([a], [b]) => a < b ? -1 : 1)];
1165
+ if (++hops > MAX_LINKS) throw fsError("ELOOP", path);
1166
+ const target = await fs.readlink(next);
1167
+ const base = target.startsWith("/") ? "/" : resolved || "/";
1168
+ parts = [...fs.resolvePath(base, target).split("/").filter(Boolean), ...rest];
1169
+ resolved = "";
950
1170
  }
951
- async readdir(path) {
952
- return (await this.listing(path))[1].map(([name2]) => name2);
1171
+ return resolved || "/";
1172
+ }
1173
+ async function fsCall(fs, method, args) {
1174
+ const path = (i) => fs.resolvePath("/", String(args[i]));
1175
+ switch (method) {
1176
+ case "readFile": {
1177
+ const bytes = (await fs.readFileBuffer(path(0))).slice();
1178
+ return { result: bytes, transfer: [bytes.buffer] };
1179
+ }
1180
+ case "writeFile":
1181
+ await fs.writeFile(path(0), args[1]);
1182
+ return { result: true };
1183
+ case "stat":
1184
+ return { result: await fs.stat(path(0)) };
1185
+ case "lstat":
1186
+ return { result: await fs.lstat(path(0)) };
1187
+ case "readdir":
1188
+ return { result: await fs.readdir(path(0)) };
1189
+ case "mkdir":
1190
+ await fs.mkdir(path(0), { recursive: true });
1191
+ return { result: true };
1192
+ case "rm":
1193
+ await fs.rm(path(0), { recursive: true, force: args[1] === true });
1194
+ return { result: true };
1195
+ case "rename":
1196
+ await fs.rename(path(0), path(1));
1197
+ return { result: true };
1198
+ case "realpath":
1199
+ return { result: await realpath(fs, String(args[0])) };
1200
+ case "symlink":
1201
+ await fs.symlink(String(args[0]), path(1));
1202
+ return { result: true };
1203
+ case "readlink":
1204
+ return { result: await fs.readlink(path(0)) };
1205
+ default:
1206
+ return { result: await fs.exists(path(0)) };
953
1207
  }
954
- async readdirStat(path) {
955
- const [dir, children2] = await this.listing(path);
956
- const base = dir === "/" ? "" : dir;
957
- const metas = await this.meta.get(children2.map(([name2]) => `${base}/${name2}`));
958
- return Promise.all(
959
- children2.map(async ([name2, child], i) => {
960
- if (!isHandle(child)) return [name2, this.linkStat(child)];
961
- const stat = await this.statOf(`${base}/${name2}`, child, metas[i]).catch(() => null);
962
- return [name2, stat];
963
- })
964
- );
1208
+ }
1209
+ function signalOf(host) {
1210
+ return host.signal ?? ((name2) => {
1211
+ throw fsError("EINVAL", `this kernel names no signal ${name2}`);
1212
+ });
1213
+ }
1214
+ function dialFor(l, req, open) {
1215
+ const socket = dialSocket(l.net, req.port, req.host);
1216
+ const { port1, port2 } = new MessageChannel();
1217
+ open.set(
1218
+ req.id,
1219
+ serveSocket(socket, port1, () => open.delete(req.id))
1220
+ );
1221
+ return { result: port2, transfer: [port2] };
1222
+ }
1223
+ function killGroups(host, pgids) {
1224
+ if (pgids.length === 0) return;
1225
+ void host.launcher().then((launcher) => {
1226
+ for (const pgid of pgids) launcher.kill(-pgid, SIG.KILL);
1227
+ });
1228
+ }
1229
+ function joinable(l, pgid, ours) {
1230
+ const sid = l.groupSession(pgid);
1231
+ if (sid === void 0) throw fsError("ESRCH", `no process group ${pgid}`);
1232
+ if (!ours(sid)) throw fsError("EPERM", `process group ${pgid} is in another session`);
1233
+ }
1234
+ function serveClient(port, host) {
1235
+ let state = "new";
1236
+ const terminals = /* @__PURE__ */ new Map();
1237
+ const groups = /* @__PURE__ */ new Set();
1238
+ const sessions = /* @__PURE__ */ new Set();
1239
+ const watches = /* @__PURE__ */ new Map();
1240
+ let net;
1241
+ const ended = Promise.withResolvers();
1242
+ const send = (message2, transfer = []) => {
1243
+ if (state !== "closed") port.postMessage(message2, transfer);
1244
+ };
1245
+ const reply = (id, body, transfer) => send({ id, ...body }, transfer);
1246
+ function detach(kill = false, bye = "the slicc-kernel detached this client") {
1247
+ if (state === "closed") return;
1248
+ send({ bye });
1249
+ state = "closed";
1250
+ net?.close();
1251
+ for (const unwatch of watches.values()) unwatch();
1252
+ watches.clear();
1253
+ for (const session of terminals.values()) session.close();
1254
+ terminals.clear();
1255
+ if (kill) killGroups(host, [...groups]);
1256
+ groups.clear();
1257
+ port.close?.();
1258
+ ended.resolve();
965
1259
  }
966
- async mkdir(path, options = {}) {
967
- let found;
968
- try {
969
- found = await this.locate(path, true);
970
- } catch (err) {
971
- if (!missing(err)) throw err;
1260
+ async function hello(message2) {
1261
+ const refused = versionError(message2.protocol);
1262
+ if (refused) {
1263
+ send({ hello: { protocol: PROTOCOL, error: refused } });
1264
+ detach();
1265
+ return;
1266
+ }
1267
+ const launcher = await host.launcher();
1268
+ const transport = host.transport?.() ?? launcher.transport;
1269
+ net = serveTransport({ postMessage: (m, t) => send(m, t) }, transport);
1270
+ state = "open";
1271
+ if (message2.lock && host.locks) void host.locks.request(message2.lock, () => detach());
1272
+ send({
1273
+ hello: {
1274
+ protocol: PROTOCOL,
1275
+ traits: transport.traits,
1276
+ ...host.lock ? { lock: host.lock } : {}
1277
+ }
1278
+ });
1279
+ }
1280
+ async function terminal(req, l) {
1281
+ const session = await l.openTerminal(req.argv, {
1282
+ ...req.options,
1283
+ onData: (bytes) => reply(req.id, { fd: 1, bytes: bytes.slice() })
1284
+ });
1285
+ if (state === "closed") {
1286
+ session.close();
1287
+ } else {
1288
+ terminals.set(req.id, session);
1289
+ sessions.add(session.pid);
1290
+ reply(req.id, { started: session.pid });
972
1291
  }
973
- if (found) {
974
- const directory = found.handle?.kind === "directory";
975
- if (options.recursive && directory) return;
976
- throw fsError(options.recursive ? "ENOTDIR" : "EEXIST", path);
1292
+ try {
1293
+ return { result: await session.exited };
1294
+ } finally {
1295
+ terminals.delete(req.id);
977
1296
  }
978
- const [entry] = await this.meta.get([path]);
979
- if (entry?.link !== void 0) throw fsError("EEXIST", path);
980
- if (options.recursive) await this.mkdir(parentOf2(path), options);
981
- const parent = await this.locate(parentOf2(path), true);
982
- if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
983
- await parent.handle.getDirectoryHandle(baseOf(path), { create: true });
984
1297
  }
985
- async rm(path, options = {}) {
986
- let found;
987
- try {
988
- found = await this.locate(path, false);
989
- } catch (err) {
990
- const error = translate(err, path);
991
- if (options.force && error.code === "ENOENT") return;
992
- throw error;
1298
+ async function spawn(req, l) {
1299
+ const out = (fd) => (bytes) => reply(req.id, { fd, bytes: bytes.slice() });
1300
+ if (req.options.pgid !== void 0) joinable(l, req.options.pgid, (sid) => sessions.has(sid));
1301
+ let leader;
1302
+ const result = await l.run(req.argv, {
1303
+ ...req.options,
1304
+ collect: false,
1305
+ onStdout: out(1),
1306
+ onStderr: out(2),
1307
+ onStarted: (pid) => {
1308
+ leader = pid;
1309
+ if (state === "closed") return;
1310
+ groups.add(req.options.pgid ?? pid);
1311
+ if (req.options.pgid === void 0) sessions.add(pid);
1312
+ reply(req.id, { started: pid });
1313
+ }
1314
+ });
1315
+ if (leader === void 0) throw fsError("ENOENT", `${req.argv[0] ?? ""}: command not found`);
1316
+ if (!l.list().some((p) => p.pgid === leader)) groups.delete(leader);
1317
+ return { result: result.status };
1318
+ }
1319
+ async function handle(req) {
1320
+ if (host.scope === "transport" && req.op !== "detach") {
1321
+ throw fsError("EPERM", `${req.op}: this client may only use the network transport`);
993
1322
  }
994
- if (found.handle) {
995
- const [dir, name2] = await this.parent(found.path);
996
- try {
997
- await this.inDir(
998
- dir,
999
- (parent) => parent.removeEntry(name2, { recursive: options.recursive === true })
1323
+ const l = await host.launcher();
1324
+ switch (req.op) {
1325
+ case "spawn":
1326
+ return spawn(req, l);
1327
+ case "open-terminal":
1328
+ return terminal(req, l);
1329
+ case "terminal": {
1330
+ const session = terminals.get(req.terminal);
1331
+ if (!session) throw new Error(`no terminal ${req.terminal}`);
1332
+ act(session, req, signalOf(host));
1333
+ return { result: true };
1334
+ }
1335
+ case "kill":
1336
+ if (!l.kill(req.pid, signalOf(host)(req.signal))) throw fsError("ESRCH", `pid ${req.pid}`);
1337
+ return { result: true };
1338
+ case "ps":
1339
+ return {
1340
+ result: l.list().map(({ tid: _, ...entry }) => entry)
1341
+ };
1342
+ case "fs":
1343
+ if (!FS_METHODS.includes(req.method)) {
1344
+ throw new Error(`unknown file system call ${req.method}`);
1345
+ }
1346
+ return fsCall(l.fs, req.method, req.args);
1347
+ case "watch": {
1348
+ const unwatch = l.watchers.watch(
1349
+ req.paths,
1350
+ { recursive: req.recursive },
1351
+ (change) => send({ watch: req.id, change })
1000
1352
  );
1001
- } catch (err) {
1002
- throw translate(err, path);
1353
+ watches.set(req.id, unwatch);
1354
+ return { result: req.id };
1003
1355
  }
1004
- this.changed(found.path);
1356
+ case "unwatch":
1357
+ watches.get(req.watch)?.();
1358
+ watches.delete(req.watch);
1359
+ return { result: true };
1360
+ case "mount":
1361
+ return { result: await l.mount(req.spec) };
1362
+ case "umount":
1363
+ l.umount(req.target);
1364
+ return { result: true };
1365
+ case "mounts":
1366
+ return { result: l.mounts.list() };
1367
+ case "dial":
1368
+ return dialFor(l, req, watches);
1369
+ case "detach":
1370
+ return { result: true };
1371
+ default:
1372
+ throw new Error(`unknown op ${req.op}`);
1005
1373
  }
1006
- await this.meta.remove([found.path]);
1007
1374
  }
1008
- async rename(from, to) {
1009
- if (from === to) return;
1010
- const source = await this.locate(from, false);
1011
- this.forget(source.path);
1012
- const parent = await this.locate(parentOf2(to), true);
1013
- const destination = resolvePath(parent.path, baseOf(to));
1014
- const sourceStat = await this.describe(source);
1015
- if (sourceStat.isDirectory && within(destination, source.path)) throw fsError("EINVAL", to);
1016
- const target = await this.locate(destination, false).catch((err) => {
1017
- if (missing(err)) return null;
1018
- throw err;
1019
- });
1020
- const targetStat = target && await this.describe(target);
1021
- if (targetStat?.isDirectory && !sourceStat.isDirectory) throw fsError("EISDIR", to);
1022
- if (targetStat && !targetStat.isDirectory && sourceStat.isDirectory) {
1023
- throw fsError("ENOTDIR", to);
1024
- }
1025
- if (targetStat?.isDirectory && (await this.readdir(destination)).length > 0) {
1026
- throw fsError("ENOTEMPTY", to);
1375
+ port.addEventListener("message", (event) => {
1376
+ const data = event.data;
1377
+ if (state === "closed") return;
1378
+ if (data.hello) {
1379
+ if (state === "new") {
1380
+ hello(data.hello).catch((err) => {
1381
+ send({ hello: { protocol: PROTOCOL, error: String(err?.message ?? err) } });
1382
+ detach();
1383
+ });
1384
+ }
1385
+ return;
1027
1386
  }
1028
- const now = Date.now();
1029
- const carry = (entry) => ({
1030
- ino: inodeOf(source.path),
1031
- ...entry,
1032
- path: destination,
1033
- ctimeMs: now
1034
- });
1035
- if (source.link) {
1036
- if (target?.handle) await this.rm(destination);
1037
- await this.meta.move(source.path, destination, carry);
1387
+ if (state !== "open") return;
1388
+ if (data.net !== void 0) {
1389
+ net?.answer(data);
1038
1390
  return;
1039
1391
  }
1040
- const dir = parent.handle;
1041
- const name2 = baseOf(destination);
1042
- const swap = targetStat?.isDirectory === true;
1043
- const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
1044
- this.forget(destination);
1045
- const timed = await this.explicitTimes(source.path);
1046
- const moved2 = await this.place(source.handle, dir, staging);
1047
- if (swap) await this.rm(destination, { recursive: true });
1048
- await this.meta.move(source.path, destination, carry);
1049
- if (!moved2) await this.rm(source.path, { recursive: true });
1050
- let copied = !moved2;
1051
- if (swap) {
1052
- const staged = await dir.getDirectoryHandle(staging);
1053
- const placed = await this.place(staged, dir, name2);
1054
- if (!placed) await dir.removeEntry(staging, { recursive: true });
1055
- copied ||= !placed;
1392
+ const req = data;
1393
+ handle(req).then(
1394
+ ({ result, transfer }) => {
1395
+ reply(req.id, { result }, transfer);
1396
+ if (req.op === "detach") detach(req.kill === true);
1397
+ },
1398
+ (err) => {
1399
+ const code = err?.code;
1400
+ reply(req.id, {
1401
+ error: err instanceof Error ? err.message : String(err),
1402
+ ...typeof code === "string" ? { code } : {}
1403
+ });
1404
+ }
1405
+ );
1406
+ });
1407
+ port.addEventListener("close", () => detach());
1408
+ port.start?.();
1409
+ return { detach, closed: ended.promise };
1410
+ }
1411
+
1412
+ // src/fs/opfs.ts
1413
+ var S_IFDIR = 16384;
1414
+ var S_IFREG = 32768;
1415
+ var S_IFLNK = 40960;
1416
+ var PERM_MASK = 4095;
1417
+ var READ_ATTEMPTS = 3;
1418
+ var MAX_LINKS2 = 40;
1419
+ var CHECK_MS = 1e3;
1420
+ var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
1421
+ var ERRNO_BY_DOM_ERROR = {
1422
+ NotFoundError: "ENOENT",
1423
+ TypeMismatchError: "ENOTDIR",
1424
+ InvalidModificationError: "ENOTEMPTY",
1425
+ NoModificationAllowedError: "EBUSY",
1426
+ TypeError: "EINVAL",
1427
+ NotAllowedError: "EACCES"
1428
+ };
1429
+ function translate(err, path) {
1430
+ if (typeof err?.code === "string") return err;
1431
+ const name2 = err?.name;
1432
+ return fsError(typeof name2 === "string" && ERRNO_BY_DOM_ERROR[name2] || "EIO", path);
1433
+ }
1434
+ function missing(err) {
1435
+ const code = err?.code;
1436
+ return code === "ENOENT" || code === "ENOTDIR";
1437
+ }
1438
+ function split(path) {
1439
+ return path.split("/").filter(Boolean);
1440
+ }
1441
+ function parentOf2(path) {
1442
+ return path.slice(0, path.lastIndexOf("/")) || "/";
1443
+ }
1444
+ function baseOf(path) {
1445
+ return path.slice(path.lastIndexOf("/") + 1);
1446
+ }
1447
+ function prefixes(path) {
1448
+ const parts = split(path);
1449
+ return parts.map((_, i) => `/${parts.slice(0, i + 1).join("/")}`);
1450
+ }
1451
+ function within(path, root) {
1452
+ return path === root || path.startsWith(`${root}/`);
1453
+ }
1454
+ function isHandle(child) {
1455
+ return "kind" in child;
1456
+ }
1457
+ var OpfsFs = class {
1458
+ root;
1459
+ meta;
1460
+ dirs = /* @__PURE__ */ new Map();
1461
+ checked = /* @__PURE__ */ new WeakMap();
1462
+ channel;
1463
+ constructor(root, meta = new MemoryMeta(), channel) {
1464
+ this.root = root;
1465
+ this.meta = meta;
1466
+ this.channel = channel === void 0 ? void 0 : new BroadcastChannel(channel);
1467
+ if (this.channel) {
1468
+ this.channel.onmessage = ({ data }) => {
1469
+ for (const path of data) this.forget(path);
1470
+ };
1471
+ this.channel.unref?.();
1056
1472
  }
1057
- if (copied) await this.restamp(destination, timed);
1058
- this.changed(source.path, destination);
1059
1473
  }
1060
- async modified(path) {
1061
- const handle = await this.handle(path);
1062
- return handle.kind === "file" ? (await handle.getFile()).lastModified : 0;
1474
+ changed(...paths) {
1475
+ for (const path of paths) this.forget(path);
1476
+ this.channel?.postMessage(paths);
1063
1477
  }
1064
- async explicitTimes(path) {
1065
- const timed = [];
1066
- for (const entry of await this.meta.under(path)) {
1067
- if (entry.mtimeFor === void 0 || entry.link !== void 0) continue;
1068
- const current = await this.modified(entry.path).catch(() => void 0);
1069
- if (current === entry.mtimeFor) timed.push(entry.path.slice(path.length));
1070
- }
1071
- return timed;
1478
+ resolvePath(base, path) {
1479
+ return resolvePath(base, path);
1072
1480
  }
1073
- async restamp(path, timed) {
1074
- for (const rest of timed) {
1075
- const mtimeFor = await this.modified(path + rest);
1076
- await this.meta.update(path + rest, (entry) => entry && { ...entry, mtimeFor });
1481
+ async walk(parts, from, start) {
1482
+ let dir = start;
1483
+ for (let at = from; at < parts.length; at++) {
1484
+ dir = await dir.getDirectoryHandle(parts[at]);
1485
+ this.dirs.set(`/${parts.slice(0, at + 1).join("/")}`, dir);
1486
+ this.checked.set(dir, performance.now());
1077
1487
  }
1488
+ return dir;
1078
1489
  }
1079
- async place(handle, dir, name2) {
1080
- const moved2 = await handle.move?.(dir, name2).then(
1081
- () => true,
1082
- () => false
1083
- );
1084
- if (moved2) return true;
1490
+ async dir(path) {
1491
+ const parts = split(path);
1492
+ let from = parts.length;
1493
+ let cached;
1494
+ for (; from > 0 && !cached; from--)
1495
+ cached = this.dirs.get(`/${parts.slice(0, from).join("/")}`);
1496
+ if (cached) from++;
1497
+ const at = `/${parts.slice(0, from).join("/")}`;
1498
+ if (cached && !await this.current(at, cached)) {
1499
+ this.forget(at);
1500
+ cached = void 0;
1501
+ from = 0;
1502
+ }
1085
1503
  try {
1086
- await this.copy(handle, dir, name2);
1504
+ return await this.walk(parts, from, cached ?? this.root);
1087
1505
  } catch (err) {
1088
- if (handle.kind === "directory") await dir.removeEntry(name2, { recursive: true });
1089
- throw translate(err, name2);
1090
- }
1091
- return false;
1092
- }
1093
- async copy(handle, dir, name2) {
1094
- if (handle.kind === "file") {
1095
- const writable = await (await dir.getFileHandle(name2, { create: true })).createWritable();
1096
- await writable.write(await handle.getFile());
1097
- await writable.close();
1098
- return;
1099
- }
1100
- const target = await dir.getDirectoryHandle(name2, { create: true });
1101
- for await (const child of handle.values()) await this.copy(child, target, child.name);
1102
- }
1103
- async symlink(target, path) {
1104
- const parent = await this.locate(parentOf2(path), true);
1105
- if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
1106
- const at = resolvePath(parent.path, baseOf(path));
1107
- if (await this.locate(at, false).then(
1108
- () => true,
1109
- (err) => !missing(err)
1110
- )) {
1111
- throw fsError("EEXIST", path);
1112
- }
1113
- const now = Date.now();
1114
- await this.meta.update(at, () => ({
1115
- path: at,
1116
- link: target,
1117
- dir: parent.path,
1118
- ino: inodeOf(at),
1119
- mtimeMs: now,
1120
- ctimeMs: now
1121
- }));
1122
- }
1123
- async readlink(path) {
1124
- const found = await this.locate(path, false);
1125
- if (found.link) return found.link.link;
1126
- throw fsError("EINVAL", path);
1127
- }
1128
- async chmod(path, mode) {
1129
- const found = await this.locate(path, true);
1130
- const ctimeMs = Date.now();
1131
- await this.meta.update(found.path, (entry) => ({
1132
- ...entry,
1133
- path: found.path,
1134
- mode: mode & PERM_MASK,
1135
- ctimeMs
1136
- }));
1137
- }
1138
- async utimes(path, atime, mtime) {
1139
- const found = await this.locate(path, true);
1140
- const handle = found.handle;
1141
- const file = handle.kind === "file" ? await handle.getFile() : void 0;
1142
- const ctimeMs = Date.now();
1143
- await this.meta.update(found.path, (entry) => ({
1144
- ...entry,
1145
- path: found.path,
1146
- atimeMs: atime.getTime(),
1147
- mtimeMs: mtime.getTime(),
1148
- mtimeFor: file?.lastModified ?? 0,
1149
- ctimeMs
1150
- }));
1151
- }
1152
- async reconcile() {
1153
- let removed = 0;
1154
- for (const entry of await this.meta.all()) {
1155
- const real = await this.real(entry.path);
1156
- const stale = entry.link === void 0 ? !real : real || !await this.real(parentOf2(entry.path));
1157
- if (!stale) continue;
1158
- await this.meta.update(entry.path, () => void 0);
1159
- removed++;
1506
+ throw translate(err, path);
1160
1507
  }
1161
- return removed;
1162
- }
1163
- };
1164
-
1165
- // src/kernel/pipe.ts
1166
- var PIPE_CAPACITY = 64 * 1024;
1167
- var PipeError = class extends Error {
1168
- code;
1169
- constructor(code) {
1170
- super(code);
1171
- this.code = code;
1172
- }
1173
- };
1174
- var KernelPipe = class {
1175
- chunks = [];
1176
- size = 0;
1177
- readers = 0;
1178
- writers = 0;
1179
- waiters = [];
1180
- capacity;
1181
- constructor(capacity = PIPE_CAPACITY) {
1182
- this.capacity = capacity;
1183
- }
1184
- get buffered() {
1185
- return this.size;
1186
- }
1187
- get readReady() {
1188
- return this.size > 0 || this.writers === 0;
1189
- }
1190
- get writeReady() {
1191
- return this.size < this.capacity || this.readers === 0;
1192
- }
1193
- get writersGone() {
1194
- return this.writers === 0;
1195
- }
1196
- get readersGone() {
1197
- return this.readers === 0;
1198
1508
  }
1199
- openRead() {
1200
- this.readers += 1;
1509
+ async current(path, dir) {
1510
+ if (performance.now() - (this.checked.get(dir) ?? -CHECK_MS) < CHECK_MS) return true;
1511
+ const where = await this.root.resolve(dir).catch(() => null);
1512
+ if (where === null || `/${where.join("/")}` !== path) return false;
1513
+ this.checked.set(dir, performance.now());
1514
+ return true;
1201
1515
  }
1202
- openWrite() {
1203
- this.writers += 1;
1516
+ forget(path) {
1517
+ for (const key of [...this.dirs.keys()]) if (within(key, path)) this.dirs.delete(key);
1204
1518
  }
1205
- closeRead() {
1206
- if (this.readers > 0) this.readers -= 1;
1207
- if (this.readers === 0) {
1208
- this.chunks = [];
1209
- this.size = 0;
1519
+ async inDir(path, op) {
1520
+ const dir = await this.dir(path);
1521
+ try {
1522
+ return await op(dir);
1523
+ } catch (err) {
1524
+ if (err?.name !== "NotFoundError" || await this.current(path, dir))
1525
+ throw err;
1526
+ this.forget(path);
1527
+ return op(await this.dir(path));
1210
1528
  }
1211
- this.wake();
1212
- }
1213
- closeWrite() {
1214
- if (this.writers > 0) this.writers -= 1;
1215
- this.wake();
1216
1529
  }
1217
- async read(max, signal) {
1218
- while (this.size === 0) {
1219
- if (this.writers === 0) return new Uint8Array(0);
1220
- await this.changed(signal);
1221
- }
1222
- const out = this.take(Math.min(max, this.size));
1223
- this.wake();
1224
- return out;
1530
+ async child(parent, name2, kind) {
1531
+ return kind === "file" ? parent.getFileHandle(name2) : parent.getDirectoryHandle(name2);
1225
1532
  }
1226
- async peek(max, signal) {
1227
- while (this.size === 0) {
1228
- if (this.writers === 0) return new Uint8Array(0);
1229
- await this.changed(signal);
1230
- }
1231
- const out = new Uint8Array(Math.min(max, this.size));
1232
- let filled = 0;
1233
- for (const chunk2 of this.chunks) {
1234
- if (filled === out.length) break;
1235
- const count = Math.min(chunk2.length, out.length - filled);
1236
- out.set(chunk2.subarray(0, count), filled);
1237
- filled += count;
1238
- }
1239
- return out;
1533
+ async parent(path) {
1534
+ const parts = split(path);
1535
+ const name2 = parts.pop();
1536
+ if (name2 === void 0) throw fsError("EBUSY", path);
1537
+ return [`/${parts.join("/")}`, name2];
1240
1538
  }
1241
- async write(bytes, signal) {
1242
- let offset = 0;
1243
- while (offset < bytes.length) {
1244
- if (this.readers === 0) throw new PipeError("EPIPE");
1245
- const room = this.capacity - this.size;
1246
- if (room === 0) {
1247
- try {
1248
- await this.changed(signal);
1249
- } catch (e) {
1250
- if (offset > 0) return offset;
1251
- throw e;
1252
- }
1253
- continue;
1539
+ async handle(path) {
1540
+ if (split(path).length === 0) return this.root;
1541
+ const [dir, name2] = await this.parent(path);
1542
+ const known = this.dirs.has(path);
1543
+ return this.inDir(dir, async (parent) => {
1544
+ const [first, second] = known ? ["directory", "file"] : ["file", "directory"];
1545
+ try {
1546
+ return await this.child(parent, name2, first);
1547
+ } catch (err) {
1548
+ if (err?.name !== "TypeMismatchError") throw err;
1254
1549
  }
1255
- const n = Math.min(room, bytes.length - offset);
1256
- this.chunks.push(bytes.slice(offset, offset + n));
1257
- this.size += n;
1258
- offset += n;
1259
- this.wake();
1550
+ return this.child(parent, name2, second);
1551
+ }).catch((err) => {
1552
+ throw translate(err, path);
1553
+ });
1554
+ }
1555
+ real(path) {
1556
+ return this.handle(path).then(
1557
+ () => true,
1558
+ () => false
1559
+ );
1560
+ }
1561
+ async linkAlong(path) {
1562
+ for (const entry of await this.meta.get(prefixes(path))) {
1563
+ if (entry?.link === void 0) continue;
1564
+ if (!await this.real(entry.path) && await this.real(parentOf2(entry.path))) return entry;
1260
1565
  }
1261
- return bytes.length;
1566
+ return void 0;
1262
1567
  }
1263
- take(n) {
1264
- const out = new Uint8Array(n);
1265
- let filled = 0;
1266
- while (filled < n) {
1267
- const head = this.chunks[0];
1268
- const count = Math.min(head.length, n - filled);
1269
- out.set(head.subarray(0, count), filled);
1270
- filled += count;
1271
- if (count === head.length) this.chunks.shift();
1272
- else this.chunks[0] = head.subarray(count);
1568
+ async locate(path, follow) {
1569
+ let current = path;
1570
+ for (let hops = 0; hops <= MAX_LINKS2; hops++) {
1571
+ try {
1572
+ return { path: current, handle: await this.handle(current) };
1573
+ } catch (err) {
1574
+ if (!missing(err)) throw err;
1575
+ const link2 = await this.linkAlong(current);
1576
+ if (!link2) throw err;
1577
+ if (link2.path === current && !follow) return { path: current, link: link2 };
1578
+ const rest = current.slice(link2.path.length);
1579
+ current = resolvePath(parentOf2(link2.path), `${link2.link}${rest}`);
1580
+ }
1273
1581
  }
1274
- this.size -= n;
1275
- return out;
1582
+ throw fsError("ELOOP", path);
1276
1583
  }
1277
- changed(signal) {
1278
- if (signal?.aborted) return Promise.reject(new PipeError("EINTR"));
1279
- return new Promise((resolve, reject) => {
1280
- const onAbort = () => {
1281
- this.waiters = this.waiters.filter((w) => w !== waiter);
1282
- reject(new PipeError("EINTR"));
1283
- };
1284
- const waiter = () => {
1285
- signal?.removeEventListener("abort", onAbort);
1286
- resolve();
1287
- };
1288
- this.waiters.push(waiter);
1289
- signal?.addEventListener("abort", onAbort, { once: true });
1290
- });
1584
+ mode(path, directory, meta) {
1585
+ const fallback = directory || EXECUTABLE.test(path) ? 493 : 420;
1586
+ return (directory ? S_IFDIR : S_IFREG) | (meta?.mode ?? fallback);
1291
1587
  }
1292
- wake() {
1293
- const waiters = this.waiters;
1294
- this.waiters = [];
1295
- for (const resolve of waiters) resolve();
1588
+ async statOf(path, handle, meta) {
1589
+ const directory = handle.kind === "directory";
1590
+ const file = directory ? void 0 : await handle.getFile();
1591
+ const modified = file?.lastModified ?? 0;
1592
+ const mtime = meta?.mtimeMs !== void 0 && meta.mtimeFor === modified ? meta.mtimeMs : modified;
1593
+ return {
1594
+ isFile: !directory,
1595
+ isDirectory: directory,
1596
+ isSymbolicLink: false,
1597
+ size: file?.size ?? 0,
1598
+ mode: this.mode(path, directory, meta),
1599
+ mtime: new Date(mtime),
1600
+ atime: new Date(meta?.atimeMs ?? mtime),
1601
+ ctime: new Date(Math.max(meta?.ctimeMs ?? mtime, modified)),
1602
+ ino: meta?.ino ?? inodeOf(path)
1603
+ };
1296
1604
  }
1297
- };
1298
-
1299
- // src/kernel/fd-table.ts
1300
- var KernelError = class extends Error {
1301
- code;
1302
- constructor(code) {
1303
- super(code);
1304
- this.code = code;
1605
+ linkStat(entry) {
1606
+ const time2 = new Date(entry.mtimeMs ?? 0);
1607
+ return {
1608
+ isFile: false,
1609
+ isDirectory: false,
1610
+ isSymbolicLink: true,
1611
+ size: new TextEncoder().encode(entry.link).length,
1612
+ mode: S_IFLNK | 511,
1613
+ mtime: time2,
1614
+ atime: time2,
1615
+ ctime: new Date(entry.ctimeMs ?? 0),
1616
+ ino: entry.ino ?? inodeOf(entry.path)
1617
+ };
1305
1618
  }
1306
- };
1307
- function pollFile(file) {
1308
- return file.poll?.() ?? { readable: !!file.read, writable: !!file.write, hangup: false };
1309
- }
1310
- var OpenFile = class {
1311
- refs = 1;
1312
- file;
1313
- constructor(file) {
1314
- this.file = file;
1619
+ async describe(found) {
1620
+ if (found.link) return this.linkStat(found.link);
1621
+ const [meta] = await this.meta.get([found.path]);
1622
+ return this.statOf(found.path, found.handle, meta);
1315
1623
  }
1316
- retain() {
1317
- this.refs += 1;
1318
- return this;
1624
+ async stat(path) {
1625
+ return this.describe(await this.locate(path, true));
1319
1626
  }
1320
- release() {
1321
- this.refs -= 1;
1322
- if (this.refs === 0) return this.file.close();
1627
+ async lstat(path) {
1628
+ return this.describe(await this.locate(path, false));
1323
1629
  }
1324
- };
1325
- function openPipe(capacity) {
1326
- const pipe = new KernelPipe(capacity);
1327
- pipe.openRead();
1328
- pipe.openWrite();
1329
- return {
1330
- read: new OpenFile({
1331
- read: async (max, signal) => {
1332
- try {
1333
- return await pipe.read(max, signal);
1334
- } catch (e) {
1335
- throw new KernelError(e.code);
1336
- }
1337
- },
1338
- close: () => pipe.closeRead(),
1339
- poll: () => ({ readable: pipe.readReady, writable: false, hangup: pipe.writersGone }),
1340
- changed: (signal) => pipe.changed(signal)
1341
- }),
1342
- write: new OpenFile({
1343
- write: async (bytes, signal) => {
1344
- try {
1345
- return await pipe.write(bytes, signal);
1346
- } catch (e) {
1347
- throw new KernelError(e.code);
1630
+ async exists(path) {
1631
+ return this.locate(path, true).then(
1632
+ () => true,
1633
+ () => false
1634
+ );
1635
+ }
1636
+ async readFileBuffer(path) {
1637
+ const handle = (await this.locate(path, true)).handle;
1638
+ if (handle.kind === "directory") throw fsError("EISDIR", path);
1639
+ for (let attempt = 1; ; attempt++) {
1640
+ try {
1641
+ return new Uint8Array(await (await handle.getFile()).arrayBuffer());
1642
+ } catch (err) {
1643
+ if (err?.name !== "NotReadableError" || attempt >= READ_ATTEMPTS) {
1644
+ throw translate(err, path);
1348
1645
  }
1349
- },
1350
- close: () => pipe.closeWrite(),
1351
- poll: () => ({ readable: false, writable: pipe.writeReady, hangup: pipe.readersGone }),
1352
- changed: (signal) => pipe.changed(signal)
1353
- })
1354
- };
1355
- }
1356
- function bytesSource(data) {
1357
- let offset = 0;
1358
- return new OpenFile({
1359
- read: async (max) => {
1360
- const out = data.subarray(offset, offset + max);
1361
- offset += out.length;
1362
- return out;
1363
- },
1364
- close: () => {
1365
- }
1366
- });
1367
- }
1368
- function sinkFile(onData) {
1369
- return new OpenFile({
1370
- write: async (bytes) => {
1371
- onData(bytes.slice());
1372
- return bytes.length;
1373
- },
1374
- close: () => {
1375
- }
1376
- });
1377
- }
1378
- function nullFile() {
1379
- return new OpenFile({
1380
- read: async () => new Uint8Array(0),
1381
- write: async (bytes) => bytes.length,
1382
- close: () => {
1383
- }
1384
- });
1385
- }
1386
- function deviceFile(device, access) {
1387
- const read = async (max) => {
1388
- if (device === "null") return new Uint8Array(0);
1389
- const out = new Uint8Array(max);
1390
- if (device === "urandom") {
1391
- for (let at = 0; at < max; at += 65536) {
1392
- crypto.getRandomValues(out.subarray(at, Math.min(max, at + 65536)));
1646
+ await new Promise((resolve) => setTimeout(resolve, attempt * 10));
1393
1647
  }
1394
1648
  }
1395
- return out;
1396
- };
1397
- return new OpenFile({
1398
- ...access !== "write" ? { read } : {},
1399
- ...access !== "read" ? { write: async (bytes) => bytes.length } : {},
1400
- seek: async () => 0,
1401
- heldMeta: { device, ...access ? { access } : {} },
1402
- close: () => {
1649
+ }
1650
+ async readFile(path) {
1651
+ return new TextDecoder().decode(await this.readFileBuffer(path));
1652
+ }
1653
+ async writeTarget(path) {
1654
+ try {
1655
+ return [(await this.locate(path, true)).path, true];
1656
+ } catch (err) {
1657
+ if (!missing(err)) throw err;
1403
1658
  }
1404
- });
1405
- }
1406
- function heldFile(meta) {
1407
- return new OpenFile({ held: true, ...meta ? { heldMeta: meta } : {}, close: () => {
1408
- } });
1409
- }
1410
- function kernelFdKind(file) {
1411
- if (file.held) return "held";
1412
- if (file.heldMeta && "device" in file.heldMeta) return "device";
1413
- if (file.tty) return "tty";
1414
- return file.seek ? "file" : "stream";
1415
- }
1416
- var FdTable = class _FdTable {
1417
- static MAX_FDS = 1024;
1418
- fds = /* @__PURE__ */ new Map();
1419
- cloexec = /* @__PURE__ */ new Set();
1420
- status = /* @__PURE__ */ new Map();
1421
- get(fd) {
1422
- const file = this.fds.get(fd);
1423
- if (!file) throw new KernelError("EBADF");
1424
- return file;
1659
+ const [entry] = await this.meta.get([path]);
1660
+ const wanted = entry?.link === void 0 ? path : resolvePath(parentOf2(path), entry.link);
1661
+ return [resolvePath((await this.locate(parentOf2(wanted), true)).path, baseOf(wanted)), false];
1662
+ }
1663
+ async writeFile(path, content) {
1664
+ const [target, existed] = await this.writeTarget(path);
1665
+ const [dir, name2] = await this.parent(target);
1666
+ let file;
1667
+ try {
1668
+ file = await this.inDir(dir, (parent) => parent.getFileHandle(name2, { create: true }));
1669
+ } catch (err) {
1670
+ const code = err?.name === "TypeMismatchError" ? "EISDIR" : null;
1671
+ throw code ? fsError(code, path) : translate(err, path);
1672
+ }
1673
+ if (!existed && content.length === 0) return;
1674
+ const writable = await file.createWritable();
1675
+ await writable.write(content);
1676
+ await writable.close();
1425
1677
  }
1426
- stdioTerminal() {
1427
- for (const fd of [0, 1, 2]) {
1428
- const tty = this.fds.get(fd)?.file.tty;
1429
- if (tty) return tty;
1678
+ async listing(path) {
1679
+ const found = await this.locate(path, true);
1680
+ const handle = found.handle;
1681
+ if (handle.kind !== "directory") throw fsError("ENOTDIR", path);
1682
+ const children2 = /* @__PURE__ */ new Map();
1683
+ for await (const child of handle.values()) children2.set(child.name, child);
1684
+ for (const link2 of await this.meta.links(found.path)) {
1685
+ const name2 = baseOf(link2.path);
1686
+ if (!children2.has(name2)) children2.set(name2, link2);
1430
1687
  }
1431
- return void 0;
1688
+ return [found.path, [...children2].sort(([a], [b]) => a < b ? -1 : 1)];
1432
1689
  }
1433
- numbers() {
1434
- return [...this.fds.keys()].sort((a, b) => a - b);
1690
+ async readdir(path) {
1691
+ return (await this.listing(path))[1].map(([name2]) => name2);
1435
1692
  }
1436
- has(fd) {
1437
- return this.fds.has(fd);
1693
+ async readdirStat(path) {
1694
+ const [dir, children2] = await this.listing(path);
1695
+ const base = dir === "/" ? "" : dir;
1696
+ const metas = await this.meta.get(children2.map(([name2]) => `${base}/${name2}`));
1697
+ return Promise.all(
1698
+ children2.map(async ([name2, child], i) => {
1699
+ if (!isHandle(child)) return [name2, this.linkStat(child)];
1700
+ const stat = await this.statOf(`${base}/${name2}`, child, metas[i]).catch(() => null);
1701
+ return [name2, stat];
1702
+ })
1703
+ );
1438
1704
  }
1439
- setCloseOnExec(fd) {
1440
- this.get(fd);
1441
- this.cloexec.add(fd);
1705
+ async mkdir(path, options = {}) {
1706
+ let found;
1707
+ try {
1708
+ found = await this.locate(path, true);
1709
+ } catch (err) {
1710
+ if (!missing(err)) throw err;
1711
+ }
1712
+ if (found) {
1713
+ const directory = found.handle?.kind === "directory";
1714
+ if (options.recursive && directory) return;
1715
+ throw fsError(options.recursive ? "ENOTDIR" : "EEXIST", path);
1716
+ }
1717
+ const [entry] = await this.meta.get([path]);
1718
+ if (entry?.link !== void 0) throw fsError("EEXIST", path);
1719
+ if (options.recursive) await this.mkdir(parentOf2(path), options);
1720
+ const parent = await this.locate(parentOf2(path), true);
1721
+ if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
1722
+ await parent.handle.getDirectoryHandle(baseOf(path), { create: true });
1442
1723
  }
1443
- clearCloseOnExec(fd) {
1444
- this.get(fd);
1445
- this.cloexec.delete(fd);
1724
+ async rm(path, options = {}) {
1725
+ let found;
1726
+ try {
1727
+ found = await this.locate(path, false);
1728
+ } catch (err) {
1729
+ const error = translate(err, path);
1730
+ if (options.force && error.code === "ENOENT") return;
1731
+ throw error;
1732
+ }
1733
+ if (found.handle) {
1734
+ const [dir, name2] = await this.parent(found.path);
1735
+ try {
1736
+ await this.inDir(
1737
+ dir,
1738
+ (parent) => parent.removeEntry(name2, { recursive: options.recursive === true })
1739
+ );
1740
+ } catch (err) {
1741
+ throw translate(err, path);
1742
+ }
1743
+ this.changed(found.path);
1744
+ }
1745
+ await this.meta.remove([found.path]);
1446
1746
  }
1447
- closesOnExec(fd) {
1448
- return this.cloexec.has(fd);
1747
+ async rename(from, to) {
1748
+ if (from === to) return;
1749
+ const source = await this.locate(from, false);
1750
+ this.forget(source.path);
1751
+ const parent = await this.locate(parentOf2(to), true);
1752
+ const destination = resolvePath(parent.path, baseOf(to));
1753
+ const sourceStat = await this.describe(source);
1754
+ if (sourceStat.isDirectory && within(destination, source.path)) throw fsError("EINVAL", to);
1755
+ const target = await this.locate(destination, false).catch((err) => {
1756
+ if (missing(err)) return null;
1757
+ throw err;
1758
+ });
1759
+ const targetStat = target && await this.describe(target);
1760
+ if (targetStat?.isDirectory && !sourceStat.isDirectory) throw fsError("EISDIR", to);
1761
+ if (targetStat && !targetStat.isDirectory && sourceStat.isDirectory) {
1762
+ throw fsError("ENOTDIR", to);
1763
+ }
1764
+ if (targetStat?.isDirectory && (await this.readdir(destination)).length > 0) {
1765
+ throw fsError("ENOTEMPTY", to);
1766
+ }
1767
+ const now = Date.now();
1768
+ const carry = (entry) => ({
1769
+ ino: inodeOf(source.path),
1770
+ ...entry,
1771
+ path: destination,
1772
+ ctimeMs: now
1773
+ });
1774
+ if (source.link) {
1775
+ if (target?.handle) await this.rm(destination);
1776
+ await this.meta.move(source.path, destination, carry);
1777
+ return;
1778
+ }
1779
+ const dir = parent.handle;
1780
+ const name2 = baseOf(destination);
1781
+ const swap = targetStat?.isDirectory === true;
1782
+ const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
1783
+ this.forget(destination);
1784
+ const timed = await this.explicitTimes(source.path);
1785
+ const moved2 = await this.place(source.handle, dir, staging);
1786
+ if (swap) await this.rm(destination, { recursive: true });
1787
+ await this.meta.move(source.path, destination, carry);
1788
+ if (!moved2) await this.rm(source.path, { recursive: true });
1789
+ let copied = !moved2;
1790
+ if (swap) {
1791
+ const staged = await dir.getDirectoryHandle(staging);
1792
+ const placed = await this.place(staged, dir, name2);
1793
+ if (!placed) await dir.removeEntry(staging, { recursive: true });
1794
+ copied ||= !placed;
1795
+ }
1796
+ if (copied) await this.restamp(destination, timed);
1797
+ this.changed(source.path, destination);
1449
1798
  }
1450
- setStatusFlags(fd, flags) {
1451
- this.get(fd);
1452
- this.status.set(fd, flags);
1799
+ async modified(path) {
1800
+ const handle = await this.handle(path);
1801
+ return handle.kind === "file" ? (await handle.getFile()).lastModified : 0;
1453
1802
  }
1454
- statusFlags(fd) {
1455
- return this.status.get(fd);
1803
+ async explicitTimes(path) {
1804
+ const timed = [];
1805
+ for (const entry of await this.meta.under(path)) {
1806
+ if (entry.mtimeFor === void 0 || entry.link !== void 0) continue;
1807
+ const current = await this.modified(entry.path).catch(() => void 0);
1808
+ if (current === entry.mtimeFor) timed.push(entry.path.slice(path.length));
1809
+ }
1810
+ return timed;
1456
1811
  }
1457
- install(file, min = 0) {
1458
- for (let fd = min; fd < _FdTable.MAX_FDS; fd++) {
1459
- if (!this.fds.has(fd)) {
1460
- this.fds.set(fd, file);
1461
- return fd;
1462
- }
1812
+ async restamp(path, timed) {
1813
+ for (const rest of timed) {
1814
+ const mtimeFor = await this.modified(path + rest);
1815
+ await this.meta.update(path + rest, (entry) => entry && { ...entry, mtimeFor });
1463
1816
  }
1464
- void Promise.resolve(file.release()).catch(() => void 0);
1465
- throw new KernelError("EMFILE");
1466
1817
  }
1467
- installAt(fd, file) {
1468
- if (fd < 0 || fd >= _FdTable.MAX_FDS) {
1469
- void Promise.resolve(file.release()).catch(() => void 0);
1470
- throw new KernelError("EBADF");
1818
+ async place(handle, dir, name2) {
1819
+ const moved2 = await handle.move?.(dir, name2).then(
1820
+ () => true,
1821
+ () => false
1822
+ );
1823
+ if (moved2) return true;
1824
+ try {
1825
+ await this.copy(handle, dir, name2);
1826
+ } catch (err) {
1827
+ if (handle.kind === "directory") await dir.removeEntry(name2, { recursive: true });
1828
+ throw translate(err, name2);
1471
1829
  }
1472
- const previous = this.fds.get(fd);
1473
- this.fds.set(fd, file);
1474
- this.cloexec.delete(fd);
1475
- this.status.delete(fd);
1476
- void Promise.resolve(previous?.release()).catch(() => void 0);
1830
+ return false;
1477
1831
  }
1478
- dup(fd, min = 0) {
1479
- return this.install(this.get(fd).retain(), min);
1832
+ async copy(handle, dir, name2) {
1833
+ if (handle.kind === "file") {
1834
+ const writable = await (await dir.getFileHandle(name2, { create: true })).createWritable();
1835
+ await writable.write(await handle.getFile());
1836
+ await writable.close();
1837
+ return;
1838
+ }
1839
+ const target = await dir.getDirectoryHandle(name2, { create: true });
1840
+ for await (const child of handle.values()) await this.copy(child, target, child.name);
1480
1841
  }
1481
- dup2(oldFd, newFd) {
1482
- const file = this.get(oldFd);
1483
- if (oldFd !== newFd) this.installAt(newFd, file.retain());
1484
- return newFd;
1842
+ async symlink(target, path) {
1843
+ const parent = await this.locate(parentOf2(path), true);
1844
+ if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
1845
+ const at = resolvePath(parent.path, baseOf(path));
1846
+ if (await this.locate(at, false).then(
1847
+ () => true,
1848
+ (err) => !missing(err)
1849
+ )) {
1850
+ throw fsError("EEXIST", path);
1851
+ }
1852
+ const now = Date.now();
1853
+ await this.meta.update(at, () => ({
1854
+ path: at,
1855
+ link: target,
1856
+ dir: parent.path,
1857
+ ino: inodeOf(at),
1858
+ mtimeMs: now,
1859
+ ctimeMs: now
1860
+ }));
1485
1861
  }
1486
- close(fd) {
1487
- const file = this.get(fd);
1488
- this.fds.delete(fd);
1489
- this.cloexec.delete(fd);
1490
- this.status.delete(fd);
1491
- return file.release();
1862
+ async readlink(path) {
1863
+ const found = await this.locate(path, false);
1864
+ if (found.link) return found.link.link;
1865
+ throw fsError("EINVAL", path);
1492
1866
  }
1493
- fork() {
1494
- const child = new _FdTable();
1495
- for (const [fd, file] of this.fds) child.fds.set(fd, file.retain());
1496
- for (const fd of this.cloexec) child.cloexec.add(fd);
1497
- for (const [fd, flags] of this.status) child.status.set(fd, flags);
1498
- return child;
1867
+ async chmod(path, mode) {
1868
+ const found = await this.locate(path, true);
1869
+ const ctimeMs = Date.now();
1870
+ await this.meta.update(found.path, (entry) => ({
1871
+ ...entry,
1872
+ path: found.path,
1873
+ mode: mode & PERM_MASK,
1874
+ ctimeMs
1875
+ }));
1499
1876
  }
1500
- async closeAll() {
1501
- const files = [...this.fds.values()];
1502
- this.fds.clear();
1503
- this.cloexec.clear();
1504
- this.status.clear();
1505
- await Promise.allSettled(files.map((file) => Promise.resolve().then(() => file.release())));
1877
+ async utimes(path, atime, mtime) {
1878
+ const found = await this.locate(path, true);
1879
+ const handle = found.handle;
1880
+ const file = handle.kind === "file" ? await handle.getFile() : void 0;
1881
+ const ctimeMs = Date.now();
1882
+ await this.meta.update(found.path, (entry) => ({
1883
+ ...entry,
1884
+ path: found.path,
1885
+ atimeMs: atime.getTime(),
1886
+ mtimeMs: mtime.getTime(),
1887
+ mtimeFor: file?.lastModified ?? 0,
1888
+ ctimeMs
1889
+ }));
1890
+ }
1891
+ async reconcile() {
1892
+ let removed = 0;
1893
+ for (const entry of await this.meta.all()) {
1894
+ const real = await this.real(entry.path);
1895
+ const stale = entry.link === void 0 ? !real : real || !await this.real(parentOf2(entry.path));
1896
+ if (!stale) continue;
1897
+ await this.meta.update(entry.path, () => void 0);
1898
+ removed++;
1899
+ }
1900
+ return removed;
1506
1901
  }
1507
1902
  };
1508
1903
 
1904
+ // src/kernel/net/loopback-names.ts
1905
+ var HOST_LOOPBACK = "host.slicc.internal";
1906
+ var PAGE_LOOPBACK = "kernel.localhost";
1907
+ var PAGE_LOOPBACK_URL = `http://{port}.${PAGE_LOOPBACK}`;
1908
+ function toHostLoopback(href) {
1909
+ const url = new URL(href);
1910
+ if (url.hostname.replace(/\.$/, "").toLowerCase() !== HOST_LOOPBACK) return href;
1911
+ url.hostname = "127.0.0.1";
1912
+ return url.href;
1913
+ }
1914
+
1509
1915
  // src/kernel/net/http1.ts
1510
1916
  var HttpError = class extends Error {
1511
1917
  status;
@@ -1790,7 +2196,7 @@ var Budget = class {
1790
2196
  for (const wake of waiters) wake();
1791
2197
  }
1792
2198
  };
1793
- function canonicalHost(host) {
2199
+ function canonicalHost2(host) {
1794
2200
  const bare = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
1795
2201
  try {
1796
2202
  return new URL(`http://${bare}/`).hostname;
@@ -1820,7 +2226,7 @@ function localV6(address) {
1820
2226
  return g[5] === 0 && g[6] === 0;
1821
2227
  }
1822
2228
  function isLoopbackHost(hostname) {
1823
- const canonical = canonicalHost(hostname);
2229
+ const canonical = canonicalHost2(hostname);
1824
2230
  if (canonical === void 0) return false;
1825
2231
  const host = canonical.replace(/\.+$/, "");
1826
2232
  if (host === "localhost" || host.endsWith(".localhost")) return true;
@@ -1893,7 +2299,7 @@ function tunnelTarget(req) {
1893
2299
  if (!match || port < 1 || port > 65535) {
1894
2300
  throw new HttpError(400, "CONNECT needs host:port");
1895
2301
  }
1896
- const host = canonicalHost(match[1]);
2302
+ const host = canonicalHost2(match[1]);
1897
2303
  if (host === void 0) throw new HttpError(400, "CONNECT needs host:port");
1898
2304
  if (isLoopbackHost(host)) {
1899
2305
  throw new HttpError(
@@ -2079,7 +2485,7 @@ var RealmProxy = class {
2079
2485
  async upstream(req, url, body, signal) {
2080
2486
  try {
2081
2487
  const fetching = this.options.transport.fetch({
2082
- url,
2488
+ url: toHostLoopback(url),
2083
2489
  method: req.method.toUpperCase(),
2084
2490
  headers: forwardRequestHeaders(req.headers),
2085
2491
  body: req.method === "GET" || req.method === "HEAD" ? void 0 : body,
@@ -2759,7 +3165,10 @@ function networkEnv() {
2759
3165
  NO_PROXY,
2760
3166
  SSL_CERT_FILE: CA_PATH,
2761
3167
  CURL_CA_BUNDLE: CA_PATH,
2762
- GIT_SSL_CAINFO: CA_PATH
3168
+ GIT_SSL_CAINFO: CA_PATH,
3169
+ SLICC_HOST_LOOPBACK: HOST_LOOPBACK,
3170
+ SLICC_PAGE_LOOPBACK: PAGE_LOOPBACK,
3171
+ SLICC_PAGE_LOOPBACK_URL: PAGE_LOOPBACK_URL
2763
3172
  };
2764
3173
  }
2765
3174
  function missingTransport() {
@@ -4590,588 +4999,292 @@ var HttpHandles = class {
4590
4999
  handle,
4591
5000
  status: response.status,
4592
5001
  statusText: response.statusText,
4593
- url: req.url,
4594
- headers: response.headers
4595
- };
4596
- return { ok: true, kind: "json", json: head };
4597
- } catch (e) {
4598
- const text = message(e);
4599
- if (text === NO_TRANSPORT) return { ok: false, errno: "ENETUNREACH", message: text };
4600
- const status = e.status;
4601
- if (typeof status !== "number" || status === 502) {
4602
- return { ok: false, errno: "ECONNREFUSED", message: text };
4603
- }
4604
- const body = new TextEncoder().encode(text);
4605
- const handle = this.add(abort, once(body), async () => void 0);
4606
- const head = {
4607
- handle,
4608
- status,
4609
- statusText: "",
4610
- url: req.url,
4611
- headers: [["content-type", "text/plain; charset=utf-8"]]
4612
- };
4613
- return { ok: true, kind: "json", json: head };
4614
- }
4615
- }
4616
- async read(handle, max) {
4617
- const entry = this.open.get(handle);
4618
- if (!entry) return { ok: false, errno: "EBADF", message: `no request ${handle}` };
4619
- const turn = entry.reading.then(() => this.take(entry, max));
4620
- entry.reading = turn.then(() => void 0);
4621
- return turn;
4622
- }
4623
- async take(entry, max) {
4624
- if (entry.left.length === 0 && !entry.done) {
4625
- try {
4626
- const next = await entry.body.next();
4627
- if (next.done) entry.done = true;
4628
- else entry.left = next.value;
4629
- } catch (e) {
4630
- entry.done = true;
4631
- const timedOut = e?.code === "ETIMEDOUT";
4632
- return { ok: false, errno: timedOut ? "ETIMEDOUT" : "EIO", message: message(e) };
4633
- }
4634
- }
4635
- const bytes = entry.left.subarray(0, Math.max(0, max));
4636
- entry.left = entry.left.subarray(bytes.length);
4637
- return { ok: true, kind: "bytes", bytes };
4638
- }
4639
- async close(handle) {
4640
- const entry = this.open.get(handle);
4641
- if (!entry) return { ok: false, errno: "EBADF", message: `no request ${handle}` };
4642
- this.open.delete(handle);
4643
- entry.abort.abort();
4644
- await entry.cancel().catch(() => void 0);
4645
- return { ok: true, kind: "void" };
4646
- }
4647
- async closeAll() {
4648
- await Promise.all([...this.open.keys()].map((handle) => this.close(handle)));
4649
- }
4650
- };
4651
-
4652
- // src/kernel/tty.ts
4653
- var ICRNL = 256;
4654
- var IUTF8 = 16384;
4655
- var OPOST = 1;
4656
- var ONLCR = 4;
4657
- var ISIG = 1;
4658
- var ICANON = 2;
4659
- var ECHO = 8;
4660
- var ECHOE = 16;
4661
- var ECHOCTL = 512;
4662
- var NOFLSH = 128;
4663
- var IEXTEN = 32768;
4664
- var VINTR = 0;
4665
- var VQUIT = 1;
4666
- var VERASE = 2;
4667
- var VKILL = 3;
4668
- var VEOF = 4;
4669
- var VSUSP = 10;
4670
- var VWERASE = 14;
4671
- function defaultTermios() {
4672
- const cCc = new Array(32).fill(0);
4673
- Object.assign(cCc, { [VINTR]: 3, [VQUIT]: 28, [VERASE]: 127, [VKILL]: 21 });
4674
- Object.assign(cCc, { [VEOF]: 4, 6: 1, 8: 17, 9: 19, [VSUSP]: 26 });
4675
- Object.assign(cCc, { 12: 18, 13: 15, [VWERASE]: 23, 15: 22 });
4676
- return {
4677
- c_iflag: ICRNL | 1024 | 8192 | IUTF8,
4678
- c_oflag: OPOST | ONLCR,
4679
- c_cflag: 15 | 48 | 128,
4680
- c_lflag: ISIG | ICANON | ECHO | ECHOE | 32 | ECHOCTL | 2048 | IEXTEN,
4681
- c_cc: cCc
4682
- };
4683
- }
4684
- var encoder2 = new TextEncoder();
4685
- function isContinuation(byte) {
4686
- return (byte & 192) === 128;
4687
- }
4688
- var KernelTty = class {
4689
- termios = defaultTermios();
4690
- cols = 80;
4691
- rows = 24;
4692
- readable = [];
4693
- line = [];
4694
- waiters = [];
4695
- hungUp = false;
4696
- name;
4697
- screen;
4698
- signal;
4699
- constructor(screen, signal) {
4700
- this.screen = screen;
4701
- this.signal = signal;
4702
- }
4703
- receive(bytes) {
4704
- for (const byte of bytes) this.key(byte);
4705
- this.wake();
4706
- }
4707
- resize(cols, rows) {
4708
- this.setSize(cols, rows);
4709
- this.signal(SIG.WINCH);
4710
- }
4711
- hangup() {
4712
- this.hungUp = true;
4713
- this.wake();
4714
- }
4715
- signalHangup() {
4716
- this.signal(SIG.HUP);
4717
- }
4718
- setSize(cols, rows) {
4719
- this.cols = cols;
4720
- this.rows = rows;
4721
- }
4722
- tcgets() {
4723
- return { ...this.termios, c_cc: [...this.termios.c_cc] };
4724
- }
4725
- tcsets(termios) {
4726
- const wasCanonical = this.canonical;
4727
- this.termios = { ...termios, c_cc: [...termios.c_cc] };
4728
- if (wasCanonical && !this.canonical && this.line.length > 0) {
4729
- this.readable.push(Uint8Array.from(this.line));
4730
- this.line = [];
4731
- this.wake();
4732
- }
4733
- }
4734
- winsize() {
4735
- return [this.rows, this.cols];
4736
- }
4737
- file() {
4738
- return new OpenFile({
4739
- read: (max, signal) => this.read(max, signal),
4740
- write: async (bytes) => {
4741
- if (this.hungUp) throw new KernelError("EIO");
4742
- this.output(bytes);
4743
- return bytes.length;
4744
- },
4745
- poll: () => ({
4746
- readable: this.readable.length > 0 || this.hungUp,
4747
- writable: true,
4748
- hangup: this.hungUp
4749
- }),
4750
- changed: (signal) => this.changed(signal),
4751
- close: () => {
4752
- },
4753
- tty: this
4754
- });
4755
- }
4756
- get canonical() {
4757
- return (this.termios.c_lflag & ICANON) !== 0;
4758
- }
4759
- cc(slot) {
4760
- return this.termios.c_cc[slot] ?? 0;
4761
- }
4762
- key(input) {
4763
- const { c_iflag, c_lflag } = this.termios;
4764
- const byte = input === 13 && c_iflag & ICRNL ? 10 : input;
4765
- if (c_lflag & ISIG && this.signalKey(byte)) return;
4766
- if (!this.canonical) {
4767
- this.readable.push(Uint8Array.of(byte));
4768
- if (c_lflag & ECHO) this.echo(byte);
4769
- return;
4770
- }
4771
- this.edit(byte);
4772
- }
4773
- signalKey(byte) {
4774
- const sig = byte === this.cc(VINTR) ? SIG.INT : byte === this.cc(VQUIT) ? SIG.QUIT : byte === this.cc(VSUSP) ? SIG.TSTP : 0;
4775
- if (!sig || byte === 0) return false;
4776
- if (this.termios.c_lflag & ECHO) this.echo(byte);
4777
- if (!(this.termios.c_lflag & NOFLSH)) {
4778
- this.line = [];
4779
- this.readable = [];
4780
- }
4781
- this.signal(sig);
4782
- return true;
4783
- }
4784
- edit(byte) {
4785
- const echo = (this.termios.c_lflag & ECHO) !== 0;
4786
- if (byte === this.cc(VERASE) || byte === 8) {
4787
- this.rubOut(this.lastCharLength());
4788
- } else if (byte === this.cc(VKILL)) {
4789
- this.rubOut(this.line.length);
4790
- } else if (byte === this.cc(VWERASE)) {
4791
- this.rubOut(this.lastWordLength());
4792
- } else if (byte === this.cc(VEOF)) {
4793
- this.readable.push(this.line.length > 0 ? Uint8Array.from(this.line) : null);
4794
- this.line = [];
4795
- } else if (byte === 10) {
4796
- this.line.push(byte);
4797
- if (echo) this.output(encoder2.encode("\n"));
4798
- this.readable.push(Uint8Array.from(this.line));
4799
- this.line = [];
4800
- } else {
4801
- this.line.push(byte);
4802
- if (echo) this.echo(byte);
4803
- }
4804
- }
4805
- lastWordLength() {
4806
- let end = this.line.length;
4807
- while (end > 0 && this.line[end - 1] === 32) end--;
4808
- while (end > 0 && this.line[end - 1] !== 32) end--;
4809
- return this.line.length - end;
4810
- }
4811
- lastCharLength() {
4812
- const { line } = this;
4813
- let n = Math.min(1, line.length);
4814
- if (this.termios.c_iflag & IUTF8) {
4815
- while (n < line.length && isContinuation(line[line.length - n] ?? 0)) n++;
4816
- }
4817
- return n;
4818
- }
4819
- rubOut(n) {
4820
- const dropped2 = this.line.splice(this.line.length - n, n);
4821
- const utf82 = (this.termios.c_iflag & IUTF8) !== 0;
4822
- const cells = utf82 ? dropped2.filter((b) => !isContinuation(b)).length : dropped2.length;
4823
- if (cells > 0 && this.termios.c_lflag & ECHO) {
4824
- this.output(encoder2.encode("\b \b".repeat(cells)), false);
4825
- }
4826
- }
4827
- echo(byte) {
4828
- if (byte < 32 && byte !== 10 && byte !== 9 && this.termios.c_lflag & ECHOCTL) {
4829
- this.output(Uint8Array.of(94, byte + 64), false);
4830
- return;
5002
+ url: req.url,
5003
+ headers: response.headers
5004
+ };
5005
+ return { ok: true, kind: "json", json: head };
5006
+ } catch (e) {
5007
+ const text = message(e);
5008
+ if (text === NO_TRANSPORT) return { ok: false, errno: "ENETUNREACH", message: text };
5009
+ const status = e.status;
5010
+ if (typeof status !== "number" || status === 502) {
5011
+ return { ok: false, errno: "ECONNREFUSED", message: text };
5012
+ }
5013
+ const body = new TextEncoder().encode(text);
5014
+ const handle = this.add(abort, once(body), async () => void 0);
5015
+ const head = {
5016
+ handle,
5017
+ status,
5018
+ statusText: "",
5019
+ url: req.url,
5020
+ headers: [["content-type", "text/plain; charset=utf-8"]]
5021
+ };
5022
+ return { ok: true, kind: "json", json: head };
4831
5023
  }
4832
- this.output(Uint8Array.of(byte));
4833
5024
  }
4834
- output(bytes, translate2 = true) {
4835
- const { c_oflag } = this.termios;
4836
- if (!translate2 || !(c_oflag & OPOST && c_oflag & ONLCR) || !bytes.includes(10)) {
4837
- this.screen.write(bytes);
4838
- return;
4839
- }
4840
- const out = [];
4841
- for (const byte of bytes) {
4842
- if (byte === 10) out.push(13);
4843
- out.push(byte);
4844
- }
4845
- this.screen.write(Uint8Array.from(out));
5025
+ async read(handle, max) {
5026
+ const entry = this.open.get(handle);
5027
+ if (!entry) return { ok: false, errno: "EBADF", message: `no request ${handle}` };
5028
+ const turn = entry.reading.then(() => this.take(entry, max));
5029
+ entry.reading = turn.then(() => void 0);
5030
+ return turn;
4846
5031
  }
4847
- async read(max, signal) {
4848
- while (this.readable.length === 0) {
4849
- if (this.hungUp) return new Uint8Array(0);
4850
- await this.changed(signal);
4851
- }
4852
- const head = this.readable[0];
4853
- if (head === null) {
4854
- this.readable.shift();
4855
- return new Uint8Array(0);
4856
- }
4857
- if (head === void 0) return new Uint8Array(0);
4858
- const chunks = [];
4859
- let n = 0;
4860
- while (this.readable.length > 0 && n < max) {
4861
- const next = this.readable[0];
4862
- if (next === null || next === void 0) break;
4863
- const take = next.subarray(0, max - n);
4864
- chunks.push(take);
4865
- n += take.length;
4866
- if (take.length < next.length) this.readable[0] = next.subarray(take.length);
4867
- else this.readable.shift();
4868
- if (this.canonical) break;
4869
- }
4870
- const out = new Uint8Array(n);
4871
- let at = 0;
4872
- for (const chunk2 of chunks) {
4873
- out.set(chunk2, at);
4874
- at += chunk2.length;
5032
+ async take(entry, max) {
5033
+ if (entry.left.length === 0 && !entry.done) {
5034
+ try {
5035
+ const next = await entry.body.next();
5036
+ if (next.done) entry.done = true;
5037
+ else entry.left = next.value;
5038
+ } catch (e) {
5039
+ entry.done = true;
5040
+ const timedOut = e?.code === "ETIMEDOUT";
5041
+ return { ok: false, errno: timedOut ? "ETIMEDOUT" : "EIO", message: message(e) };
5042
+ }
4875
5043
  }
4876
- return out;
5044
+ const bytes = entry.left.subarray(0, Math.max(0, max));
5045
+ entry.left = entry.left.subarray(bytes.length);
5046
+ return { ok: true, kind: "bytes", bytes };
4877
5047
  }
4878
- changed(signal) {
4879
- if (signal?.aborted) return Promise.reject(new KernelError("EINTR"));
4880
- return new Promise((resolve, reject) => {
4881
- const onAbort = () => {
4882
- this.waiters = this.waiters.filter((w) => w !== waiter);
4883
- reject(new KernelError("EINTR"));
4884
- };
4885
- const waiter = () => {
4886
- signal?.removeEventListener("abort", onAbort);
4887
- resolve();
4888
- };
4889
- this.waiters.push(waiter);
4890
- signal?.addEventListener("abort", onAbort, { once: true });
4891
- });
5048
+ async close(handle) {
5049
+ const entry = this.open.get(handle);
5050
+ if (!entry) return { ok: false, errno: "EBADF", message: `no request ${handle}` };
5051
+ this.open.delete(handle);
5052
+ entry.abort.abort();
5053
+ await entry.cancel().catch(() => void 0);
5054
+ return { ok: true, kind: "void" };
4892
5055
  }
4893
- wake() {
4894
- const waiters = this.waiters;
4895
- this.waiters = [];
4896
- for (const resolve of waiters) resolve();
5056
+ async closeAll() {
5057
+ await Promise.all([...this.open.keys()].map((handle) => this.close(handle)));
4897
5058
  }
4898
5059
  };
4899
5060
 
4900
- // src/kernel/pty.ts
4901
- var PtyPair = class {
4902
- slave;
4903
- locked = true;
4904
- packet = false;
4905
- queue = [];
5061
+ // src/kernel/tty.ts
5062
+ var ICRNL = 256;
5063
+ var IUTF8 = 16384;
5064
+ var OPOST = 1;
5065
+ var ONLCR = 4;
5066
+ var ISIG = 1;
5067
+ var ICANON = 2;
5068
+ var ECHO = 8;
5069
+ var ECHOE = 16;
5070
+ var ECHOCTL = 512;
5071
+ var NOFLSH = 128;
5072
+ var IEXTEN = 32768;
5073
+ var VINTR = 0;
5074
+ var VQUIT = 1;
5075
+ var VERASE = 2;
5076
+ var VKILL = 3;
5077
+ var VEOF = 4;
5078
+ var VSUSP = 10;
5079
+ var VWERASE = 14;
5080
+ function defaultTermios() {
5081
+ const cCc = new Array(32).fill(0);
5082
+ Object.assign(cCc, { [VINTR]: 3, [VQUIT]: 28, [VERASE]: 127, [VKILL]: 21 });
5083
+ Object.assign(cCc, { [VEOF]: 4, 6: 1, 8: 17, 9: 19, [VSUSP]: 26 });
5084
+ Object.assign(cCc, { 12: 18, 13: 15, [VWERASE]: 23, 15: 22 });
5085
+ return {
5086
+ c_iflag: ICRNL | 1024 | 8192 | IUTF8,
5087
+ c_oflag: OPOST | ONLCR,
5088
+ c_cflag: 15 | 48 | 128,
5089
+ c_lflag: ISIG | ICANON | ECHO | ECHOE | 32 | ECHOCTL | 2048 | IEXTEN,
5090
+ c_cc: cCc
5091
+ };
5092
+ }
5093
+ var encoder2 = new TextEncoder();
5094
+ function isContinuation(byte) {
5095
+ return (byte & 192) === 128;
5096
+ }
5097
+ var KernelTty = class {
5098
+ termios = defaultTermios();
5099
+ cols = 80;
5100
+ rows = 24;
5101
+ readable = [];
5102
+ line = [];
4906
5103
  waiters = [];
4907
- masterOpen = true;
4908
- slaveRefs = 0;
4909
- slaveSeen = false;
4910
- index;
4911
- onFree;
4912
- constructor(index, signal, onFree) {
4913
- this.index = index;
4914
- this.onFree = onFree;
4915
- this.slave = new KernelTty(
4916
- { write: (bytes) => this.fromSlave(bytes) },
4917
- (sig) => signal(this.slave, sig)
4918
- );
4919
- this.slave.name = `/dev/pts/${index}`;
4920
- }
4921
- master() {
4922
- return new OpenFile({
4923
- read: (max, signal) => this.readMaster(max, signal),
4924
- write: async (bytes) => {
4925
- this.slave.receive(bytes);
4926
- return bytes.length;
4927
- },
4928
- poll: () => ({
4929
- readable: this.queue.length > 0 || this.slaveGone,
4930
- writable: true,
4931
- hangup: this.slaveGone
4932
- }),
4933
- changed: (signal) => this.changed(signal),
4934
- close: () => this.closeMaster(),
4935
- pty: this
4936
- });
5104
+ hungUp = false;
5105
+ name;
5106
+ screen;
5107
+ signal;
5108
+ constructor(screen, signal) {
5109
+ this.screen = screen;
5110
+ this.signal = signal;
4937
5111
  }
4938
- openSlave() {
4939
- if (!this.masterOpen || this.locked) throw new KernelError("EIO");
4940
- const file = this.slave.file().file;
4941
- this.slaveRefs += 1;
4942
- this.slaveSeen = true;
4943
- return new OpenFile({ ...file, close: () => this.closeSlave() });
5112
+ receive(bytes) {
5113
+ for (const byte of bytes) this.key(byte);
5114
+ this.wake();
4944
5115
  }
4945
- get slaveGone() {
4946
- return this.slaveSeen && this.slaveRefs === 0;
5116
+ resize(cols, rows) {
5117
+ this.setSize(cols, rows);
5118
+ this.signal(SIG.WINCH);
4947
5119
  }
4948
- fromSlave(bytes) {
4949
- if (!this.masterOpen) return;
4950
- this.queue.push(bytes.slice());
5120
+ hangup() {
5121
+ this.hungUp = true;
4951
5122
  this.wake();
4952
5123
  }
4953
- async readMaster(max, signal) {
4954
- while (this.queue.length === 0) {
4955
- if (this.slaveGone) throw new KernelError("EIO");
4956
- await this.changed(signal);
4957
- }
4958
- const status = this.packet ? 1 : 0;
4959
- const chunks = [];
4960
- let n = 0;
4961
- while (this.queue.length > 0 && n < max - status) {
4962
- const next = this.queue[0];
4963
- const take = next.subarray(0, max - status - n);
4964
- chunks.push(take);
4965
- n += take.length;
4966
- if (take.length < next.length) this.queue[0] = next.subarray(take.length);
4967
- else this.queue.shift();
4968
- }
4969
- const out = new Uint8Array(status + n);
4970
- let at = status;
4971
- for (const chunk2 of chunks) {
4972
- out.set(chunk2, at);
4973
- at += chunk2.length;
4974
- }
4975
- return out;
5124
+ signalHangup() {
5125
+ this.signal(SIG.HUP);
4976
5126
  }
4977
- closeMaster() {
4978
- this.masterOpen = false;
4979
- this.queue = [];
4980
- this.slave.hangup();
4981
- this.slave.signalHangup();
4982
- this.freeIfUnused();
5127
+ setSize(cols, rows) {
5128
+ this.cols = cols;
5129
+ this.rows = rows;
4983
5130
  }
4984
- closeSlave() {
4985
- this.slaveRefs -= 1;
4986
- this.wake();
4987
- this.freeIfUnused();
5131
+ tcgets() {
5132
+ return { ...this.termios, c_cc: [...this.termios.c_cc] };
5133
+ }
5134
+ tcsets(termios) {
5135
+ const wasCanonical = this.canonical;
5136
+ this.termios = { ...termios, c_cc: [...termios.c_cc] };
5137
+ if (wasCanonical && !this.canonical && this.line.length > 0) {
5138
+ this.readable.push(Uint8Array.from(this.line));
5139
+ this.line = [];
5140
+ this.wake();
5141
+ }
4988
5142
  }
4989
- freeIfUnused() {
4990
- if (!this.masterOpen && this.slaveRefs === 0) this.onFree(this);
5143
+ winsize() {
5144
+ return [this.rows, this.cols];
4991
5145
  }
4992
- changed(signal) {
4993
- if (signal?.aborted) return Promise.reject(new KernelError("EINTR"));
4994
- return new Promise((resolve, reject) => {
4995
- const onAbort = () => {
4996
- this.waiters = this.waiters.filter((w) => w !== waiter);
4997
- reject(new KernelError("EINTR"));
4998
- };
4999
- const waiter = () => {
5000
- signal?.removeEventListener("abort", onAbort);
5001
- resolve();
5002
- };
5003
- this.waiters.push(waiter);
5004
- signal?.addEventListener("abort", onAbort, { once: true });
5146
+ file() {
5147
+ return new OpenFile({
5148
+ read: (max, signal) => this.read(max, signal),
5149
+ write: async (bytes) => {
5150
+ if (this.hungUp) throw new KernelError("EIO");
5151
+ this.output(bytes);
5152
+ return bytes.length;
5153
+ },
5154
+ poll: () => ({
5155
+ readable: this.readable.length > 0 || this.hungUp,
5156
+ writable: true,
5157
+ hangup: this.hungUp
5158
+ }),
5159
+ changed: (signal) => this.changed(signal),
5160
+ close: () => {
5161
+ },
5162
+ tty: this
5005
5163
  });
5006
5164
  }
5007
- wake() {
5008
- const waiters = this.waiters;
5009
- this.waiters = [];
5010
- for (const resolve of waiters) resolve();
5165
+ get canonical() {
5166
+ return (this.termios.c_lflag & ICANON) !== 0;
5011
5167
  }
5012
- };
5013
- var PtyTable = class {
5014
- pairs = /* @__PURE__ */ new Map();
5015
- signal;
5016
- constructor(signal) {
5017
- this.signal = signal;
5168
+ cc(slot) {
5169
+ return this.termios.c_cc[slot] ?? 0;
5018
5170
  }
5019
- open() {
5020
- let index = 0;
5021
- while (this.pairs.has(index)) index += 1;
5022
- const pair = new PtyPair(index, this.signal, (p) => this.pairs.delete(p.index));
5023
- this.pairs.set(index, pair);
5024
- return { pair, master: pair.master() };
5171
+ key(input) {
5172
+ const { c_iflag, c_lflag } = this.termios;
5173
+ const byte = input === 13 && c_iflag & ICRNL ? 10 : input;
5174
+ if (c_lflag & ISIG && this.signalKey(byte)) return;
5175
+ if (!this.canonical) {
5176
+ this.readable.push(Uint8Array.of(byte));
5177
+ if (c_lflag & ECHO) this.echo(byte);
5178
+ return;
5179
+ }
5180
+ this.edit(byte);
5025
5181
  }
5026
- get(index) {
5027
- const pair = this.pairs.get(index);
5028
- if (!pair) throw new KernelError("ENOENT");
5029
- return pair;
5182
+ signalKey(byte) {
5183
+ const sig = byte === this.cc(VINTR) ? SIG.INT : byte === this.cc(VQUIT) ? SIG.QUIT : byte === this.cc(VSUSP) ? SIG.TSTP : 0;
5184
+ if (!sig || byte === 0) return false;
5185
+ if (this.termios.c_lflag & ECHO) this.echo(byte);
5186
+ if (!(this.termios.c_lflag & NOFLSH)) {
5187
+ this.line = [];
5188
+ this.readable = [];
5189
+ }
5190
+ this.signal(sig);
5191
+ return true;
5030
5192
  }
5031
- numbers() {
5032
- return [...this.pairs.keys()].sort((a, b) => a - b);
5193
+ edit(byte) {
5194
+ const echo = (this.termios.c_lflag & ECHO) !== 0;
5195
+ if (byte === this.cc(VERASE) || byte === 8) {
5196
+ this.rubOut(this.lastCharLength());
5197
+ } else if (byte === this.cc(VKILL)) {
5198
+ this.rubOut(this.line.length);
5199
+ } else if (byte === this.cc(VWERASE)) {
5200
+ this.rubOut(this.lastWordLength());
5201
+ } else if (byte === this.cc(VEOF)) {
5202
+ this.readable.push(this.line.length > 0 ? Uint8Array.from(this.line) : null);
5203
+ this.line = [];
5204
+ } else if (byte === 10) {
5205
+ this.line.push(byte);
5206
+ if (echo) this.output(encoder2.encode("\n"));
5207
+ this.readable.push(Uint8Array.from(this.line));
5208
+ this.line = [];
5209
+ } else {
5210
+ this.line.push(byte);
5211
+ if (echo) this.echo(byte);
5212
+ }
5033
5213
  }
5034
- };
5035
- var PTY_OPS = [
5036
- "pty-open",
5037
- "pty-slave-open",
5038
- "pty-number",
5039
- "pty-lock",
5040
- "pty-ctty",
5041
- "pty-packet",
5042
- "pty-list",
5043
- "pty-winsz-set"
5044
- ];
5045
- function masterOf(ctx, fd) {
5046
- const pair = ctx.fds.get(fd).file.pty;
5047
- if (!pair) throw new KernelError("ENOTTY");
5048
- return pair;
5049
- }
5050
- function terminalOf(ctx, fd) {
5051
- const file = ctx.fds.get(fd).file;
5052
- const tty = file.tty ?? file.pty?.slave;
5053
- if (!tty) throw new KernelError("ENOTTY");
5054
- return tty;
5055
- }
5056
- var json3 = (value) => ({ ok: true, kind: "json", json: value });
5057
- var done3 = { ok: true, kind: "void" };
5058
- function ptySyscall(req, ctx) {
5059
- switch (req.op) {
5060
- case "pty-open": {
5061
- if (!ctx.ptys) throw new KernelError("ENOENT");
5062
- return json3(ctx.fds.install(ctx.ptys.open().master, 3));
5214
+ lastWordLength() {
5215
+ let end = this.line.length;
5216
+ while (end > 0 && this.line[end - 1] === 32) end--;
5217
+ while (end > 0 && this.line[end - 1] !== 32) end--;
5218
+ return this.line.length - end;
5219
+ }
5220
+ lastCharLength() {
5221
+ const { line } = this;
5222
+ let n = Math.min(1, line.length);
5223
+ if (this.termios.c_iflag & IUTF8) {
5224
+ while (n < line.length && isContinuation(line[line.length - n] ?? 0)) n++;
5063
5225
  }
5064
- case "pty-slave-open": {
5065
- if (!ctx.ptys) throw new KernelError("ENOENT");
5066
- const pair = ctx.ptys.get(req.n);
5067
- const fd = ctx.fds.install(pair.openSlave(), 3);
5068
- if (!req.noctty) ctx.jobs?.acquireTerminal(ctx.pid, pair.slave);
5069
- return json3(fd);
5226
+ return n;
5227
+ }
5228
+ rubOut(n) {
5229
+ const dropped2 = this.line.splice(this.line.length - n, n);
5230
+ const utf82 = (this.termios.c_iflag & IUTF8) !== 0;
5231
+ const cells = utf82 ? dropped2.filter((b) => !isContinuation(b)).length : dropped2.length;
5232
+ if (cells > 0 && this.termios.c_lflag & ECHO) {
5233
+ this.output(encoder2.encode("\b \b".repeat(cells)), false);
5070
5234
  }
5071
- case "pty-number":
5072
- return json3(masterOf(ctx, req.fd).index);
5073
- case "pty-lock":
5074
- masterOf(ctx, req.fd).locked = req.lock;
5075
- return done3;
5076
- case "pty-ctty": {
5077
- const tty = terminalOf(ctx, req.fd);
5078
- const leader = ctx.jobs?.getsid(ctx.pid, 0) === ctx.pid;
5079
- if (leader && ctx.jobs?.controllingTerminal(ctx.pid) === tty) return done3;
5080
- if (!ctx.jobs?.acquireTerminal(ctx.pid, tty)) throw new KernelError("EPERM");
5081
- return done3;
5235
+ }
5236
+ echo(byte) {
5237
+ if (byte < 32 && byte !== 10 && byte !== 9 && this.termios.c_lflag & ECHOCTL) {
5238
+ this.output(Uint8Array.of(94, byte + 64), false);
5239
+ return;
5082
5240
  }
5083
- case "pty-list":
5084
- return json3(ctx.ptys?.numbers() ?? []);
5085
- case "pty-packet":
5086
- masterOf(ctx, req.fd).packet = req.on;
5087
- return done3;
5088
- case "pty-winsz-set":
5089
- terminalOf(ctx, req.fd).resize(req.cols, req.rows);
5090
- return done3;
5241
+ this.output(Uint8Array.of(byte));
5091
5242
  }
5092
- }
5093
-
5094
- // src/kernel/select.ts
5095
- function ready(fds, read, write) {
5096
- const hangup = /* @__PURE__ */ new Set();
5097
- const pick = (list, dir) => list.filter((fd) => {
5098
- const state = pollFile(fds.get(fd).file);
5099
- if (state.hangup) hangup.add(fd);
5100
- return state[dir] || state.hangup;
5101
- });
5102
- const out = { read: pick(read, "readable"), write: pick(write, "writable") };
5103
- if (hangup.size > 0) out.hangup = [...hangup];
5104
- return out;
5105
- }
5106
- function interruption(signal) {
5107
- const { promise, reject } = Promise.withResolvers();
5108
- const fail = () => reject(new KernelError("EINTR"));
5109
- if (signal.aborted) fail();
5110
- else signal.addEventListener("abort", fail, { once: true });
5111
- return { promise, done: () => signal.removeEventListener("abort", fail) };
5112
- }
5113
- async function selectFds(fds, read, write, timeoutMs, interrupt) {
5114
- const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
5115
- const interrupted2 = interruption(interrupt);
5116
- interrupted2.promise.catch(() => {
5117
- });
5118
- try {
5119
- return await waitReady(fds, read, write, deadline, interrupted2.promise);
5120
- } finally {
5121
- interrupted2.done();
5243
+ output(bytes, translate2 = true) {
5244
+ const { c_oflag } = this.termios;
5245
+ if (!translate2 || !(c_oflag & OPOST && c_oflag & ONLCR) || !bytes.includes(10)) {
5246
+ this.screen.write(bytes);
5247
+ return;
5248
+ }
5249
+ const out = [];
5250
+ for (const byte of bytes) {
5251
+ if (byte === 10) out.push(13);
5252
+ out.push(byte);
5253
+ }
5254
+ this.screen.write(Uint8Array.from(out));
5122
5255
  }
5123
- }
5124
- async function waitReady(fds, read, write, deadline, interrupted2) {
5125
- for (; ; ) {
5126
- const now = ready(fds, read, write);
5127
- if (now.read.length > 0 || now.write.length > 0 || Date.now() >= deadline) return now;
5128
- const round = new AbortController();
5129
- const changes = [.../* @__PURE__ */ new Set([...read, ...write])].flatMap((fd) => {
5130
- const file = fds.get(fd).file;
5131
- return file.changed === void 0 ? [] : [file.changed(round.signal).catch(() => void 0)];
5132
- });
5133
- let timer;
5134
- const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve) => timer = setTimeout(resolve, deadline - Date.now()))];
5135
- try {
5136
- await Promise.race([...changes, ...expiry, interrupted2]);
5137
- } finally {
5138
- round.abort();
5139
- clearTimeout(timer);
5256
+ async read(max, signal) {
5257
+ while (this.readable.length === 0) {
5258
+ if (this.hungUp) return new Uint8Array(0);
5259
+ await this.changed(signal);
5260
+ }
5261
+ const head = this.readable[0];
5262
+ if (head === null) {
5263
+ this.readable.shift();
5264
+ return new Uint8Array(0);
5265
+ }
5266
+ if (head === void 0) return new Uint8Array(0);
5267
+ const chunks = [];
5268
+ let n = 0;
5269
+ while (this.readable.length > 0 && n < max) {
5270
+ const next = this.readable[0];
5271
+ if (next === null || next === void 0) break;
5272
+ const take = next.subarray(0, max - n);
5273
+ chunks.push(take);
5274
+ n += take.length;
5275
+ if (take.length < next.length) this.readable[0] = next.subarray(take.length);
5276
+ else this.readable.shift();
5277
+ if (this.canonical) break;
5140
5278
  }
5279
+ const out = new Uint8Array(n);
5280
+ let at = 0;
5281
+ for (const chunk2 of chunks) {
5282
+ out.set(chunk2, at);
5283
+ at += chunk2.length;
5284
+ }
5285
+ return out;
5141
5286
  }
5142
- }
5143
-
5144
- // src/kernel/socket.ts
5145
- var SOMAXCONN = 4096;
5146
- var EPHEMERAL_FIRST = 32768;
5147
- var EPHEMERAL_LAST = 60999;
5148
- var SOL_SOCKET = 1;
5149
- var SO_TYPE = 3;
5150
- var SO_ERROR = 4;
5151
- var SO_SNDBUF = 7;
5152
- var SO_RCVBUF = 8;
5153
- var SO_ACCEPTCONN = 30;
5154
- var SO_DOMAIN = 39;
5155
- var SOCK_STREAM = 1;
5156
- var AF_UNIX = 1;
5157
- var AF_INET = 2;
5158
- var SHUT_RD = 0;
5159
- var SHUT_WR = 1;
5160
- var SHUT_RDWR = 2;
5161
- function isLoopback(host) {
5162
- return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
5163
- }
5164
- async function pipeCall(op) {
5165
- try {
5166
- return await op();
5167
- } catch (e) {
5168
- if (e instanceof PipeError) throw new KernelError(e.code);
5169
- throw e;
5170
- }
5171
- }
5172
- var Changes = class {
5173
- waiters = [];
5174
- wait(signal) {
5287
+ changed(signal) {
5175
5288
  if (signal?.aborted) return Promise.reject(new KernelError("EINTR"));
5176
5289
  return new Promise((resolve, reject) => {
5177
5290
  const onAbort = () => {
@@ -5192,267 +5305,250 @@ var Changes = class {
5192
5305
  for (const resolve of waiters) resolve();
5193
5306
  }
5194
5307
  };
5195
- function link(a, b) {
5196
- const ab = new KernelPipe();
5197
- const ba = new KernelPipe();
5198
- for (const pipe of [ab, ba]) {
5199
- pipe.openRead();
5200
- pipe.openWrite();
5201
- }
5202
- a.attach({ rx: ba, tx: ab });
5203
- b.attach({ rx: ab, tx: ba });
5204
- }
5205
- var KernelSocket = class _KernelSocket {
5206
- net;
5207
- domain;
5208
- state = "open";
5209
- local;
5210
- peer;
5211
- link;
5212
- readShut = false;
5213
- writeShut = false;
5308
+
5309
+ // src/kernel/pty.ts
5310
+ var PtyPair = class {
5311
+ slave;
5312
+ locked = true;
5313
+ packet = false;
5214
5314
  queue = [];
5215
- backlog = 0;
5216
- bound = false;
5217
- changes = new Changes();
5218
- options = /* @__PURE__ */ new Map();
5219
- constructor(net, domain) {
5220
- this.net = net;
5221
- this.domain = domain;
5222
- }
5223
- attach(link2) {
5224
- this.link = link2;
5225
- this.state = "connected";
5226
- this.changes.wake();
5227
- }
5228
- get listening() {
5229
- return this.state === "listening";
5230
- }
5231
- connection() {
5232
- if (!this.link || this.state !== "connected") throw new KernelError("ENOTCONN");
5233
- return this.link;
5234
- }
5235
- async read(max, signal) {
5236
- const { rx } = this.connection();
5237
- if (this.readShut || max <= 0) return new Uint8Array(0);
5238
- return pipeCall(() => rx.read(max, signal));
5239
- }
5240
- async peek(max, signal) {
5241
- const { rx } = this.connection();
5242
- if (this.readShut || max <= 0) return new Uint8Array(0);
5243
- return pipeCall(() => rx.peek(max, signal));
5244
- }
5245
- async write(bytes, signal) {
5246
- const { tx } = this.connection();
5247
- if (this.writeShut) throw new KernelError("EPIPE");
5248
- return pipeCall(() => tx.write(bytes, signal));
5249
- }
5250
- poll() {
5251
- if (this.state === "listening") {
5252
- return { readable: this.queue.length > 0, writable: false, hangup: false };
5253
- }
5254
- if (this.state !== "connected" || !this.link) {
5255
- return { readable: false, writable: false, hangup: true };
5256
- }
5257
- const { rx, tx } = this.link;
5258
- return {
5259
- readable: this.readShut || rx.readReady,
5260
- writable: this.writeShut || tx.writeReady,
5261
- hangup: rx.writersGone && tx.readersGone
5262
- };
5263
- }
5264
- changed(signal) {
5265
- const own = this.changes.wait(signal);
5266
- if (this.state !== "connected" || !this.link) return own;
5267
- const waits = [own, this.link.rx.changed(signal), this.link.tx.changed(signal)];
5268
- for (const wait of waits) wait.catch(() => void 0);
5269
- return pipeCall(() => Promise.race(waits));
5270
- }
5271
- bind(addr) {
5272
- if (this.state !== "open" || this.local) throw new KernelError("EINVAL");
5273
- if (addr.family !== this.domain) throw new KernelError("EAFNOSUPPORT");
5274
- this.local = this.net.bind(this, addr);
5275
- this.bound = true;
5315
+ waiters = [];
5316
+ masterOpen = true;
5317
+ slaveRefs = 0;
5318
+ slaveSeen = false;
5319
+ index;
5320
+ onFree;
5321
+ constructor(index, signal, onFree) {
5322
+ this.index = index;
5323
+ this.onFree = onFree;
5324
+ this.slave = new KernelTty(
5325
+ { write: (bytes) => this.fromSlave(bytes) },
5326
+ (sig) => signal(this.slave, sig)
5327
+ );
5328
+ this.slave.name = `/dev/pts/${index}`;
5276
5329
  }
5277
- listen(backlog) {
5278
- if (this.state === "connected") throw new KernelError("EISCONN");
5279
- if (this.state !== "open" && this.state !== "listening") throw new KernelError("EINVAL");
5280
- if (!this.local) {
5281
- if (this.domain === "unix") throw new KernelError("EINVAL");
5282
- this.bind({ family: "inet", host: "0.0.0.0", port: 0 });
5283
- }
5284
- this.backlog = Math.min(Math.max(backlog, 1), SOMAXCONN);
5285
- this.state = "listening";
5330
+ master() {
5331
+ return new OpenFile({
5332
+ read: (max, signal) => this.readMaster(max, signal),
5333
+ write: async (bytes) => {
5334
+ this.slave.receive(bytes);
5335
+ return bytes.length;
5336
+ },
5337
+ poll: () => ({
5338
+ readable: this.queue.length > 0 || this.slaveGone,
5339
+ writable: true,
5340
+ hangup: this.slaveGone
5341
+ }),
5342
+ changed: (signal) => this.changed(signal),
5343
+ close: () => this.closeMaster(),
5344
+ pty: this
5345
+ });
5286
5346
  }
5287
- async accept(signal, nonblock = false) {
5288
- for (; ; ) {
5289
- if (this.state !== "listening") throw new KernelError("EINVAL");
5290
- const next = this.queue.shift();
5291
- if (next) return next;
5292
- if (nonblock) throw new KernelError("EAGAIN");
5293
- await this.changes.wait(signal);
5294
- }
5347
+ openSlave() {
5348
+ if (!this.masterOpen || this.locked) throw new KernelError("EIO");
5349
+ const file = this.slave.file().file;
5350
+ this.slaveRefs += 1;
5351
+ this.slaveSeen = true;
5352
+ return new OpenFile({ ...file, close: () => this.closeSlave() });
5295
5353
  }
5296
- offer(server) {
5297
- if (this.state !== "listening" || this.queue.length >= this.backlog) return false;
5298
- this.queue.push(server);
5299
- this.changes.wake();
5300
- return true;
5354
+ get slaveGone() {
5355
+ return this.slaveSeen && this.slaveRefs === 0;
5301
5356
  }
5302
- connect(addr) {
5303
- if (this.state === "connected") throw new KernelError("EISCONN");
5304
- if (this.state !== "open") throw new KernelError("EINVAL");
5305
- if (addr.family !== this.domain) throw new KernelError("EAFNOSUPPORT");
5306
- const target = this.net.target(addr);
5307
- if (!this.local) {
5308
- if (addr.family === "inet") {
5309
- this.local = this.net.bind(this, { family: "inet", host: "127.0.0.1", port: 0 });
5310
- this.bound = true;
5311
- } else this.local = { family: "unix", path: "" };
5312
- }
5313
- const server = new _KernelSocket(this.net, this.domain);
5314
- server.local = addr.family === "inet" ? { ...addr, host: canonicalHost2(addr.host) } : addr;
5315
- server.peer = this.local;
5316
- this.peer = server.local;
5317
- link(this, server);
5318
- if (!target.offer(server)) {
5319
- this.unlink();
5320
- server.close();
5321
- throw new KernelError("ECONNREFUSED");
5322
- }
5357
+ fromSlave(bytes) {
5358
+ if (!this.masterOpen) return;
5359
+ this.queue.push(bytes.slice());
5360
+ this.wake();
5323
5361
  }
5324
- unlink() {
5325
- const link2 = this.link;
5326
- if (link2) {
5327
- link2.rx.closeRead();
5328
- link2.tx.closeWrite();
5362
+ async readMaster(max, signal) {
5363
+ while (this.queue.length === 0) {
5364
+ if (this.slaveGone) throw new KernelError("EIO");
5365
+ await this.changed(signal);
5329
5366
  }
5330
- this.link = void 0;
5331
- this.peer = void 0;
5332
- this.state = "open";
5333
- }
5334
- shutdown(how) {
5335
- const { rx, tx } = this.connection();
5336
- if (how !== SHUT_RD && how !== SHUT_WR && how !== SHUT_RDWR) throw new KernelError("EINVAL");
5337
- if (how !== SHUT_WR && !this.readShut) {
5338
- this.readShut = true;
5339
- rx.closeRead();
5367
+ const status = this.packet ? 1 : 0;
5368
+ const chunks = [];
5369
+ let n = 0;
5370
+ while (this.queue.length > 0 && n < max - status) {
5371
+ const next = this.queue[0];
5372
+ const take = next.subarray(0, max - status - n);
5373
+ chunks.push(take);
5374
+ n += take.length;
5375
+ if (take.length < next.length) this.queue[0] = next.subarray(take.length);
5376
+ else this.queue.shift();
5340
5377
  }
5341
- if (how !== SHUT_RD && !this.writeShut) {
5342
- this.writeShut = true;
5343
- tx.closeWrite();
5378
+ const out = new Uint8Array(status + n);
5379
+ let at = status;
5380
+ for (const chunk2 of chunks) {
5381
+ out.set(chunk2, at);
5382
+ at += chunk2.length;
5344
5383
  }
5345
- this.changes.wake();
5384
+ return out;
5346
5385
  }
5347
- getOption(level, name2) {
5348
- if (level === SOL_SOCKET) {
5349
- switch (name2) {
5350
- case SO_TYPE:
5351
- return SOCK_STREAM;
5352
- case SO_ERROR:
5353
- return 0;
5354
- case SO_ACCEPTCONN:
5355
- return this.state === "listening" ? 1 : 0;
5356
- case SO_DOMAIN:
5357
- return this.domain === "inet" ? AF_INET : AF_UNIX;
5358
- case SO_SNDBUF:
5359
- case SO_RCVBUF:
5360
- return this.options.get(`${level}:${name2}`) ?? PIPE_CAPACITY;
5361
- }
5362
- }
5363
- return this.options.get(`${level}:${name2}`) ?? 0;
5386
+ closeMaster() {
5387
+ this.masterOpen = false;
5388
+ this.queue = [];
5389
+ this.slave.hangup();
5390
+ this.slave.signalHangup();
5391
+ this.freeIfUnused();
5364
5392
  }
5365
- setOption(level, name2, value) {
5366
- this.options.set(`${level}:${name2}`, value);
5393
+ closeSlave() {
5394
+ this.slaveRefs -= 1;
5395
+ this.wake();
5396
+ this.freeIfUnused();
5367
5397
  }
5368
- close() {
5369
- const was = this.state;
5370
- this.state = "closed";
5371
- if (was === "listening") {
5372
- for (const pending of this.queue.splice(0)) pending.close();
5373
- }
5374
- if (this.link) {
5375
- if (!this.readShut) this.link.rx.closeRead();
5376
- if (!this.writeShut) this.link.tx.closeWrite();
5377
- this.readShut = true;
5378
- this.writeShut = true;
5379
- }
5380
- if (this.bound && this.local) this.net.release(this.local, this);
5381
- this.bound = false;
5382
- this.changes.wake();
5398
+ freeIfUnused() {
5399
+ if (!this.masterOpen && this.slaveRefs === 0) this.onFree(this);
5383
5400
  }
5384
- static pair(net, domain) {
5385
- const a = new _KernelSocket(net, domain);
5386
- const b = new _KernelSocket(net, domain);
5387
- const name2 = domain === "inet" ? { family: "inet", host: "127.0.0.1", port: 0 } : { family: "unix", path: "" };
5388
- a.local = b.local = a.peer = b.peer = name2;
5389
- link(a, b);
5390
- return [a, b];
5401
+ changed(signal) {
5402
+ if (signal?.aborted) return Promise.reject(new KernelError("EINTR"));
5403
+ return new Promise((resolve, reject) => {
5404
+ const onAbort = () => {
5405
+ this.waiters = this.waiters.filter((w) => w !== waiter);
5406
+ reject(new KernelError("EINTR"));
5407
+ };
5408
+ const waiter = () => {
5409
+ signal?.removeEventListener("abort", onAbort);
5410
+ resolve();
5411
+ };
5412
+ this.waiters.push(waiter);
5413
+ signal?.addEventListener("abort", onAbort, { once: true });
5414
+ });
5415
+ }
5416
+ wake() {
5417
+ const waiters = this.waiters;
5418
+ this.waiters = [];
5419
+ for (const resolve of waiters) resolve();
5391
5420
  }
5392
5421
  };
5393
- function canonicalHost2(host) {
5394
- return host === "0.0.0.0" ? "127.0.0.1" : host;
5395
- }
5396
- function keyOf(addr) {
5397
- return addr.family === "inet" ? `inet:${addr.port}` : `unix:${addr.path}`;
5398
- }
5399
- var LoopbackNet = class {
5400
- bound = /* @__PURE__ */ new Map();
5401
- activators = /* @__PURE__ */ new Map();
5402
- nextEphemeral = EPHEMERAL_FIRST;
5403
- activate(addr, start) {
5404
- this.activators.set(keyOf(addr), start);
5422
+ var PtyTable = class {
5423
+ pairs = /* @__PURE__ */ new Map();
5424
+ signal;
5425
+ constructor(signal) {
5426
+ this.signal = signal;
5405
5427
  }
5406
- socket(domain) {
5407
- return new KernelSocket(this, domain);
5428
+ open() {
5429
+ let index = 0;
5430
+ while (this.pairs.has(index)) index += 1;
5431
+ const pair = new PtyPair(index, this.signal, (p) => this.pairs.delete(p.index));
5432
+ this.pairs.set(index, pair);
5433
+ return { pair, master: pair.master() };
5408
5434
  }
5409
- listen(addr, backlog = 128) {
5410
- const socket = this.socket(addr.family);
5411
- socket.bind(addr);
5412
- socket.listen(backlog);
5413
- return socket;
5435
+ get(index) {
5436
+ const pair = this.pairs.get(index);
5437
+ if (!pair) throw new KernelError("ENOENT");
5438
+ return pair;
5414
5439
  }
5415
- connect(addr) {
5416
- const socket = this.socket(addr.family);
5417
- socket.connect(addr);
5418
- return socket;
5440
+ numbers() {
5441
+ return [...this.pairs.keys()].sort((a, b) => a - b);
5419
5442
  }
5420
- bind(socket, addr) {
5421
- if (addr.family === "unix") {
5422
- if (addr.path === "") throw new KernelError("EINVAL");
5423
- } else if (addr.host !== "0.0.0.0" && !isLoopback(addr.host)) {
5424
- throw new KernelError("EADDRNOTAVAIL");
5443
+ };
5444
+ var PTY_OPS = [
5445
+ "pty-open",
5446
+ "pty-slave-open",
5447
+ "pty-number",
5448
+ "pty-lock",
5449
+ "pty-ctty",
5450
+ "pty-packet",
5451
+ "pty-list",
5452
+ "pty-winsz-set"
5453
+ ];
5454
+ function masterOf(ctx, fd) {
5455
+ const pair = ctx.fds.get(fd).file.pty;
5456
+ if (!pair) throw new KernelError("ENOTTY");
5457
+ return pair;
5458
+ }
5459
+ function terminalOf(ctx, fd) {
5460
+ const file = ctx.fds.get(fd).file;
5461
+ const tty = file.tty ?? file.pty?.slave;
5462
+ if (!tty) throw new KernelError("ENOTTY");
5463
+ return tty;
5464
+ }
5465
+ var json3 = (value) => ({ ok: true, kind: "json", json: value });
5466
+ var done3 = { ok: true, kind: "void" };
5467
+ function ptySyscall(req, ctx) {
5468
+ switch (req.op) {
5469
+ case "pty-open": {
5470
+ if (!ctx.ptys) throw new KernelError("ENOENT");
5471
+ return json3(ctx.fds.install(ctx.ptys.open().master, 3));
5425
5472
  }
5426
- const named = addr.family === "inet" && addr.port === 0 ? { ...addr, port: this.ephemeral() } : addr;
5427
- const key = keyOf(named);
5428
- if (this.bound.has(key)) throw new KernelError("EADDRINUSE");
5429
- this.bound.set(key, socket);
5430
- return named;
5431
- }
5432
- release(addr, socket) {
5433
- const key = keyOf(addr);
5434
- if (this.bound.get(key) === socket) this.bound.delete(key);
5435
- }
5436
- target(addr) {
5437
- if (addr.family === "inet" && !isLoopback(canonicalHost2(addr.host))) {
5438
- throw new KernelError("ENETUNREACH");
5473
+ case "pty-slave-open": {
5474
+ if (!ctx.ptys) throw new KernelError("ENOENT");
5475
+ const pair = ctx.ptys.get(req.n);
5476
+ const fd = ctx.fds.install(pair.openSlave(), 3);
5477
+ if (!req.noctty) ctx.jobs?.acquireTerminal(ctx.pid, pair.slave);
5478
+ return json3(fd);
5439
5479
  }
5440
- const key = keyOf(addr);
5441
- if (!this.bound.has(key)) this.activators.get(key)?.();
5442
- const socket = this.bound.get(key);
5443
- if (!socket?.listening) throw new KernelError("ECONNREFUSED");
5444
- return socket;
5480
+ case "pty-number":
5481
+ return json3(masterOf(ctx, req.fd).index);
5482
+ case "pty-lock":
5483
+ masterOf(ctx, req.fd).locked = req.lock;
5484
+ return done3;
5485
+ case "pty-ctty": {
5486
+ const tty = terminalOf(ctx, req.fd);
5487
+ const leader = ctx.jobs?.getsid(ctx.pid, 0) === ctx.pid;
5488
+ if (leader && ctx.jobs?.controllingTerminal(ctx.pid) === tty) return done3;
5489
+ if (!ctx.jobs?.acquireTerminal(ctx.pid, tty)) throw new KernelError("EPERM");
5490
+ return done3;
5491
+ }
5492
+ case "pty-list":
5493
+ return json3(ctx.ptys?.numbers() ?? []);
5494
+ case "pty-packet":
5495
+ masterOf(ctx, req.fd).packet = req.on;
5496
+ return done3;
5497
+ case "pty-winsz-set":
5498
+ terminalOf(ctx, req.fd).resize(req.cols, req.rows);
5499
+ return done3;
5445
5500
  }
5446
- ephemeral() {
5447
- const span = EPHEMERAL_LAST - EPHEMERAL_FIRST + 1;
5448
- for (let i = 0; i < span; i++) {
5449
- const port = this.nextEphemeral;
5450
- this.nextEphemeral = port === EPHEMERAL_LAST ? EPHEMERAL_FIRST : port + 1;
5451
- if (!this.bound.has(`inet:${port}`)) return port;
5501
+ }
5502
+
5503
+ // src/kernel/select.ts
5504
+ function ready(fds, read, write) {
5505
+ const hangup = /* @__PURE__ */ new Set();
5506
+ const pick = (list, dir) => list.filter((fd) => {
5507
+ const state = pollFile(fds.get(fd).file);
5508
+ if (state.hangup) hangup.add(fd);
5509
+ return state[dir] || state.hangup;
5510
+ });
5511
+ const out = { read: pick(read, "readable"), write: pick(write, "writable") };
5512
+ if (hangup.size > 0) out.hangup = [...hangup];
5513
+ return out;
5514
+ }
5515
+ function interruption(signal) {
5516
+ const { promise, reject } = Promise.withResolvers();
5517
+ const fail = () => reject(new KernelError("EINTR"));
5518
+ if (signal.aborted) fail();
5519
+ else signal.addEventListener("abort", fail, { once: true });
5520
+ return { promise, done: () => signal.removeEventListener("abort", fail) };
5521
+ }
5522
+ async function selectFds(fds, read, write, timeoutMs, interrupt) {
5523
+ const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
5524
+ const interrupted2 = interruption(interrupt);
5525
+ interrupted2.promise.catch(() => {
5526
+ });
5527
+ try {
5528
+ return await waitReady(fds, read, write, deadline, interrupted2.promise);
5529
+ } finally {
5530
+ interrupted2.done();
5531
+ }
5532
+ }
5533
+ async function waitReady(fds, read, write, deadline, interrupted2) {
5534
+ for (; ; ) {
5535
+ const now = ready(fds, read, write);
5536
+ if (now.read.length > 0 || now.write.length > 0 || Date.now() >= deadline) return now;
5537
+ const round = new AbortController();
5538
+ const changes = [.../* @__PURE__ */ new Set([...read, ...write])].flatMap((fd) => {
5539
+ const file = fds.get(fd).file;
5540
+ return file.changed === void 0 ? [] : [file.changed(round.signal).catch(() => void 0)];
5541
+ });
5542
+ let timer;
5543
+ const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve) => timer = setTimeout(resolve, deadline - Date.now()))];
5544
+ try {
5545
+ await Promise.race([...changes, ...expiry, interrupted2]);
5546
+ } finally {
5547
+ round.abort();
5548
+ clearTimeout(timer);
5452
5549
  }
5453
- throw new KernelError("EADDRINUSE");
5454
5550
  }
5455
- };
5551
+ }
5456
5552
 
5457
5553
  // src/kernel/socket-syscalls.ts
5458
5554
  var SOCKET_OPS = [
@@ -6984,8 +7080,8 @@ function mediumSlot(lost, onLost) {
6984
7080
 
6985
7081
  // src/mount/fsa.ts
6986
7082
  var FSA_CAPABILITIES = { listingStats: true };
6987
- var codeOf = (err) => err?.code;
6988
- var denied = (err) => codeOf(err) === "EACCES" || err?.name === "NotAllowedError";
7083
+ var codeOf2 = (err) => err?.code;
7084
+ var denied = (err) => codeOf2(err) === "EACCES" || err?.name === "NotAllowedError";
6989
7085
  function attrOf(st) {
6990
7086
  return {
6991
7087
  kind: st.isDirectory ? "directory" : st.isSymbolicLink ? "symlink" : "file",
@@ -7007,7 +7103,7 @@ function kernelFsHandlers(fs) {
7007
7103
  try {
7008
7104
  return await fs.stat(path);
7009
7105
  } catch (err) {
7010
- if (codeOf(err) === "ENOENT") return void 0;
7106
+ if (codeOf2(err) === "ENOENT") return void 0;
7011
7107
  throw err;
7012
7108
  }
7013
7109
  }
@@ -7091,8 +7187,8 @@ function removableMedium(onLost, open = (handle) => new OpfsFs(handle)) {
7091
7187
 
7092
7188
  // src/mount/hostfs.ts
7093
7189
  var HOSTFS_IO = 16 * 1024 * 1024;
7094
- var codeOf2 = (err) => err?.code;
7095
- var unreachable = (err) => codeOf2(err) === "ENOTCONN";
7190
+ var codeOf3 = (err) => err?.code;
7191
+ var unreachable = (err) => codeOf3(err) === "ENOTCONN";
7096
7192
  function rel(path) {
7097
7193
  return path.replace(/^\/+/, "");
7098
7194
  }
@@ -7227,7 +7323,7 @@ function hostfsHandlers(current) {
7227
7323
  try {
7228
7324
  return { opened: await opening(), restore: void 0 };
7229
7325
  } catch (err) {
7230
- if (!flags.write || codeOf2(err) !== "EACCES") throw err;
7326
+ if (!flags.write || codeOf3(err) !== "EACCES") throw err;
7231
7327
  const mode = (await client.call("stat", { path: rel(path) })).mode ?? 0;
7232
7328
  if (mode & 128) throw err;
7233
7329
  const restore = { path: rel(path), mode };
@@ -8512,6 +8608,12 @@ function serveKernel(port, deps) {
8512
8608
  void served.closed.then(() => clients.delete(served));
8513
8609
  return port2;
8514
8610
  }
8611
+ async function dial(req) {
8612
+ const socket = dialSocket((await ready2()).net, req.port, req.host);
8613
+ const { port1, port2 } = new MessageChannel();
8614
+ serveSocket(socket, port1);
8615
+ return port2;
8616
+ }
8515
8617
  async function ready2() {
8516
8618
  if (!launcher) throw new Error("the kernel is not initialized");
8517
8619
  return launcher;
@@ -8564,6 +8666,7 @@ function serveKernel(port, deps) {
8564
8666
  if (req.op === "init") return init(req);
8565
8667
  if (req.op === "open-terminal") return terminal(req);
8566
8668
  if (req.op === "connect") return connect();
8669
+ if (req.op === "dial") return dial(req);
8567
8670
  if (req.op === "mount" || req.op === "umount" || req.op === "mounts" || req.op === "insert")
8568
8671
  return mountOp(req);
8569
8672
  if (req.op === "terminal") {