@ai-ecoverse/slicc-kernel 1.143.3 → 1.143.4
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 +5 -4
- package/dist/kernel-worker.js +29 -18
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +70 -41
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +1 -1
- package/dist/types/fs/watch.d.ts +5 -1
- package/dist/types/node.d.ts +7 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -202,11 +202,11 @@ const { status, stdout } = await kernel.run(['bash', '-c', 'echo hi'], { cwd: '/
|
|
|
202
202
|
kernel.terminate();
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
-
`createNodeKernel({ root, modules, commands, env, network, worker, facades, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch, users, onDenial, approve, compileCache, warmWorker })` 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`, `users`, `setRoutes`, `updateTransportTraits` and `terminate` as above, plus `integrity.revoke(path?)`, `root`, `writeFile(path, data, { mode }?)` (creating the parent directories; `mode` sets the permission bits, `0` to `0o7777`, before the data is written), `chmod(path, mode)` and `readFile(path)` to put files in place and read results; like the `fs` of an attached client, they go through the mounts, as the processes see them. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect({ user })` 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.
|
|
205
|
+
`createNodeKernel({ root, modules, commands, env, network, worker, facades, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch, users, onDenial, approve, compileCache, warmWorker, warmWorkers })` 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`, `users`, `setRoutes`, `updateTransportTraits` and `terminate` as above, plus `integrity.revoke(path?)`, `root`, `writeFile(path, data, { mode }?)` (creating the parent directories; `mode` sets the permission bits, `0` to `0o7777`, before the data is written), `chmod(path, mode)` and `readFile(path)` to put files in place and read results; like the `fs` of an attached client, they go through the mounts, as the processes see them. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect({ user })` 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.
|
|
206
206
|
|
|
207
207
|
`compileCache` (opt-in, off by default; #1285, slicc-jsh#566) is an absolute host directory where each process worker keeps the compiled code of the JS programs it imports (`module.enableCompileCache`), so the next spawn of the same program skips V8's compile. It stores V8's code cache, keyed and checksummed by the module's source, one file per program (about 7 MB for a 2.4 MB bundle), shared by every kernel given the same directory; programs can't reach or write it (the worker hands it to Node before the lockdown, and `node:worker_threads` stays refused). Measured on a 2.4 MB bundle, a spawn went from about 54 to 39 ms; a trivial program costs about 1 ms more. **The directory must be private to the embedder:** V8's code cache is trusted bytecode that Node only checksums, so whoever can write it can plant code that runs inside a process worker past the lockdown. Never make it reachable from inside the kernel (no `hostfs` mount of it or of a parent), and don't share one between kernels of different trust. A host's own `NODE_COMPILE_CACHE` is never handed to the process workers, with or without the option, so programs never land in a host-wide cache. If a host preload (`--import` or `--require`) has already enabled another compile cache in the workers, a kernel given `compileCache` refuses to run programs there instead of caching them in the host's directory.
|
|
208
208
|
|
|
209
|
-
`warmWorker` (on by default; `false` turns it off; #1341, slicc-jsh#566) keeps one process worker booted ahead of the next spawn, fork or thread on the Node entry, so the spawn skips the worker's boot: it has loaded the process worker module and nothing else, no pid, credentials, environment, cwd, descriptors, lockdown or program, all of which arrive with its init message, and every worker still serves exactly one process or thread and is then terminated. Its own `process.env` is a snapshot taken when the spare is created rather than at the spawn, but a program never sees it (`process` is withheld, and a program's environment is the one its spawn gives it). The spare never keeps the host alive, is replaced after each spawn off the critical path, is dropped if it errors or exits while idle, and is released after 30 s without a spawn; `terminate()` ends it. It costs about 11 MB of host RSS while it waits.
|
|
209
|
+
`warmWorker` (on by default; `false` turns it off; #1341, slicc-jsh#566) keeps one process worker booted ahead of the next spawn, fork or thread on the Node entry, so the spawn skips the worker's boot: it has loaded the process worker module and nothing else, no pid, credentials, environment, cwd, descriptors, lockdown or program, all of which arrive with its init message, and every worker still serves exactly one process or thread and is then terminated. Its own `process.env` is a snapshot taken when the spare is created rather than at the spawn, but a program never sees it (`process` is withheld, and a program's environment is the one its spawn gives it). The spare never keeps the host alive, is replaced after each spawn off the critical path, is dropped if it errors or exits while idle, and is released after 30 s without a spawn; `terminate()` ends it. It costs about 11 MB of host RSS while it waits. `warmWorkers: n` (opt-in; #1361, slicc-jsh#566) keeps up to `n` spares instead of one (an integer from 0 to 64; `warmWorker: false` still means none), so a burst of starts, as worker pools make, finds a spare for each: all are refilled after each take off the critical path, and all are released together after 30 s without a spawn and ended by `terminate()`. Each costs about 7–11 MB of host RSS while it waits. It pays off for bursts of `Worker` threads (jsh measured about 13 ms less per start in a burst of 8); for bursts of child processes that boot a large runtime of their own, the refills compete for CPU with the children and the gain is small or none.
|
|
210
210
|
|
|
211
211
|
By default `nodeTransport()` refuses destinations on the host itself, because Node can reach them and a page can't: local DevTools on 9222–9229, dev servers, local admin APIs, and the cloud metadata address.
|
|
212
212
|
- **What it refuses:** loopback (`127.0.0.0/8`, `::1`, and their IPv4-mapped forms), unspecified addresses (`0.0.0.0/8`, `::`), `localhost` and `*.localhost`, and link-local addresses (`169.254.0.0/16`, `fe80::/10`), plus AWS's IPv6 metadata address `fd00:ec2::254`.
|
|
@@ -370,9 +370,10 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
370
370
|
- `fs`: `stat`, `lstat`, `exists`, `readdir`, `mkdir` (with parents, or, with `{ recursive: false }`, as Node's `fs.mkdir` without `recursive`: `EEXIST` for an existing entry and `ENOENT` for a missing parent, atomically among the processes of one kernel, so it can serve as a lock there, but not between two kernels on one origin, such as two tabs ([#519](https://github.com/ai-ecoverse/slicc-kernel/issues/519)); `sync.fs.mkdir` takes the same option), `rm` (recursive), `unlink`, `rename`, `symlink`, `readlink`, `statfs`, `chmod(path, mode)`, and `readFile`/`writeFile` for small files. `chmod` takes the permission bits (`0` to `0o7777`, `EINVAL` otherwise) and follows a symlink; as `chmod(2)`, only the file's owner or root may (`EPERM`), so a program can make a script it wrote runnable by path. `sync.fs.chmod` is the same, synchronously. `statfs(path)` answers `{ type, bsize, blocks, bfree, bavail, files, ffree, quota, usage }`: the fields of Node's `StatFs`, plus `quota` and `usage` in bytes (what jsh 0.17 reads). The values come from the same source as an Emscripten program's `statfs`/`df`: the driver of the mount the path is on, or, on the root file system, the origin's storage estimate (`navigator.storage.estimate()`). `bsize` is 4096, `bfree` and `bavail` are what's left of the quota, and `type`, `files` and `ffree` are 0, since the kernel doesn't know them. Where there's no estimate (the Node entry's root), every count is 0. A missing path is `ENOENT`. `sync.fs` has it too. `rm` removes a whole tree in one kernel call (on the OPFS root, one recursive removal and one metadata delete: 50,000 files take about 3 s in Chrome, where `rm -r` takes about 25 s) with the result of a file-by-file removal: symlinks inside the tree are removed, never followed; a tree holding a protected path is refused with `EROFS` and nothing is removed; a mount inside the tree is emptied and its mount point answers `EBUSY`, while the rest is removed; under `users`, what the user may remove is removed and the call then fails with the first `EACCES`/`EPERM`; files a process holds open stay readable to it;
|
|
371
371
|
- **`fs.watch(path, options?, onEvent)`** resolves with `{ close() }` ([#1031](https://github.com/ai-ecoverse/slicc-kernel/issues/1031)). `sync.fs.watch` returns the watcher directly and is in place when it returns, so it sees every change made after the call. This is the shape for Node's synchronous `fs.watch`.
|
|
372
372
|
- **What it reports:** a file reports its own changes. A directory reports its direct entries, or with `{ recursive: true }`, everything below it.
|
|
373
|
-
- **Events:** each one is `{ type, filename }`, in the order the operations happened.
|
|
373
|
+
- **Events:** each one is `{ type, filename, seq }`, in the order the operations happened.
|
|
374
374
|
- `'rename'`: an entry appeared or went away (create, `mkdir`, `unlink`, `rm`, both names of a `rename`, `link`, `symlink`).
|
|
375
|
-
- `'change'`: an existing entry's data or metadata changed (a write, `truncate`, `chmod`, `chown`, `utimes`). A write that creates a file gives `'rename'` and then `'change'`, as on Linux.
|
|
375
|
+
- `'change'`: an existing entry's data or metadata changed (a write, `truncate`, `chmod`, `chown`, `utimes`). A write that creates a file gives `'rename'` and then `'change'`, as on Linux. Writing a whole file over an existing one is a truncate and a write, so it gives two `'change'`s, or one if the new content is empty ([#1357](https://github.com/ai-ecoverse/slicc-kernel/issues/1357)).
|
|
376
|
+
- `seq` numbers the operations across the whole kernel, increasing. One operation heard by several watches (a directory's and a file's in it) has the same `seq` in each, so a program with several watches can merge their events back into one queue in operation order and, as inotify does, merge only events that are adjacent in it. `{ overflow: true }` and `{ ended: true }` carry the `seq` of the operation that caused them.
|
|
376
377
|
- `filename` is the entry's name relative to the watched directory (`sub/a.txt` when recursive), or the file's basename for a file watch.
|
|
377
378
|
- **Overflow:** a watch holds 1,000 events between reads. Beyond that, the rest are dropped, and `{ overflow: true }` follows the ones kept.
|
|
378
379
|
- **The watched path itself:** like inotify, a watch follows the file or directory it was opened on, not its name. When a directory above it is renamed, the watch stays on the moved path with no event, and later events keep the name it was opened with. When the path itself is renamed, the watch gets one `{ type: 'rename', filename, moved: true }` and stays on the new path. Only a removal ends it: `rm` of the path or a directory above it, or a rename of something else over it gives one `'rename'` with its own name, then `{ ended: true }`. Renames are followed as this kernel makes them ([#1356](https://github.com/ai-ecoverse/slicc-kernel/issues/1356)). `close()` is safe at any time and idempotent, and nothing is delivered after it returns.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -7598,7 +7598,7 @@ function satisfies(version, range2) {
|
|
|
7598
7598
|
}
|
|
7599
7599
|
|
|
7600
7600
|
// src/version.ts
|
|
7601
|
-
var KERNEL_VERSION = true ? "1.143.
|
|
7601
|
+
var KERNEL_VERSION = true ? "1.143.4" : "0.0.0-dev";
|
|
7602
7602
|
var OS_RELEASE = "6.0.0-slicc";
|
|
7603
7603
|
function procVersion() {
|
|
7604
7604
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -10500,22 +10500,22 @@ function noted(watch, note) {
|
|
|
10500
10500
|
if (watch.overflowed) return;
|
|
10501
10501
|
if (watch.notes.length >= WATCH_NOTES_LIMIT) {
|
|
10502
10502
|
watch.overflowed = true;
|
|
10503
|
-
watch.notes.push({ overflow: true });
|
|
10503
|
+
watch.notes.push({ overflow: true, seq: note.seq });
|
|
10504
10504
|
} else watch.notes.push(note);
|
|
10505
10505
|
watch.wake?.();
|
|
10506
10506
|
}
|
|
10507
|
-
function route(watch, type, path) {
|
|
10508
|
-
for (const root of watch.roots) if (routed(watch, root, type, path)) return;
|
|
10507
|
+
function route(watch, type, path, seq2) {
|
|
10508
|
+
for (const root of watch.roots) if (routed(watch, root, type, path, seq2)) return;
|
|
10509
10509
|
}
|
|
10510
|
-
function routed(watch, root, type, path) {
|
|
10510
|
+
function routed(watch, root, type, path, seq2) {
|
|
10511
10511
|
if (path === root || type === "rename" && root !== "/" && under3(root, path)) {
|
|
10512
|
-
noted(watch, { type, filename: watch.names.get(root) ?? baseOf3(root) });
|
|
10513
|
-
if (type === "rename") noted(watch, { ended: true });
|
|
10512
|
+
noted(watch, { type, filename: watch.names.get(root) ?? baseOf3(root), seq: seq2 });
|
|
10513
|
+
if (type === "rename") noted(watch, { ended: true, seq: seq2 });
|
|
10514
10514
|
return true;
|
|
10515
10515
|
}
|
|
10516
|
-
if (parentOf7(path) === root) noted(watch, { type, filename: baseOf3(path) });
|
|
10516
|
+
if (parentOf7(path) === root) noted(watch, { type, filename: baseOf3(path), seq: seq2 });
|
|
10517
10517
|
else if (watch.recursive && under3(path, root)) {
|
|
10518
|
-
noted(watch, { type, filename: path.slice(root === "/" ? 1 : root.length + 1) });
|
|
10518
|
+
noted(watch, { type, filename: path.slice(root === "/" ? 1 : root.length + 1), seq: seq2 });
|
|
10519
10519
|
} else return false;
|
|
10520
10520
|
return true;
|
|
10521
10521
|
}
|
|
@@ -10529,6 +10529,7 @@ var FsWatchers = class {
|
|
|
10529
10529
|
typed = /* @__PURE__ */ new Set();
|
|
10530
10530
|
taps = [];
|
|
10531
10531
|
reals = /* @__PURE__ */ new Map();
|
|
10532
|
+
seq = 0;
|
|
10532
10533
|
reshapes = [];
|
|
10533
10534
|
onReshape(listener) {
|
|
10534
10535
|
this.reshapes.push(listener);
|
|
@@ -10588,7 +10589,7 @@ var FsWatchers = class {
|
|
|
10588
10589
|
const notes = watch.notes.splice(0);
|
|
10589
10590
|
watch.overflowed = false;
|
|
10590
10591
|
if (watch.ended) this.typed.delete(watch);
|
|
10591
|
-
return notes.length === 0 && watch.ended ? [{ ended: true }] : notes;
|
|
10592
|
+
return notes.length === 0 && watch.ended ? [{ ended: true, seq: this.seq }] : notes;
|
|
10592
10593
|
}
|
|
10593
10594
|
};
|
|
10594
10595
|
}
|
|
@@ -10605,10 +10606,11 @@ var FsWatchers = class {
|
|
|
10605
10606
|
if (this.typed.size === 0) return;
|
|
10606
10607
|
for (const path of paths) {
|
|
10607
10608
|
const at = normalizePath(path);
|
|
10608
|
-
|
|
10609
|
+
const seq2 = ++this.seq;
|
|
10610
|
+
for (const watch of [...this.typed]) route(watch, type, at, seq2);
|
|
10609
10611
|
}
|
|
10610
10612
|
}
|
|
10611
|
-
moved(from, to) {
|
|
10613
|
+
moved(from, to, seq2 = ++this.seq) {
|
|
10612
10614
|
const source = normalizePath(from);
|
|
10613
10615
|
const target = normalizePath(to);
|
|
10614
10616
|
const followed2 = /* @__PURE__ */ new Set();
|
|
@@ -10624,17 +10626,25 @@ var FsWatchers = class {
|
|
|
10624
10626
|
watch.names = names2;
|
|
10625
10627
|
watch.roots = [...new Set(roots)];
|
|
10626
10628
|
if (main === source) {
|
|
10627
|
-
noted(watch, {
|
|
10629
|
+
noted(watch, {
|
|
10630
|
+
type: "rename",
|
|
10631
|
+
filename: names2.get(target),
|
|
10632
|
+
moved: true,
|
|
10633
|
+
seq: seq2
|
|
10634
|
+
});
|
|
10628
10635
|
}
|
|
10629
10636
|
}
|
|
10630
10637
|
return followed2;
|
|
10631
10638
|
}
|
|
10632
10639
|
renamed(from, to) {
|
|
10633
|
-
const
|
|
10640
|
+
const first = ++this.seq;
|
|
10641
|
+
const followed2 = this.moved(from, to, first);
|
|
10634
10642
|
if (this.typed.size === 0) return;
|
|
10635
|
-
for (const path of [from, to]) {
|
|
10643
|
+
for (const [i, path] of [from, to].entries()) {
|
|
10636
10644
|
const at = normalizePath(path);
|
|
10637
|
-
|
|
10645
|
+
const seq2 = i === 0 ? first : ++this.seq;
|
|
10646
|
+
for (const watch of [...this.typed])
|
|
10647
|
+
if (!followed2.has(watch)) route(watch, "rename", at, seq2);
|
|
10638
10648
|
}
|
|
10639
10649
|
}
|
|
10640
10650
|
changed(...paths) {
|
|
@@ -10699,10 +10709,11 @@ var FsWatchers = class {
|
|
|
10699
10709
|
const real = await realOf(at);
|
|
10700
10710
|
const paths = real === at ? [at] : [at, real];
|
|
10701
10711
|
this.report(paths);
|
|
10702
|
-
if (done5) for (const type of types(fresh2)) this.emit(type, paths);
|
|
10712
|
+
if (done5) for (const type of types(fresh2, args)) this.emit(type, paths);
|
|
10703
10713
|
}
|
|
10704
10714
|
};
|
|
10705
10715
|
const written2 = (fresh2) => fresh2 ? ["rename", "change"] : ["change"];
|
|
10716
|
+
const overwritten = (fresh2, args) => fresh2 || args[1].length === 0 ? written2(fresh2) : ["change", "change"];
|
|
10706
10717
|
const reshaping = (run2) => async (...args) => {
|
|
10707
10718
|
this.reshaped();
|
|
10708
10719
|
try {
|
|
@@ -10712,7 +10723,7 @@ var FsWatchers = class {
|
|
|
10712
10723
|
}
|
|
10713
10724
|
};
|
|
10714
10725
|
const overrides = {
|
|
10715
|
-
writeFile: content(fs.writeFile.bind(fs),
|
|
10726
|
+
writeFile: content(fs.writeFile.bind(fs), overwritten),
|
|
10716
10727
|
...fs.createExclusive ? { createExclusive: content(fs.createExclusive.bind(fs), () => ["rename"]) } : {},
|
|
10717
10728
|
...fs.copyFile ? {
|
|
10718
10729
|
copyFile: (from, to, options) => content(
|