@ai-ecoverse/slicc-kernel 1.122.2 → 1.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -611,6 +611,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
611
611
  - **Caching:** the kernel caches attributes and listings for those TTLs, and a driver can push `invalidate` for changes made outside.
612
612
  - **File contents:** on a mount with `ranges`, as `tmpfs` is, and on the root file system when it is in OPFS (in a browser with sync access handles) or an in-memory root, a program reads and writes a file in 64 KiB pages: reads come from the driver 1 MiB at a time, and changed pages go back in 1 MiB runs once 8 MiB have changed, at `fsync`, and when the file is closed (from the kernel's own descriptors also 250 ms after a write). A file of any size the mount can hold works, with memory to spare for about one more copy of it. In OPFS a write opens a sync access handle for just that write. An `fsa` mount (a picked folder) has no sync access handles, and Chrome copies the whole file into a swap file for every writable stream, so it writes through one stream per open file: the first write back opens it, later runs go into it at their offsets, and it is committed, one copy of the file, when the last process writing the file closes it (or exits), before another process reads what was written, and before a rename, unlink or unmount. Until then other processes see the new size but read the old bytes outside what was written. Elsewhere (`hostfs` with an older proxy, and a root outside OPFS), file contents are read whole when a program opens a file and written back when it closes it. Before a program's next call into the kernel (any wait, read of a pipe or terminal, sleep or spawn), its changed pages also go back, so other processes and the embedder see a write as soon as the writer does anything but compute: a daemon's pidfile, a log another process follows. On a whole-file mount that holds only for files of up to 64 KiB; a larger one goes back at close, fsync and exit, as before. Beyond that, both give close-to-open consistency, as NFS has. A process that ends without closing its files keeps what it wrote: on exit, on a trap, and when a signal kills it. For a killed WASI program, the kernel lets its worker write its changed pages back before `waitpid` reports the death. The only exception is a worker that makes no call into the kernel for 2 seconds after the signal; it is stopped without that write-back.
613
613
  - **Exclusive create across tabs** ([#912](https://github.com/ai-ecoverse/slicc-kernel/issues/912)): two kernels on one OPFS root (two tabs, or a tab and a worker page) agree on `O_CREAT|O_EXCL` and `set -C`: exactly one wins a name. While another kernel on the root is present, the create runs under a Web Lock for its path and checks the OPFS entry itself. A kernel alone on its root takes no lock. Each kernel holds an alive Web Lock, granted before it first looks for peers. A kernel that finds peers waits for their answer (at most 500 ms) before its first exclusive create, and a kernel answers a newcomer only once its unlocked creates are done. So of two kernels that boot at the same moment, the later one to look sees the other, and no unlocked create overlaps a locked one.
614
+ - **A name another process or tab just removed** ([#950](https://github.com/ai-ecoverse/slicc-kernel/issues/950)): sqlite3 deletes its `-journal` at every commit and the next writer creates it again, so two writers (two processes in one kernel, or two tabs on one OPFS root) keep removing and re-creating one name. Two things used to turn that into `unable to open database file`. The root removes a file's sidecar record a few milliseconds after the file itself (removals are batched), and an exclusive create read that pending record as the name being taken: a plain record whose file is gone now counts as gone, and the new file gets a record of its own. A symlink, which lives only in the sidecar, still takes its name. And an Emscripten program keeps the nodes it has looked up, so after another tab removed a name it still held the old node, and `open(O_CREAT)` opened the gone file instead of creating one. Now, when the kernel answers `ENOENT` for a cached node, for `open`, `stat`, `lstat`, `readlink`, `truncate`, `unlink` or a rename's source, or the program's own lookup answers `EEXIST` to `open(O_CREAT|O_EXCL)`, the node is dropped and the call runs once more, against what is there now. A node the program has open or has unwritten changes in is kept. A rename over a name another tab removed needs nothing: the kernel's rename replaces whatever is there.
614
615
  - **One file, several processes** ([#665](https://github.com/ai-ecoverse/slicc-kernel/issues/665)): a process that keeps a file open sees what another process writes to it. Every change the kernel applies to a file (a write back, a truncate, an append, a rename or a removal) bumps a change ring the kernel shares with all its processes. Before each read, write or seek to the end, a process's open file checks the ring (one atomic load) and, if its file changed, drops its clean pages and reads the size again, keeping its own unwritten changes. A write by one process is visible to another one's next read once it has gone back to the kernel: at the latest at the writer's next `fsync`, close, exit or call into the kernel (see above), so sqlite, which `fsync`s every commit, sees another connection's commit at its next transaction. A write through a file the kernel holds (a JS program's `ctx.open`, a WASI program's file after a fork or spawn) is in the kernel already, so other processes see it at their next read, before it is written back. A file the kernel itself holds open (a JS program's file, a WASI program's file after a fork or spawn) is the one copy: reads, writes, truncates and appends by path from other processes go to it, and their writes go through to storage at once, so a commit is in storage (and in `kernel.readFile`) when the committing process returns from `fsync`. `O_APPEND` writes go to the kernel at once, and the kernel picks the offset, so appenders in several processes keep every line. A file system without ranged I/O (`hostfs` with an older proxy, a root outside OPFS) rereads a file another process changed, but writes it back whole, so two writers there still need locks. The same file under two names is one file too ([#669](https://github.com/ai-ecoverse/slicc-kernel/issues/669)): a write by path through a symlink reaches the file the kernel holds under its real path (resolved without following links in a `sudo` process's no-links view, so an untrusted link is still refused), a WASI program's open file also watches its real path (resolved once, the first time another file changes), and an open file with more than one hard link checks its size, version and mtime when anything changed, so a write through another name is seen. Limits for now: an alias on a mount that has no `realpath` is still only the name it was opened by; another tab's writes reach this tab's processes only as far as the root's change notifications (OPFS's `BroadcastChannel` watch) carry them. A by-path `stat` or `lstat` of a file the kernel holds open has its current size, writes not yet written back included ([#780](https://github.com/ai-ecoverse/slicc-kernel/issues/780)). Its mtime is the file system's until write-back, though, so a tool that compares times, like `make`, may see a just-written file as older than it is for up to the write-back delay.
615
616
  - **Locks between processes** ([#665](https://github.com/ai-ecoverse/slicc-kernel/issues/665)): an Emscripten program's `fcntl` byte-range locks (`F_SETLK`, `F_SETLKW`, `F_GETLK`) are the kernel's, and so are a WASIX program's through the `slicc.fcntl_lock` import (below), so sqlite3 connections in different processes exclude each other: a second writer gets `database is locked` while the first holds its transaction, or waits with a busy timeout. The kernel keeps one table keyed by file identity (device and inode, so every name of a hard link is the same file), with POSIX semantics: locks belong to the process (an `exec`'d program keeps them under its pid; a forked child doesn't get them), a conflicting `F_SETLK` fails with `EAGAIN`, `F_GETLK` reports the holder's pid and range, `F_SETLKW` waits in the kernel without holding up anything else and a signal ends the wait with `EINTR`, a wait that would close a cycle between waiting processes fails with `EDEADLK`, a write lock on a descriptor not open for writing (or a read lock on one not open for reading) is `EBADF`, closing any descriptor of the file drops the process's locks on it, and exit releases them all. Before an unlock, the process writes back what it changed, so the next holder reads it. An open file's directory entry is checked against the change ring too: a name another process removed or replaced is looked up again, so sqlite's check for a hot journal sees the other connection's commit. A WASI imports module's `tryLock`/`unlock` keeps its behaviour (whole file, per process, keyed by path). Limits: locks are per kernel, not across tabs ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)); and a WASIX program built against a wasix-sysroot without the import keeps its libc's local answer (every lock granted), so python's sqlite excludes other processes only once wasix-python is rebuilt on the sysroot that has it ([homescoop#407](https://github.com/ai-ecoverse/homescoop/issues/407)). An Emscripten program's `flock()` reaches the kernel's `flock` table (per open file description) through homescoop's shim, a strong `flock()` that calls the import `env.__syscall_flock(fd, op)`. The kernel wraps that import by name (#947): `LOCK_SH`/`LOCK_EX`/`LOCK_UN` with `LOCK_NB` (`EWOULDBLOCK` for a conflict), 0 or -errno. A glue without the shim keeps Emscripten's stub, which answers 0. A lock belongs to the file its descriptor was opened on: a file renamed over the name later, or the name's removal, doesn't move it.
616
617
  - **Locks across tabs** ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)): two kernels on one OPFS root (two tabs, or a tab and a worker page) share one table of `fcntl` record locks and `flock` locks, so a sqlite3 in one tab finds the database locked while a transaction is open in another. **Known issue, data corruption: lock-protected writes across tabs** ([#949](https://github.com/ai-ecoverse/slicc-kernel/issues/949)). The locks are enforced across tabs, but what one tab writes under a lock isn't yet guaranteed to be what the other tab reads once it holds the lock, so a lock-protected read-modify-write across tabs can lose or corrupt data. Don't rely on cross-tab locking until #949 is fixed. Use within a single tab is unaffected.
@@ -917,6 +918,15 @@ Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `
917
918
 
918
919
  The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#users)). While the root has none of them, or no `/etc/mtab`, reading them gets per-process ones: root is uid 0 and the realm user, uid 1000, is named after `USER`, or `web_user` (Emscripten's default) without one, so it matches `$USER`, with `HOME` as its home, so `id`, `whoami`, `ls -l` and `ps` show names. Nothing is written to OPFS, and a real file there wins.
919
920
 
921
+ ### How many processes at once
922
+
923
+ In a browser every process worker of the page shares one renderer. V8 reserves about 8 GiB of address space for each 32-bit wasm memory, whatever its declared maximum, and its sandbox holds about 1 TiB. So a page runs about 125 wasm processes (or programs that own a wasm memory) at once, counting everything in the renderer (#970). Node has no such ceiling, and neither does Linux. The kernel keeps processes inside that ceiling:
924
+ - **The ceiling:** 112 by default in a browser and none in Node. It's the kernel's count of live wasm processes, with the kernel's other wasm memories in the page left outside it. When an instantiation runs out of memory anyway, the kernel learns the real ceiling at that moment (the live count minus one), and that learned value wins over the default.
925
+ - **Admission:** a spawn, fork, exec or `kernel.run` at the ceiling waits for another process to exit. It keeps waiting as long as processes exit or are admitted, and fails with `EAGAIN` only after 10 s with neither. A long `make -j` or `xargs -P` therefore queues, and a pipeline that needs more live processes than the ceiling fails cleanly. bash's own retry on a fork's `EAGAIN` (`fork: retry: Resource temporarily unavailable`) handles the rest.
926
+ - **Close to the ceiling** (within 16 of it), the kernel waits for each new process to finish instantiating before the spawn returns. If it ran out of memory, the kernel starts it again, quietly, once there's room, so the caller sees no failure.
927
+ - **The rare residual case:** a process that fails to instantiate with nobody waiting on it, which happens only when something outside the count fills the renderer. It ends with status 126 and `<program>: cannot allocate memory for a new process (too many processes at once)` on its stderr, never a JS `RangeError`.
928
+ - **`/proc/sys/kernel/slicc_wasm_memories`** reads `<live> <ceiling>` (the ceiling 0 for none).
929
+
920
930
  ### Metadata
921
931
 
922
932
  OPFS stores names, bytes, sizes and modification times, nothing else. Everything POSIX needs on top of that lives in an IndexedDB sidecar, so it survives reloads and new kernels:
@@ -2051,6 +2051,7 @@ var OpfsFs = class {
2051
2051
  if (await this.handleExists(dir, name2)) throw fsError("EEXIST", path);
2052
2052
  const freshMeta = this.meta.fresh ? await this.meta.fresh([path]) : await this.meta.get([path]);
2053
2053
  if (freshMeta[0]?.link !== void 0) throw fsError("EEXIST", path);
2054
+ if (freshMeta[0] !== void 0) await this.meta.remove([path]);
2054
2055
  this.forget(physical);
2055
2056
  this.refuse(physical);
2056
2057
  await dir.getDirectoryHandle(name2, { create: true });
@@ -2070,7 +2071,8 @@ var OpfsFs = class {
2070
2071
  await this.locked(physical, async () => {
2071
2072
  if (await this.handleExists(dir, name2)) throw fsError("EEXIST", path);
2072
2073
  const freshMeta = this.meta.fresh ? await this.meta.fresh([path]) : await this.meta.get([path]);
2073
- if (freshMeta[0] !== void 0) throw fsError("EEXIST", path);
2074
+ if (freshMeta[0]?.link !== void 0) throw fsError("EEXIST", path);
2075
+ if (freshMeta[0] !== void 0) await this.meta.remove([path]);
2074
2076
  this.forget(physical);
2075
2077
  this.refuse(physical);
2076
2078
  await dir.getFileHandle(name2, { create: true });
@@ -6406,7 +6408,7 @@ function satisfies(version, range2) {
6406
6408
  }
6407
6409
 
6408
6410
  // src/version.ts
6409
- var KERNEL_VERSION = true ? "1.122.2" : "0.0.0-dev";
6411
+ var KERNEL_VERSION = true ? "1.123.0" : "0.0.0-dev";
6410
6412
  var OS_RELEASE = "6.0.0-slicc";
6411
6413
  function procVersion() {
6412
6414
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -12549,17 +12551,28 @@ var FALLBACK_MEMORY = 4 * KIB * KIB * KIB;
12549
12551
  var PID_FILES = ["cmdline", "comm", "stat", "statm", "status"];
12550
12552
  var SYSTEM_FILES = ["loadavg", "meminfo", "stat", "uptime", "version"];
12551
12553
  var SYS_TREE = {
12552
- kernel: { osrelease: `${OS_RELEASE}
12553
- `, ostype: "Linux\n" }
12554
+ kernel: {
12555
+ osrelease: `${OS_RELEASE}
12556
+ `,
12557
+ ostype: "Linux\n",
12558
+ slicc_wasm_memories: (listing) => `${listing.wasm?.live ?? 0} ${listing.wasm?.ceiling ?? 0}
12559
+ `
12560
+ }
12554
12561
  };
12562
+ function isSysFile(at) {
12563
+ return typeof at === "string" || typeof at === "function";
12564
+ }
12555
12565
  function sysEntry(parts2) {
12556
12566
  let at = SYS_TREE;
12557
12567
  for (const part of parts2) {
12558
- if (typeof at !== "object" || !Object.hasOwn(at, part)) return void 0;
12568
+ if (isSysFile(at) || !Object.hasOwn(at, part)) return void 0;
12559
12569
  at = at[part];
12560
12570
  }
12561
12571
  return at;
12562
12572
  }
12573
+ function sysText(file, listing) {
12574
+ return typeof file === "string" ? file : file(listing());
12575
+ }
12563
12576
  function commOf(info) {
12564
12577
  const name2 = info.argv[0] ?? "";
12565
12578
  return name2.slice(name2.lastIndexOf("/") + 1).slice(0, 15);
@@ -12795,7 +12808,9 @@ var ProcTree = class {
12795
12808
  }
12796
12809
  if (top === "sys") {
12797
12810
  const entry = sysEntry(parts2.slice(1));
12798
- if (typeof entry === "string") return { kind: "file", content: () => entry };
12811
+ if (typeof entry === "string" || typeof entry === "function") {
12812
+ return { kind: "file", content: () => sysText(entry, () => this.listing()) };
12813
+ }
12799
12814
  return entry && { kind: "dir", names: () => Object.keys(entry) };
12800
12815
  }
12801
12816
  if (top === "self") {
@@ -19252,6 +19267,7 @@ var WasmProcess = class {
19252
19267
  var WASM_PROCESS_INIT = "wasm-process-init";
19253
19268
  var WASM_PROCESS_EXIT = "wasm-process-exit";
19254
19269
  var WASM_PROCESS_ERROR = "wasm-process-error";
19270
+ var WASM_PROCESS_READY = "wasm-process-ready";
19255
19271
  var WASM_THREAD_SPAWN = "wasm-thread-spawn";
19256
19272
  var WASM_THREAD_INIT = "wasm-thread-init";
19257
19273
  var WASM_THREAD_EXIT = "wasm-thread-exit";
@@ -19259,6 +19275,8 @@ var WASM_MAX_THREADS = 256;
19259
19275
 
19260
19276
  // src/kernel/host.ts
19261
19277
  var CRASHED = 70;
19278
+ var NO_MEMORY = 126;
19279
+ var WASM_OOM = /Cannot allocate Wasm memory|WebAssembly\.Memory\(\): could not allocate memory/;
19262
19280
  var KILL_GRACE_MS = 2e3;
19263
19281
  var KILL_HARD_MS = 5e3;
19264
19282
  var descIds = /* @__PURE__ */ new WeakMap();
@@ -19524,7 +19542,22 @@ function threadInit(opts, program, sab, cred, thread, main) {
19524
19542
  function settleExit(exit, settle3, code) {
19525
19543
  void exit.catch(() => void 0).then(() => settle3(code & 255));
19526
19544
  }
19545
+ function readiness(abi) {
19546
+ const ready2 = Promise.withResolvers();
19547
+ if (abi === "js") ready2.resolve(true);
19548
+ return ready2;
19549
+ }
19550
+ function outOfMemory(opts, data) {
19551
+ if (data?.type !== WASM_PROCESS_ERROR || !WASM_OOM.test(String(data.message))) return false;
19552
+ if (!opts.onOom?.()) {
19553
+ opts.onError?.(
19554
+ `${opts.argv0}: cannot allocate memory for a new process (too many processes at once)`
19555
+ );
19556
+ }
19557
+ return true;
19558
+ }
19527
19559
  function spawnWasmProcess(opts) {
19560
+ const { promise: ready2, resolve: started } = readiness(opts.program.abi);
19528
19561
  const sab = new SharedArrayBuffer(SAB_HEADER_BYTES + SAB_DEFAULT_WINDOW_BYTES);
19529
19562
  const header3 = new Int32Array(sab, 0, SAB_HEADER_I32);
19530
19563
  const blocked2 = opts.fork?.wasi?.blocked ?? opts.blocked;
@@ -19577,6 +19610,7 @@ function spawnWasmProcess(opts) {
19577
19610
  const finish = (code, sig) => {
19578
19611
  if (done4) return;
19579
19612
  done4 = true;
19613
+ started(true);
19580
19614
  clocks.freeze();
19581
19615
  dying.clear();
19582
19616
  endedBy = sig ?? process.execTermsig;
@@ -19594,7 +19628,12 @@ function spawnWasmProcess(opts) {
19594
19628
  queueMicrotask(() => finish(...dying.status(code)));
19595
19629
  };
19596
19630
  const handle = (data) => {
19597
- if (data?.type === WASM_PROCESS_EXIT) {
19631
+ if (data?.type === WASM_PROCESS_READY) {
19632
+ started(true);
19633
+ } else if (outOfMemory(opts, data)) {
19634
+ started(false);
19635
+ finish(NO_MEMORY);
19636
+ } else if (data?.type === WASM_PROCESS_EXIT) {
19598
19637
  finish(...dying.status(data.code));
19599
19638
  } else if (data?.type === WASM_PROCESS_ERROR) {
19600
19639
  if (!dying.ending) opts.onError?.(String(data.message));
@@ -19668,6 +19707,7 @@ function spawnWasmProcess(opts) {
19668
19707
  return {
19669
19708
  pid: opts.pid,
19670
19709
  exited,
19710
+ ready: ready2,
19671
19711
  kill,
19672
19712
  signal,
19673
19713
  termsig: () => endedBy,
@@ -21262,6 +21302,63 @@ var SettlingChildren = class {
21262
21302
  }
21263
21303
  };
21264
21304
 
21305
+ // src/kernel/wasm-memories.ts
21306
+ var DEFAULT_CEILING = 112;
21307
+ var IDLE_MS = 1e4;
21308
+ var MARGIN = 16;
21309
+ var WasmMemories = class {
21310
+ live = 0;
21311
+ ceiling;
21312
+ waiters = /* @__PURE__ */ new Set();
21313
+ idleMs;
21314
+ constructor(ceiling, idleMs = IDLE_MS) {
21315
+ this.ceiling = ceiling;
21316
+ this.idleMs = idleMs;
21317
+ }
21318
+ count() {
21319
+ return { live: this.live, ceiling: this.ceiling ?? 0 };
21320
+ }
21321
+ ended() {
21322
+ this.live = Math.max(0, this.live - 1);
21323
+ this.progress();
21324
+ }
21325
+ learn() {
21326
+ this.ceiling = Math.max(1, this.live - 1);
21327
+ }
21328
+ near() {
21329
+ return this.ceiling !== void 0 && this.live + MARGIN >= this.ceiling;
21330
+ }
21331
+ async admit() {
21332
+ while (this.ceiling !== void 0 && this.live >= this.ceiling) {
21333
+ if (!await this.changed()) throw new SpawnError("EAGAIN");
21334
+ }
21335
+ this.live += 1;
21336
+ this.progress();
21337
+ }
21338
+ progress() {
21339
+ const waiters = [...this.waiters];
21340
+ this.waiters.clear();
21341
+ for (const wake of waiters) wake();
21342
+ }
21343
+ changed() {
21344
+ return new Promise((resolve2) => {
21345
+ const wake = () => {
21346
+ clearTimeout(timer);
21347
+ resolve2(true);
21348
+ };
21349
+ const timer = setTimeout(() => {
21350
+ this.waiters.delete(wake);
21351
+ resolve2(false);
21352
+ }, this.idleMs);
21353
+ this.waiters.add(wake);
21354
+ });
21355
+ }
21356
+ };
21357
+ function defaultCeiling() {
21358
+ const node = globalThis.process?.versions?.node;
21359
+ return node === void 0 ? DEFAULT_CEILING : void 0;
21360
+ }
21361
+
21265
21362
  // src/mount/driver.ts
21266
21363
  function ownerOf2(req) {
21267
21364
  const owner = {
@@ -23459,6 +23556,9 @@ function withScriptEnv(target, command) {
23459
23556
  ]
23460
23557
  };
23461
23558
  }
23559
+ function memoriesOf(given) {
23560
+ return new WasmMemories(given?.ceiling ?? defaultCeiling(), given?.idleMs);
23561
+ }
23462
23562
  function childHandle(handle) {
23463
23563
  return {
23464
23564
  pid: handle.pid,
@@ -23530,6 +23630,7 @@ var Launcher = class {
23530
23630
  jobs = new JobTable();
23531
23631
  locks;
23532
23632
  fifos = new FifoTable();
23633
+ memories;
23533
23634
  sems = new SemTable();
23534
23635
  settling = new SettlingChildren((pid) => this.jobs.pgidOf(pid));
23535
23636
  ptys = new PtyTable(
@@ -23583,6 +23684,7 @@ var Launcher = class {
23583
23684
  terminals = 0;
23584
23685
  constructor(options) {
23585
23686
  this.usersMode = usersMode(options.users);
23687
+ this.memories = memoriesOf(options.wasmMemories);
23586
23688
  this.rootFs = options.fs;
23587
23689
  const peers = options.fs.peers?.();
23588
23690
  this.locks = kernelLocks(options.fs.rootName, peers);
@@ -24413,7 +24515,11 @@ var Launcher = class {
24413
24515
  }
24414
24516
  const done4 = this.heldUrl(program);
24415
24517
  try {
24416
- const handle = this.start({ ...req, cwd, program, argv0: target.argv0, args, env });
24518
+ const handle = await this.admitted(
24519
+ program.abi !== "js",
24520
+ req.fds,
24521
+ (fds, quietOom) => this.start({ ...req, fds, quietOom, cwd, program, argv0: target.argv0, args, env })
24522
+ );
24417
24523
  void handle.exited.finally(done4);
24418
24524
  return handle;
24419
24525
  } catch (err) {
@@ -24457,6 +24563,39 @@ var Launcher = class {
24457
24563
  if (req.tag !== void 0) this.tags.set(pid, req.tag);
24458
24564
  if (req.links !== void 0) this.links.set(pid, req.links);
24459
24565
  }
24566
+ async admitted(wasm, fds, start2) {
24567
+ if (!wasm) return start2(fds);
24568
+ for (; ; ) {
24569
+ await this.memories.admit().catch(async (err) => {
24570
+ await fds.closeAll();
24571
+ throw err;
24572
+ });
24573
+ const counted2 = (handle2) => {
24574
+ void handle2.exited.finally(() => this.memories.ended());
24575
+ return handle2;
24576
+ };
24577
+ if (!this.memories.near()) {
24578
+ try {
24579
+ return counted2(start2(fds));
24580
+ } catch (err) {
24581
+ this.memories.ended();
24582
+ throw err;
24583
+ }
24584
+ }
24585
+ const forked = fds.fork();
24586
+ const handle = await Promise.resolve().then(() => counted2(start2(forked, true))).catch(async (err) => {
24587
+ this.memories.ended();
24588
+ await forked.closeAll();
24589
+ throw err;
24590
+ });
24591
+ const fit = await (handle.ready ?? true);
24592
+ if (fit) {
24593
+ await fds.closeAll();
24594
+ return handle;
24595
+ }
24596
+ await handle.exited;
24597
+ }
24598
+ }
24460
24599
  start(req) {
24461
24600
  this.sampling();
24462
24601
  if (req.pgid !== void 0 && this.jobs.sessionOf(req.pgid) === void 0) {
@@ -24507,6 +24646,10 @@ var Launcher = class {
24507
24646
  ...this.prefixes.length > 0 && !stack.writer ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
24508
24647
  createWorker: this.createWorker,
24509
24648
  onError: req.report,
24649
+ onOom: () => {
24650
+ this.memories.learn();
24651
+ return req.quietOom === true;
24652
+ },
24510
24653
  spawner: this.spawner(pid, req.report),
24511
24654
  forker: this.forker(pid, req),
24512
24655
  kill: (target, sig, info) => this.kill(target, sig, this.credOf(pid), pid, info),
@@ -24515,7 +24658,8 @@ var Launcher = class {
24515
24658
  boot: this.boot,
24516
24659
  processes: this.list(this.credOf(pid)),
24517
24660
  load: this.loadAverage(),
24518
- cpu: this.cpuTotal()
24661
+ cpu: this.cpuTotal(),
24662
+ wasm: this.memories.count()
24519
24663
  }),
24520
24664
  sudo: (call, signal) => this.sudo(pid, call, signal),
24521
24665
  crontab: (call) => this.crontab(pid, call),
@@ -24852,9 +24996,10 @@ var Launcher = class {
24852
24996
  }
24853
24997
  forker(ppid, parent) {
24854
24998
  const { exec: _exec, pgid: _pgid, group: _group, pid: _pid, scrub: _scrub, ...image } = parent;
24855
- return async (state, fds) => childHandle(
24856
- this.start({
24999
+ return async (state, given) => {
25000
+ const fresh2 = (fds, quietOom) => this.start({
24857
25001
  ...image,
25002
+ quietOom,
24858
25003
  cwd: state.cwd ?? parent.cwd,
24859
25004
  fds,
24860
25005
  ppid,
@@ -24865,8 +25010,9 @@ var Launcher = class {
24865
25010
  cred: this.credOf(ppid),
24866
25011
  tag: this.tagOf(ppid),
24867
25012
  links: this.links.get(ppid)
24868
- })
24869
- );
25013
+ });
25014
+ return childHandle(await this.admitted(parent.program.abi !== "js", given, fresh2));
25015
+ };
24870
25016
  }
24871
25017
  keySignal(tty, fallback, sig, send) {
24872
25018
  const pgid = this.jobs.tcgetpgrp(tty, fallback);