@ai-ecoverse/slicc-kernel 1.31.1 → 1.32.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 +4 -3
- package/dist/kernel-worker.js +66 -12
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +23 -6
- package/dist/node-process-worker.js.map +3 -3
- package/dist/node.js +78 -12
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +23 -6
- package/dist/process-worker.js.map +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ const { status, stdout } = await kernel.run(['bash', '-c', 'echo hi'], { cwd: '/
|
|
|
125
125
|
kernel.terminate();
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
`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`, `setRoutes` 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.
|
|
128
|
+
`createNodeKernel({ root, modules, env, network, worker, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch })` 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`, `setRoutes` 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.
|
|
129
129
|
|
|
130
130
|
## Commands
|
|
131
131
|
|
|
@@ -325,7 +325,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
|
|
|
325
325
|
- `@ai-ecoverse/slicc-kernel/driver` exports the types and `fsError`.
|
|
326
326
|
- **Capabilities:** `readonly`, `symlinks`, `chmod`, `ranges`, `maxIo` (the largest read or write per message), `maxFile`, `listingStats`, `linkTimes`, and `attrTtl`/`entryTtl` (in ms, default 1000). `linkTimes` promises that `setattr` with `mtime` on a symlink sets the link's own time. Without it, setting a link's own times (`touch -h`, `utimensat` with `AT_SYMLINK_NOFOLLOW`) fails with `EOPNOTSUPP` and never reaches the target. tmpfs has it; hostfs does not. Setting times through a link follows it in the kernel, so `setattr` gets the target. `ranges` (driver protocol 1.1) promises that a write lands in the file at once, with no copy of the whole file on `open` or `release`, and that `setattr` takes `size`.
|
|
327
327
|
- **Caching:** the kernel caches attributes and listings for those TTLs, and a driver can push `invalidate` for changes made outside.
|
|
328
|
-
- **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. Elsewhere (`fsa` mounts, `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. Both give close-to-open consistency, as NFS has.
|
|
328
|
+
- **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. Elsewhere (`fsa` mounts, `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. 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.
|
|
329
329
|
- **Rules on a mount:**
|
|
330
330
|
- a rename across mounts is `EXDEV`, so `mv` copies;
|
|
331
331
|
- `df` reports each mount from its driver's `statfs`;
|
|
@@ -373,7 +373,8 @@ A `hostfs` mount is a folder exported by the local proxy ([slicc-node](https://g
|
|
|
373
373
|
- **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.
|
|
374
374
|
- **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.
|
|
375
375
|
- **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.
|
|
376
|
-
- **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.
|
|
376
|
+
- **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. A file deleted on the host while a program holds it open fails the next read that goes to the host with `ENOENT`, not with NFS's `ESTALE`.
|
|
377
|
+
- **Node:** a page's requests carry its `Origin`, and slicc-node grants a token to that origin only. Node's `fetch` sends none, so a Node embedder passes `createNodeKernel` the origin its `hostfs` hook asked for the grant with, as `hostfsOrigin`, and the kernel adds it to every hostfs request. Without it the mount stays `nomedium`. `hostfsFetch(url, init)` replaces the `fetch` those requests use (for an agent or a test proxy), and gets the `Origin` too when both are given.
|
|
377
378
|
|
|
378
379
|
### Mounting from a program
|
|
379
380
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -5976,6 +5976,7 @@ var SAB_I_TIMERS = 9;
|
|
|
5976
5976
|
var SAB_I_MEMORY = 10;
|
|
5977
5977
|
var SAB_I_TOTAL_HI = 11;
|
|
5978
5978
|
var SAB_I_OFFSET_HI = 12;
|
|
5979
|
+
var SAB_I_KILLED = 13;
|
|
5979
5980
|
var U32 = 2 ** 32;
|
|
5980
5981
|
function storeU53(header2, lo, hi, value) {
|
|
5981
5982
|
Atomics.store(header2, hi, Math.floor(value / U32));
|
|
@@ -7817,6 +7818,7 @@ function isWasmSyscall(req) {
|
|
|
7817
7818
|
return typeof op === "string" && SYSCALL_OPS.has(op);
|
|
7818
7819
|
}
|
|
7819
7820
|
var MAX_READ = 1024 * 1024;
|
|
7821
|
+
var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
|
|
7820
7822
|
var HOSTS_ONLY = new Resolver({ routes: new Routes() });
|
|
7821
7823
|
var WasmProcess = class {
|
|
7822
7824
|
exited = false;
|
|
@@ -7825,6 +7827,7 @@ var WasmProcess = class {
|
|
|
7825
7827
|
ignored = 0;
|
|
7826
7828
|
inherited = 0;
|
|
7827
7829
|
interrupt = new AbortController();
|
|
7830
|
+
dying = false;
|
|
7828
7831
|
execChild;
|
|
7829
7832
|
execTermsig;
|
|
7830
7833
|
stopped = 0;
|
|
@@ -7857,7 +7860,10 @@ var WasmProcess = class {
|
|
|
7857
7860
|
signal(sig) {
|
|
7858
7861
|
if (this.execChild !== void 0) {
|
|
7859
7862
|
void Promise.resolve(this.options.kill?.(this.execChild, sig)).catch(() => void 0);
|
|
7860
|
-
|
|
7863
|
+
if (sig !== SIG.KILL) return "forward";
|
|
7864
|
+
if (!this.options.writesBack?.(this.execChild)) return "terminate";
|
|
7865
|
+
this.wake?.();
|
|
7866
|
+
return "forward";
|
|
7861
7867
|
}
|
|
7862
7868
|
if (sig === SIG.KILL) return "terminate";
|
|
7863
7869
|
if (sig === SIG.CONT) this.cont();
|
|
@@ -7901,14 +7907,22 @@ var WasmProcess = class {
|
|
|
7901
7907
|
this.options.onSyscall?.(req);
|
|
7902
7908
|
for (; ; ) {
|
|
7903
7909
|
await this.resumed;
|
|
7910
|
+
if (this.dying) return DYING;
|
|
7904
7911
|
const stops = this.stops;
|
|
7905
7912
|
const result = await this.dispatch(req);
|
|
7913
|
+
if (this.dying) return DYING;
|
|
7906
7914
|
const restart = !result.ok && result.errno === "EINTR" && this.stops !== stops;
|
|
7907
7915
|
if (restart && !this.options.hasPending?.()) continue;
|
|
7908
7916
|
await this.resumed;
|
|
7909
7917
|
return result;
|
|
7910
7918
|
}
|
|
7911
7919
|
}
|
|
7920
|
+
die() {
|
|
7921
|
+
if (this.dying) return;
|
|
7922
|
+
this.dying = true;
|
|
7923
|
+
this.interrupt.abort();
|
|
7924
|
+
this.wake?.();
|
|
7925
|
+
}
|
|
7912
7926
|
async dispatch(req) {
|
|
7913
7927
|
try {
|
|
7914
7928
|
if (isFdSyscall(req)) return await this.fdSyscall(req);
|
|
@@ -8369,6 +8383,7 @@ var WASM_MAX_THREADS = 256;
|
|
|
8369
8383
|
|
|
8370
8384
|
// src/kernel/host.ts
|
|
8371
8385
|
var CRASHED = 70;
|
|
8386
|
+
var KILL_GRACE_MS = 2e3;
|
|
8372
8387
|
var descIds = /* @__PURE__ */ new WeakMap();
|
|
8373
8388
|
var nextDescId = 1;
|
|
8374
8389
|
function descId(file) {
|
|
@@ -8392,6 +8407,41 @@ function inheritedFds(fds) {
|
|
|
8392
8407
|
};
|
|
8393
8408
|
});
|
|
8394
8409
|
}
|
|
8410
|
+
var Dying = class {
|
|
8411
|
+
ending;
|
|
8412
|
+
timer;
|
|
8413
|
+
busy = 0;
|
|
8414
|
+
finish;
|
|
8415
|
+
start(code, sig, header2, process, finish) {
|
|
8416
|
+
if (this.ending) return;
|
|
8417
|
+
this.ending = { code, sig };
|
|
8418
|
+
this.finish = finish;
|
|
8419
|
+
this.arm();
|
|
8420
|
+
Atomics.store(header2, SAB_I_KILLED, code);
|
|
8421
|
+
process.die();
|
|
8422
|
+
}
|
|
8423
|
+
async serving(work) {
|
|
8424
|
+
this.busy++;
|
|
8425
|
+
clearTimeout(this.timer);
|
|
8426
|
+
try {
|
|
8427
|
+
return await work();
|
|
8428
|
+
} finally {
|
|
8429
|
+
this.busy--;
|
|
8430
|
+
this.arm();
|
|
8431
|
+
}
|
|
8432
|
+
}
|
|
8433
|
+
arm() {
|
|
8434
|
+
clearTimeout(this.timer);
|
|
8435
|
+
if (this.busy === 0 && this.finish) this.timer = setTimeout(this.finish, KILL_GRACE_MS);
|
|
8436
|
+
}
|
|
8437
|
+
clear() {
|
|
8438
|
+
clearTimeout(this.timer);
|
|
8439
|
+
}
|
|
8440
|
+
status(reported) {
|
|
8441
|
+
if (this.ending) return [this.ending.code, this.ending.sig];
|
|
8442
|
+
return [typeof reported === "number" ? reported : CRASHED, void 0];
|
|
8443
|
+
}
|
|
8444
|
+
};
|
|
8395
8445
|
function spawnWasmProcess(opts) {
|
|
8396
8446
|
const sab = new SharedArrayBuffer(SAB_HEADER_BYTES + SAB_DEFAULT_WINDOW_BYTES);
|
|
8397
8447
|
const header2 = new Int32Array(sab, 0, SAB_HEADER_I32);
|
|
@@ -8403,6 +8453,7 @@ function spawnWasmProcess(opts) {
|
|
|
8403
8453
|
forker: opts.forker,
|
|
8404
8454
|
fs: opts.fs,
|
|
8405
8455
|
kill: opts.kill,
|
|
8456
|
+
...opts.writesBack ? { writesBack: opts.writesBack } : {},
|
|
8406
8457
|
processes: opts.processes,
|
|
8407
8458
|
openFiles: opts.openFiles,
|
|
8408
8459
|
...opts.nodes ? { nodes: opts.nodes } : {},
|
|
@@ -8433,18 +8484,16 @@ function spawnWasmProcess(opts) {
|
|
|
8433
8484
|
renamed: (from, to) => holds.renamed(from, to)
|
|
8434
8485
|
});
|
|
8435
8486
|
const worker = opts.createWorker();
|
|
8436
|
-
const
|
|
8437
|
-
|
|
8438
|
-
return dispatchSyncFs(req);
|
|
8439
|
-
};
|
|
8487
|
+
const dying = new Dying();
|
|
8488
|
+
const dispatch2 = (req) => dying.serving(() => isWasmSyscall(req) ? process.syscall(req) : dispatchSyncFs(req));
|
|
8440
8489
|
const responder = attachSyncSabResponder(worker, sab, token, { dispatch: dispatch2 });
|
|
8441
|
-
|
|
8442
|
-
const exited = new Promise((resolve) => settle3 = resolve);
|
|
8490
|
+
const { promise: exited, resolve: settle3 } = Promise.withResolvers();
|
|
8443
8491
|
let done4 = false;
|
|
8444
8492
|
let endedBy;
|
|
8445
8493
|
const finish = (code, sig) => {
|
|
8446
8494
|
if (done4) return;
|
|
8447
8495
|
done4 = true;
|
|
8496
|
+
dying.clear();
|
|
8448
8497
|
endedBy = sig ?? process.execTermsig;
|
|
8449
8498
|
worker.removeEventListener("message", onMessage);
|
|
8450
8499
|
worker.removeEventListener("error", onError);
|
|
@@ -8460,10 +8509,10 @@ function spawnWasmProcess(opts) {
|
|
|
8460
8509
|
};
|
|
8461
8510
|
const handle = (data) => {
|
|
8462
8511
|
if (data?.type === WASM_PROCESS_EXIT) {
|
|
8463
|
-
finish(
|
|
8512
|
+
finish(...dying.status(data.code));
|
|
8464
8513
|
} else if (data?.type === WASM_PROCESS_ERROR) {
|
|
8465
|
-
opts.onError?.(String(data.message));
|
|
8466
|
-
finish(CRASHED);
|
|
8514
|
+
if (!dying.ending) opts.onError?.(String(data.message));
|
|
8515
|
+
finish(...dying.status(CRASHED));
|
|
8467
8516
|
} else if (data?.type === WASM_THREAD_SPAWN && data.thread) {
|
|
8468
8517
|
spawnThread(data.thread);
|
|
8469
8518
|
}
|
|
@@ -8533,7 +8582,10 @@ function spawnWasmProcess(opts) {
|
|
|
8533
8582
|
};
|
|
8534
8583
|
worker.postMessage(init, opts.fork ? [opts.fork.memory.buffer] : []);
|
|
8535
8584
|
const signal = (sig) => {
|
|
8536
|
-
if (process.signal(sig)
|
|
8585
|
+
if (process.signal(sig) !== "terminate") return;
|
|
8586
|
+
const code = sig === SIG.KILL ? 137 : 128 + sig;
|
|
8587
|
+
if (opts.program.abi !== "wasi" || done4) finish(code, sig);
|
|
8588
|
+
else dying.start(code, sig, header2, process, () => finish(code, sig));
|
|
8537
8589
|
};
|
|
8538
8590
|
const kill = (code = 137) => finish(code, code > 128 && code < 160 ? code - 128 : void 0);
|
|
8539
8591
|
return {
|
|
@@ -8544,7 +8596,8 @@ function spawnWasmProcess(opts) {
|
|
|
8544
8596
|
termsig: () => endedBy,
|
|
8545
8597
|
onState: (listener) => process.onState(listener),
|
|
8546
8598
|
memory: () => Atomics.load(header2, SAB_I_MEMORY) * 65536,
|
|
8547
|
-
ignoredSignals: (fork) => process.inheritable(fork)
|
|
8599
|
+
ignoredSignals: (fork) => process.inheritable(fork),
|
|
8600
|
+
writesBack: () => opts.program.abi === "wasi" && !done4
|
|
8548
8601
|
};
|
|
8549
8602
|
}
|
|
8550
8603
|
|
|
@@ -10273,6 +10326,7 @@ var Launcher = class {
|
|
|
10273
10326
|
spawner: this.spawner(pid, req.report),
|
|
10274
10327
|
forker: this.forker(pid, req),
|
|
10275
10328
|
kill: (target, sig) => this.kill(target, sig),
|
|
10329
|
+
writesBack: (target) => this.processes.get(target)?.writesBack() === true,
|
|
10276
10330
|
processes: () => ({ boot: this.boot, processes: this.list() }),
|
|
10277
10331
|
openFiles: this.openFiles,
|
|
10278
10332
|
nodes: this.nodes,
|