@ai-ecoverse/slicc-kernel 1.26.0 → 1.26.2

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
@@ -120,7 +120,7 @@ const { status, stdout } = await kernel.run(['bash', '-c', 'echo hi'], { cwd: '/
120
120
  kernel.terminate();
121
121
  ```
122
122
 
123
- `createNodeKernel({ root, modules, env, network, worker, processMounts, fstabRetries, cdp })` takes the options of `createKernel` except `metadata`, and `fstabRetries`, the delays in ms between tries of a failing `/etc/fstab` line (default `[1000, 4000, 16000]`). `root` is an in-memory directory by default (`memoryRoot()` makes another), which keeps each file in 1 MiB chunks, and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal` and `terminate` as above, plus `root`, `writeFile(path, data)` (creating the parent directories) and `readFile(path)` to put files in place and read results. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect()` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
123
+ `createNodeKernel({ root, modules, env, network, worker, processMounts, fstabRetries, cdp, hostfs })` takes the options of `createKernel` except `metadata`, and `fstabRetries`, the delays in ms between tries of a failing `/etc/fstab` line (default `[1000, 4000, 16000]`). `root` is an in-memory directory by default (`memoryRoot()` makes another), which keeps each file in 1 MiB chunks, and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal` and `terminate` as above, plus `root`, `writeFile(path, data)` (creating the parent directories) and `readFile(path)` to put files in place and read results. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect()` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
124
124
 
125
125
  ## Commands
126
126
 
@@ -144,7 +144,7 @@ A WASI program runs in a process worker of its own like an Emscripten one, again
144
144
 
145
145
  - **Threads**: `wasi.thread-spawn` (wasm32-wasip1-threads) and WASIX `thread_spawn_v2` start a worker per thread on the process's shared memory, sized from the module's own import (a 2 GiB maximum when the engine cannot reserve more). The threads share one descriptor table; `exit` or a trap in any thread ends the process. At most 64 threads per process; `SLICC_WASM_THREADS` sets another cap, up to 256.
146
146
  - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid`, pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`.
147
- - **Sockets**: the WASIX socket calls (`sock_open`, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers loopback names and literal addresses.
147
+ - **Sockets**: the WASIX socket calls (`sock_open`, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers loopback names and literal addresses.
148
148
  - **Diagnostics**: a trap ends the program with 134 and its message on stderr; with `SLICC_WASM_BACKTRACE=1` the wasm frames follow, named from the module's name section, or from a sidecar for a module shipped without one (`<module>.names` beside it, or the same path in a `<package>-names` package). `SLICC_WASI_STATS=1` makes a program print its calls, counted and timed, as it ends.
149
149
 
150
150
  ### Program imports
@@ -184,7 +184,7 @@ await kernel.run(['curl', '-sS', 'https://registry.npmjs.org/@ai-ecoverse/wasm-b
184
184
  Programs drive a browser with the Chrome DevTools Protocol over a WebSocket, whichever host actually has the browser: slicc-extension's `chrome.debugger`, slicc-node's or slicc-swift's CDP proxy, or a test harness. The kernel relays; it does not launch a browser.
185
185
 
186
186
  - **Programs** find the endpoint in `SLICC_CDP_URL`, `ws://127.0.0.1:9222/devtools/browser/<id>`, where `<id>` is random for each kernel boot. `http://127.0.0.1:9222/json/version` answers `{ "Browser", "Protocol-Version", "webSocketDebuggerUrl" }` with the same URL (carrying the request's query), and `/json/list` (or `/json`) lists the host's targets, without per-target sockets. Every other path is `404`. The listener starts on the first connection, on the kernel's loopback; a program that binds `9222` first keeps it.
187
- - **Sessions** are Chrome's flattened ones: `Target.attachToTarget` with `flatten: true`, then commands with a top-level `sessionId`. Messages pass unchanged, as text frames up to 256 MiB (a larger one closes the socket with `1009`). Each program WebSocket gets a host connection of its own: closing the socket (or exiting) closes it, which detaches its sessions, and a host that closes is passed on as a close frame (`1011` with its reason). `terminate()` closes them all.
187
+ - **Sessions** are Chrome's flattened ones: `Target.attachToTarget` with `flatten: true`, then commands with a top-level `sessionId`. Messages pass unchanged, as text frames up to 256 MiB (a larger one closes the socket with `1009`). Each program WebSocket gets a host connection of its own: closing the socket (or exiting) closes it, which detaches its sessions, and a host that closes is passed on as a close frame (`1011` with its reason). The kernel then waits up to a second for the program's close frame before it hangs up, as RFC 6455 has it, so a program that answers the close can still report the code. `terminate()` closes them all.
188
188
  - **Runtime**: a program adds `runtime=<name>` to the query of the URL it opens, and the kernel gives it to the host; the kernel does not interpret it.
189
189
  - **No host**: `/json/*` and the WebSocket handshake answer `503` with `slicc-kernel: no CDP host is attached (createKernel({ cdp }) or client.serveCdp)`, and a host that refuses (an unknown runtime, say) gives `502` with its message.
190
190
  - **Kernel-only**: port `9222` takes connections only from programs in the kernel. `kernel.dial`, a client's `dial` and `loopbackFetch` (and so `9222.kernel.localhost` and slicc-node's tunnel) are refused with `ECONNREFUSED`, since the facade drives the user's real browser.
@@ -282,7 +282,7 @@ A `hostfs` mount is a folder exported by the local proxy ([slicc-node](https://g
282
282
  - **Options:** `ro` mounts read-only, and `maxfile` caps file size as on any mount. There is no other size limit: reads and writes go in `maxIo` pieces (16 MiB unless the proxy says otherwise), so a file of any size the host can hold works. A proxy that advertises `ranges` in its grant (slicc-node 2.4, slicc-swift 1.4) makes the mount ranged, so programs read and write its files in pages and truncate through `setattr` `size`. With an older proxy files go whole, and a truncate on a ranged mount whose proxy was replaced by an older one is done by rewriting the file.
283
283
  - **Changes on the host** arrive as invalidations on a watch stream (`POST /api/hostfs/watch`), so an edit made outside shows up on the next access without a remount.
284
284
  - **Proxy loss:** when the watch stream ends, or is silent for 45 s, the kernel reconnects once at once. If that fails, the mount is `nomedium`, as with [removable media](#removable-media-fsa): operations fail with `ENOMEDIUM` (`ENODEV` for WASI), and the kernel keeps reconnecting with backoff (1 s to 30 s), so the folder comes back by itself when the proxy does. A mount made while the proxy is down starts as `nomedium`, and so does one whose grant or first watch takes more than 15 s; it comes up by itself when they arrive.
285
- - **Consistency:** a file that changes on the host while a program reads it fails with `ESTALE`, and the kernel restarts the whole read (up to 3 times) instead of stitching two versions together. On a ranged mount an open file keeps the version it was opened at (its ETag) until it writes: a page fetched after the host changed the file fails the program's read with `ESTALE` instead of mixing versions.
285
+ - **Consistency:** a file that changes on the host while a program reads it fails with `ESTALE`, and the kernel restarts the whole read (up to 3 times) instead of stitching two versions together. On a ranged mount an open file keeps the version it was opened at (its ETag) until it writes, also when it is handed to a child process: a page fetched after the host changed the file fails the program's read with `ESTALE` ("Stale file handle") instead of mixing versions.
286
286
 
287
287
  ### Mounting from a program
288
288
 
@@ -3749,6 +3749,7 @@ Sec-WebSocket-Accept: ${accept2}\r
3749
3749
  // src/cdp/facade.ts
3750
3750
  var MAX_HEAD = 64 * 1024;
3751
3751
  var LIST_TIMEOUT = 1e4;
3752
+ var CLOSE_WAIT_MS = 1e3;
3752
3753
  function respond(status, body, type = "text/plain; charset=utf-8") {
3753
3754
  const bytes = new TextEncoder().encode(body);
3754
3755
  const head = latin1Bytes(
@@ -3894,7 +3895,11 @@ var CdpFacade = class {
3894
3895
  async upgrade(conn, incoming, req, runtime) {
3895
3896
  const accept2 = await acceptKey(req.headers);
3896
3897
  const host = await this.host(runtime);
3897
- await new Relay(conn, incoming, host, this.options.maxMessage ?? MAX_MESSAGE, this.stop.signal).start(switchingHead(accept2)).finally(() => host.close());
3898
+ const limits = {
3899
+ max: this.options.maxMessage ?? MAX_MESSAGE,
3900
+ closeWaitMs: this.options.closeWaitMs ?? CLOSE_WAIT_MS
3901
+ };
3902
+ await new Relay(conn, incoming, host, limits, this.stop.signal).start(switchingHead(accept2)).finally(() => host.close());
3898
3903
  }
3899
3904
  };
3900
3905
  var Relay = class {
@@ -3904,12 +3909,14 @@ var Relay = class {
3904
3909
  incoming;
3905
3910
  host;
3906
3911
  max;
3912
+ closeWaitMs;
3907
3913
  stop;
3908
- constructor(conn, incoming, host, max, stop) {
3914
+ constructor(conn, incoming, host, limits, stop) {
3909
3915
  this.conn = conn;
3910
3916
  this.incoming = incoming;
3911
3917
  this.host = host;
3912
- this.max = max;
3918
+ this.max = limits.max;
3919
+ this.closeWaitMs = limits.closeWaitMs;
3913
3920
  this.stop = stop;
3914
3921
  }
3915
3922
  send(bytes) {
@@ -3935,8 +3942,15 @@ var Relay = class {
3935
3942
  void this.end(CLOSE.tooBig, `a message is over ${this.max} bytes`);
3936
3943
  else void this.send(frame(OP_TEXT, bytes));
3937
3944
  };
3938
- await Promise.race([this.pump(), hostGone]);
3945
+ const pumping = this.pump();
3946
+ await Promise.race([pumping, hostGone]);
3939
3947
  await this.writes;
3948
+ let timer;
3949
+ const waited = new Promise((resolve) => {
3950
+ timer = setTimeout(resolve, this.closeWaitMs);
3951
+ });
3952
+ await Promise.race([pumping, waited]);
3953
+ clearTimeout(timer);
3940
3954
  }
3941
3955
  async pump() {
3942
3956
  const reader = new WsReader(this.incoming, this.max);
@@ -5089,6 +5103,9 @@ var MountFs = class {
5089
5103
  truncate: false,
5090
5104
  exclusive: false,
5091
5105
  ...version !== void 0 ? { ifMatch: version } : {}
5106
+ }).catch((err) => {
5107
+ const gone = version !== void 0 && err.code === "ENOENT";
5108
+ throw gone ? errnoError("ESTALE", `${real} is gone from the host`) : err;
5092
5109
  });
5093
5110
  const chunk2 = mount.caps.maxIo ?? DEFAULT_IO;
5094
5111
  const chunks = [];
@@ -6689,6 +6706,7 @@ function within3(path, root) {
6689
6706
  var VfsNode = class {
6690
6707
  data;
6691
6708
  ranged;
6709
+ pin;
6692
6710
  length = 0;
6693
6711
  dirty = false;
6694
6712
  queue = Promise.resolve();
@@ -6700,8 +6718,9 @@ var VfsNode = class {
6700
6718
  fs;
6701
6719
  path;
6702
6720
  orphaned;
6703
- constructor(fs, path, contents2, orphaned = false, dirty = false) {
6721
+ constructor(fs, path, contents2, orphaned = false, dirty = false, pin) {
6704
6722
  this.fs = fs;
6723
+ this.pin = pin;
6705
6724
  this.path = path;
6706
6725
  this.orphaned = orphaned;
6707
6726
  if (contents2 !== void 0) this.data = new Uint8Array(contents2);
@@ -6725,6 +6744,10 @@ var VfsNode = class {
6725
6744
  if (this.revoked) throw new KernelError("EIO");
6726
6745
  if (this.ranged || this.data) return this.ranged;
6727
6746
  const io = rangedOps(this.fs);
6747
+ if (io && this.pin && !this.orphaned) {
6748
+ this.ranged = new AsyncRangedFile(io, this.path, this.pin.size, this.pin.version);
6749
+ return this.ranged;
6750
+ }
6728
6751
  const st = io && !this.orphaned ? await this.fs.stat?.(this.path).catch(() => void 0) : void 0;
6729
6752
  if (io && st?.ranged) {
6730
6753
  this.ranged = new AsyncRangedFile(io, this.path, st.size ?? 0, st.version);
@@ -6864,10 +6887,10 @@ var VfsNodes = class {
6864
6887
  constructor(fs) {
6865
6888
  this.fs = fs;
6866
6889
  }
6867
- open(path) {
6890
+ open(path, pin) {
6868
6891
  let node = this.byPath.get(path);
6869
6892
  if (!node) {
6870
- node = new VfsNode(this.fs, path);
6893
+ node = new VfsNode(this.fs, path, void 0, false, false, pin);
6871
6894
  this.byPath.set(path, node);
6872
6895
  }
6873
6896
  node.opens++;
@@ -6988,7 +7011,7 @@ function vfsFile(fs, opts, nodes) {
6988
7011
  const access = opts.flags & O_ACCMODE;
6989
7012
  const readable = access !== O_WRONLY;
6990
7013
  const writable = access === O_WRONLY || access === O_RDWR;
6991
- const node = !nodes || opts.orphan ? new VfsNode(fs, opts.path, opts.contents, opts.orphan === true, opts.dirty === true) : opts.contents !== void 0 ? nodes.adopt(opts.path, opts.contents, opts.dirty === true) : nodes.open(opts.path);
7014
+ const node = !nodes || opts.orphan ? new VfsNode(fs, opts.path, opts.contents, opts.orphan === true, opts.dirty === true) : opts.contents !== void 0 ? nodes.adopt(opts.path, opts.contents, opts.dirty === true) : nodes.open(opts.path, opts.pin);
6992
7015
  let openError;
6993
7016
  const atOpen = (op) => {
6994
7017
  void node.serial(op).catch((err) => {
@@ -7306,7 +7329,8 @@ var WasmProcess = class {
7306
7329
  ...req.contents !== void 0 ? { contents: req.contents } : {},
7307
7330
  ...req.orphan ? { orphan: true } : {},
7308
7331
  ...req.truncate ? { truncate: true } : {},
7309
- ...req.create ? { create: true } : {}
7332
+ ...req.create ? { create: true } : {},
7333
+ ...req.pin ? { pin: req.pin } : {}
7310
7334
  },
7311
7335
  this.nodes
7312
7336
  );
@@ -7438,7 +7462,8 @@ var WasmProcess = class {
7438
7462
  position: req.position ?? 0,
7439
7463
  ...req.contents !== void 0 ? { contents: req.contents } : {},
7440
7464
  ...req.orphan ? { orphan: true } : {},
7441
- ...req.dirty ? { dirty: true } : {}
7465
+ ...req.dirty ? { dirty: true } : {},
7466
+ ...req.pin ? { pin: req.pin } : {}
7442
7467
  },
7443
7468
  this.nodes
7444
7469
  );