@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 +4 -4
- package/dist/kernel-worker.js +35 -10
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +174 -158
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +36 -10
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +174 -158
- package/dist/process-worker.js.map +4 -4
- package/package.json +3 -3
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
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
);
|