@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 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, so a file another writer removed is missing at once and a rewritten one shows its new contents. 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.
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
 
@@ -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
- snapshots = /* @__PURE__ */ new WeakMap();
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.snapshots.delete(handle);
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.snapshots.set(cached, await cached.getFile());
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.snapshots.delete(cached);
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
- const taken = this.snapshots.get(handle);
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 handle.getFile()).lastModified : 0;
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.3" : "0.0.0-dev";
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