@ai-ecoverse/slicc-kernel 1.117.3 → 1.117.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 +2 -2
- package/dist/kernel-worker.js +36 -10
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +63 -12
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +36 -10
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +63 -12
- package/dist/process-worker.js.map +2 -2
- package/dist/types/fs/opfs.d.ts +4 -1
- package/dist/types/process/js/js-io.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -356,7 +356,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
356
356
|
- `argv` (with `argv[0]`), `env`, `pid` (the pid `ps` shows, also after an `exec`), `ppid()` (asked each time, so an orphan sees 1) and `cwd`, and `resolve(path)` to make a path absolute against `cwd`;
|
|
357
357
|
- `stdin`, `stdout` and `stderr` as web streams, and `read(fd, max?)` (empty at end of file), `write(fd, bytes | string)` and `close(fd)` on any descriptor it inherited. A write waits until the pipe has room, so a slow reader slows the program down;
|
|
358
358
|
- `fdStatus(fd)` (`{ type, tty, seekable }`, `type` being `file`, `pipe`, `tty`, `socket`, `device` or `directory`) and `isatty(fd)`, to tell `< file` from a pipe;
|
|
359
|
-
- `open(path, { read, write, append, create, exclusive, truncate })`, which returns a file with `read(position, length)`, `write(position, bytes)`, `size()`, `truncate(size)`, `sync()` and `close()`. Reads and writes go through the kernel's paged files at those positions, so a large file is never read whole;
|
|
359
|
+
- `open(path, { read, write, append, create, exclusive, truncate, nonblock, signal })`, which returns a file with `read(position, length)`, `write(position, bytes)`, `size()`, `truncate(size)`, `sync()` and `close()`. Reads and writes go through the kernel's paged files at those positions, so a large file is never read whole; **A FIFO** ([#935](https://github.com/ai-ecoverse/slicc-kernel/issues/935)) opens through the kernel's FIFO open, as fifo(7) says. A read-open waits for a writer and a write-open for a reader, unless the open is `read` and `write` both. `nonblock: true` gives a read-open at once and a write-open with no reader `ENXIO`, and `signal` aborts a waiting open with its reason. Its file reads and writes the pipe: `read(0, length)` and `write(0, bytes)`, any other position is `ESPIPE`, `size()` is `0`, and `truncate` is `EINVAL`. A socket node is `ENXIO`.
|
|
360
360
|
- `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;
|
|
361
361
|
- `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`). `/dev/shm` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Removing a `/dev` entry fails (see [`/dev`](#dev-entries)), and so does a new name directly in `/dev` or `/proc`;
|
|
362
362
|
- `/proc`, in `fs`, `open` and their `sync` forms, the same as a WASI program's (see [`/proc`](#proc)): `stat`, `lstat`, `exists`, `readdir`, `readlink` and `readFile` answer it, and `/proc/self` is the program's own pid. It's read-only, so every change in it is `EROFS`: `writeFile`, `mkdir`, `rm`, `unlink`, `chmod`, a `rename`, `link` or `symlink` into or out of it, and an `open` for writing, creating or truncating. A file opened in it is a snapshot read at the open, with no descriptor (`fd` is `-1`): its `read` and `size` answer from the snapshot until it's closed, and `write` and `truncate` are `EBADF`. Other paths pay only a prefix check;
|
|
@@ -923,7 +923,7 @@ Not covered: hard links (OPFS cannot share contents between names; they work onl
|
|
|
923
923
|
|
|
924
924
|
Reads retry briefly when a concurrent write has invalidated the OPFS file snapshot (`NotReadableError`).
|
|
925
925
|
|
|
926
|
-
A kernel caches the directory handles it has walked and the sidecar entries it has read. Its own changes keep both caches current, and the other kernels on the origin hear of them through a `BroadcastChannel` named after the database (one for the sidecar, one for directories). A cached directory is checked against its path (`resolve()`) at most once a second before a lookup goes through it, so a directory moved, removed or replaced by another writer, such as a page using the OPFS API, is found at its new place or reported missing within a second. It also keeps the file handles it has looked up (up to 4,096, the least recently used dropped first) for a second after each lookup. Before it uses one, it reads the file through it, which is the read a `stat` needs anyway,
|
|
926
|
+
A kernel caches the directory handles it has walked and the sidecar entries it has read. Its own changes keep both caches current, and the other kernels on the origin hear of them through a `BroadcastChannel` named after the database (one for the sidecar, one for directories). A cached directory is checked against its path (`resolve()`) at most once a second before a lookup goes through it, so a directory moved, removed or replaced by another writer, such as a page using the OPFS API, is found at its new place or reported missing within a second. It also keeps the file handles it has looked up (up to 4,096, the least recently used dropped first) for a second after each lookup. Before it uses one, it reads the file through it, which is the read a `stat` needs anyway, and that read serves the handle's uses of the next 2 ms. So a file another writer removed is missing, and a rewritten one shows its new contents, within 2 ms. The kernel's own writes, truncates, rewrites and renames drop the read at once, and a read that started before an own change isn't kept, so the kernel's own changes always show at once. One replaced by a directory is seen as a directory within a second. Its own changes, and the other kernels' notices, drop the handles they touch at once. A lookup of a directory it has cached and checked within the second is answered from the cache, so a directory replaced by another writer is seen as replaced within a second, as above. A name it has just found missing stays missing for 2 ms, which serves the several checks a single create makes of a name that isn't there yet (about ten per file in a pnpm install); its own creates, renames and links in that directory end that at once, so only a file another kernel or a page using the OPFS API makes in those 2 ms is seen up to 2 ms late.
|
|
927
927
|
|
|
928
928
|
The filesystem is our own rather than [ZenFS](https://github.com/zen-fs/core) (which SLICC uses), because OPFS stays the single source of truth: other writers, such as the BIOS installing packages or a page writing files, need no index to stay consistent with, and nothing is preloaded into memory at mount. POSIX metadata lives in the IndexedDB sidecar described above, which has per-entry transactions instead of one JSON file rewritten on every change.
|
|
929
929
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -995,6 +995,7 @@ var READ_ATTEMPTS = 3;
|
|
|
995
995
|
var MAX_LINKS2 = 40;
|
|
996
996
|
var CHECK_MS = 1e3;
|
|
997
997
|
var ABSENT_MS = 2;
|
|
998
|
+
var GLANCE_MS = 2;
|
|
998
999
|
var ABSENT_NAMES = 4096;
|
|
999
1000
|
var FILE_HANDLES = 4096;
|
|
1000
1001
|
var STALE = /* @__PURE__ */ new Set(["NotFoundError", "TypeMismatchError"]);
|
|
@@ -1135,7 +1136,7 @@ var OpfsFs = class {
|
|
|
1135
1136
|
changes = 0;
|
|
1136
1137
|
replacing = Promise.resolve();
|
|
1137
1138
|
replacements = 0;
|
|
1138
|
-
|
|
1139
|
+
glances = /* @__PURE__ */ new WeakMap();
|
|
1139
1140
|
found = /* @__PURE__ */ new WeakMap();
|
|
1140
1141
|
checked = /* @__PURE__ */ new WeakMap();
|
|
1141
1142
|
channel;
|
|
@@ -1389,7 +1390,7 @@ var OpfsFs = class {
|
|
|
1389
1390
|
if (handle.kind !== "file") return handle;
|
|
1390
1391
|
if (looked) {
|
|
1391
1392
|
this.found.set(handle, performance.now());
|
|
1392
|
-
this.
|
|
1393
|
+
this.glances.delete(handle);
|
|
1393
1394
|
}
|
|
1394
1395
|
this.files.delete(path);
|
|
1395
1396
|
if (this.files.size >= FILE_HANDLES)
|
|
@@ -1423,14 +1424,15 @@ var OpfsFs = class {
|
|
|
1423
1424
|
const fresh2 = cached && performance.now() - (this.found.get(cached) ?? -CHECK_MS) < CHECK_MS;
|
|
1424
1425
|
if (cached && !fresh2) this.files.delete(path);
|
|
1425
1426
|
if (cached && fresh2) {
|
|
1427
|
+
if (this.glanced(cached)) return this.remember(path, cached);
|
|
1426
1428
|
try {
|
|
1427
|
-
this.
|
|
1429
|
+
await this.glance(cached);
|
|
1428
1430
|
return this.remember(path, cached);
|
|
1429
1431
|
} catch (err) {
|
|
1430
1432
|
if (!STALE.has(err?.name))
|
|
1431
1433
|
throw translate(err, path, "read");
|
|
1432
1434
|
this.files.delete(path);
|
|
1433
|
-
this.
|
|
1435
|
+
this.glances.delete(cached);
|
|
1434
1436
|
}
|
|
1435
1437
|
}
|
|
1436
1438
|
const kept = this.dirs.get(path);
|
|
@@ -1623,11 +1625,25 @@ var OpfsFs = class {
|
|
|
1623
1625
|
if (this.dirTimes.get(path) === ms) this.dirTimes.delete(path);
|
|
1624
1626
|
}
|
|
1625
1627
|
}
|
|
1628
|
+
glanced(handle) {
|
|
1629
|
+
const seen = this.glances.get(handle);
|
|
1630
|
+
const fresh2 = seen && seen.changes === this.changes && performance.now() - seen.at < GLANCE_MS;
|
|
1631
|
+
return fresh2 ? seen.file : void 0;
|
|
1632
|
+
}
|
|
1633
|
+
async glance(handle) {
|
|
1634
|
+
const at = performance.now();
|
|
1635
|
+
const before = this.changes;
|
|
1636
|
+
const file = await handle.getFile();
|
|
1637
|
+
if (before === this.changes) this.glances.set(handle, { at, file, changes: before });
|
|
1638
|
+
else this.glances.delete(handle);
|
|
1639
|
+
return file;
|
|
1640
|
+
}
|
|
1641
|
+
wrote(handle) {
|
|
1642
|
+
this.changes++;
|
|
1643
|
+
this.glances.delete(handle);
|
|
1644
|
+
}
|
|
1626
1645
|
async snapshot(handle) {
|
|
1627
|
-
|
|
1628
|
-
if (!taken) return handle.getFile();
|
|
1629
|
-
this.snapshots.delete(handle);
|
|
1630
|
-
return taken;
|
|
1646
|
+
return this.glanced(handle) ?? this.glance(handle);
|
|
1631
1647
|
}
|
|
1632
1648
|
async statOf(path, handle, meta, touched) {
|
|
1633
1649
|
const directory = handle.kind === "directory";
|
|
@@ -1762,9 +1778,11 @@ var OpfsFs = class {
|
|
|
1762
1778
|
async pwrite(path, offset, bytes) {
|
|
1763
1779
|
const [handle, , target] = await this.fileFor(path);
|
|
1764
1780
|
await this.counting(target);
|
|
1781
|
+
this.wrote(handle);
|
|
1765
1782
|
try {
|
|
1766
1783
|
await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
|
|
1767
1784
|
} finally {
|
|
1785
|
+
this.wrote(handle);
|
|
1768
1786
|
await this.recounting(handle, target);
|
|
1769
1787
|
}
|
|
1770
1788
|
this.written(target);
|
|
@@ -1775,9 +1793,11 @@ var OpfsFs = class {
|
|
|
1775
1793
|
const resolve2 = this.ranged || this.remote?.watched();
|
|
1776
1794
|
const target = resolve2 || this.counted.length > 0 ? (await this.locate(path, true)).path : path;
|
|
1777
1795
|
await this.counting(target);
|
|
1796
|
+
this.wrote(handle);
|
|
1778
1797
|
try {
|
|
1779
1798
|
await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
|
|
1780
1799
|
} finally {
|
|
1800
|
+
this.wrote(handle);
|
|
1781
1801
|
await this.recounting(handle, target);
|
|
1782
1802
|
}
|
|
1783
1803
|
this.written(target);
|
|
@@ -1797,9 +1817,12 @@ var OpfsFs = class {
|
|
|
1797
1817
|
throw translate(err, path, "write", false);
|
|
1798
1818
|
}),
|
|
1799
1819
|
close: async () => {
|
|
1820
|
+
this.wrote(handle);
|
|
1800
1821
|
await writable2.close().catch((err) => {
|
|
1822
|
+
this.wrote(handle);
|
|
1801
1823
|
throw translate(err, path, "commit", false);
|
|
1802
1824
|
});
|
|
1825
|
+
this.wrote(handle);
|
|
1803
1826
|
this.committed(target);
|
|
1804
1827
|
}
|
|
1805
1828
|
};
|
|
@@ -1846,6 +1869,7 @@ var OpfsFs = class {
|
|
|
1846
1869
|
if (!existed) await this.stampNew(target, owner, false);
|
|
1847
1870
|
if (!existed && content.length === 0) return;
|
|
1848
1871
|
let writable2;
|
|
1872
|
+
this.wrote(file);
|
|
1849
1873
|
try {
|
|
1850
1874
|
writable2 = await file.createWritable();
|
|
1851
1875
|
await writable2.write(content);
|
|
@@ -1853,6 +1877,8 @@ var OpfsFs = class {
|
|
|
1853
1877
|
} catch (err) {
|
|
1854
1878
|
await writable2?.abort().catch(() => void 0);
|
|
1855
1879
|
throw translate(err, path, "commit", false);
|
|
1880
|
+
} finally {
|
|
1881
|
+
this.wrote(file);
|
|
1856
1882
|
}
|
|
1857
1883
|
this.committed(target);
|
|
1858
1884
|
}
|
|
@@ -2106,7 +2132,7 @@ var OpfsFs = class {
|
|
|
2106
2132
|
}
|
|
2107
2133
|
async modified(path) {
|
|
2108
2134
|
const handle = await this.handle(path);
|
|
2109
|
-
return handle.kind === "file" ? (await
|
|
2135
|
+
return handle.kind === "file" ? (await this.snapshot(handle)).lastModified : 0;
|
|
2110
2136
|
}
|
|
2111
2137
|
async explicitTimes(path) {
|
|
2112
2138
|
const timed2 = [];
|
|
@@ -6285,7 +6311,7 @@ function satisfies(version, range2) {
|
|
|
6285
6311
|
}
|
|
6286
6312
|
|
|
6287
6313
|
// src/version.ts
|
|
6288
|
-
var KERNEL_VERSION = true ? "1.117.
|
|
6314
|
+
var KERNEL_VERSION = true ? "1.117.4" : "0.0.0-dev";
|
|
6289
6315
|
var OS_RELEASE = "6.0.0-slicc";
|
|
6290
6316
|
function procVersion() {
|
|
6291
6317
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|