@ai-ecoverse/slicc-kernel 1.130.0 → 1.130.1

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
@@ -740,7 +740,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
740
740
  - **The same file:** a lock is keyed by the file's device and inode, which both kernels read from the shared sidecar, so a rename by either tab keeps it. A file removed and created again at the same path has the same key, as it already has within one kernel.
741
741
  - **Versions:** the channel and Web Lock names carry a protocol version (`v1`), and a kernel ignores messages that aren't v1. So kernels on different protocol versions (two tabs across an update) don't share locks, as before #675.
742
742
  - **Not across tabs:** the imports module's whole-file `tryLock` (per kernel), and locks on other mounts.
743
- - **Another tab's writes** ([#959](https://github.com/ai-ecoverse/slicc-kernel/issues/959)): a program's names and stats are cached in its process, and its kernel refreshes them when the kernel itself changes a file. While another kernel shares the root (a second tab), the kernel marks that view foreign, and a program then looks names up and reads stats again on every look. So a program that keeps checking a file sees what the other tab wrote, synced and closed. A kernel alone caches as before. Stats are always read fresh while the root is shared. An Emscripten program keeps the names it has looked up, directories and files ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)), until its next call that isn't by path: a read, a write, a poll, a sleep, a wait, a spawn, a look at the clock or a draw of random bytes. Within one run of path-only calls (lstat, chmod, readlink and the like) with none of those in between, an answer the program takes from its own node, such as the node's type, can lag behind the other tab until that next call, as a path call racing a rename does on Linux. A program can only learn that the other tab acted through one of those calls, a new process or a stat, and all of them refresh what it sees. While another kernel shares the root, a kernel also tells it of each file it writes ([#962](https://github.com/ai-ecoverse/slicc-kernel/issues/962)). The other kernel then drops the clean contents of that file, both its own and its programs', so a descriptor tab B already has open reads tab A's new bytes on its next read, as Linux's shared page cache does. The notice is a message, so a read that races it by less than a message's delivery can still see the old bytes, as with any reader that doesn't lock. The whole cross-tab contract is under "What another tab sees, and when".
743
+ - **Another tab's writes** ([#959](https://github.com/ai-ecoverse/slicc-kernel/issues/959)): a program's names and stats are cached in its process, and its kernel refreshes them when the kernel itself changes a file. While another kernel shares the root (a second tab), the kernel marks that view foreign. A WASI program then keeps a stat or a missing name it looked up by path for at most 100 ms after the kernel answered it, however often it looks in between ([#1017](https://github.com/ai-ecoverse/slicc-kernel/issues/1017)). Every notice from another kernel, a lock grant, and a kernel joining or leaving the root (also one that ends without a word) drop everything its programs cached. An answer that a notice overtook on its way is not kept. An open never uses what the program cached (close-to-open), and calls on a descriptor (`fstat`, `pread`) go to the kernel, which holds them to its own 2 ms. So a program that keeps checking a file sees what the other tab wrote, synced and closed, once the notice has arrived, and within 100 ms if the notice is late or lost; one that opens the file sees it at once. A kernel alone caches as before. An Emscripten program keeps the names it has looked up, directories and files ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)), until its next call that isn't by path: a read, a write, a poll, a sleep, a wait, a spawn, a look at the clock or a draw of random bytes. Within one run of path-only calls (lstat, chmod, readlink and the like) with none of those in between, an answer the program takes from its own node, such as the node's type, can lag behind the other tab until that next call, as a path call racing a rename does on Linux. A program can only learn that the other tab acted through one of those calls, a new process or a stat, and all of them refresh what it sees. While another kernel shares the root, a kernel also tells it of each file it writes ([#962](https://github.com/ai-ecoverse/slicc-kernel/issues/962)). The other kernel then drops the clean contents of that file, both its own and its programs', so a descriptor tab B already has open reads tab A's new bytes on its next read, as Linux's shared page cache does. The notice is a message, so a read that races it by less than a message's delivery can still see the old bytes, as with any reader that doesn't lock. The whole cross-tab contract is under "What another tab sees, and when".
744
744
  - **`slicc.fcntl_lock(fd, cmd, flock_ptr) -> errno`** ([#722](https://github.com/ai-ecoverse/slicc-kernel/issues/722)): a WASIX program's `fcntl` record locks reach the same table as an Emscripten program's, so the two see each other's locks.
745
745
  - **cmd:** `12` `F_GETLK`, `13` `F_SETLK` or `14` `F_SETLKW`. Any other cmd is `EINVAL`.
746
746
  - **The struct:** musl's wasm32 `struct flock`, 32 bytes: `l_type` i16 at 0 (`0` read, `1` write, `2` unlock), `l_whence` i16 at 2 (`0` SET, `1` CUR, `2` END, resolved against the descriptor's offset or size), `l_start` i64 at 8, `l_len` i64 at 16 (`0` to EOF and beyond, negative before `l_start`), `l_pid` i32 at 24. The kernel reads and writes only those five fields and never the padding.
@@ -1097,7 +1097,7 @@ A kernel caches the directory handles it has walked and the sidecar entries it h
1097
1097
  - an exclusive create (`O_EXCL`, `mkdir`, `symlink`, `link`) and a rename's check of its target decide against OPFS itself, never against a cached missing name ([#942](https://github.com/ai-ecoverse/slicc-kernel/issues/942));
1098
1098
  - the join handshake writes back every open file before a new kernel counts as a peer, and a change in whether the root is shared drops both 2 ms windows.
1099
1099
 
1100
- So a locked read-modify-write across tabs (sqlite, a JSON counter under `flock`) is exact, and anything else between tabs is as fresh as the last notice. Programs add their own rule on top: see "Another tab's writes" above. `crowded-lookups.test.mjs` and `notice-caches.test.mjs` hold the rows. The `notice-caches` rows freeze one kernel's clock and drop the other's notices, through the kernel workers in `test/integration/page/notice/`.
1100
+ So a locked read-modify-write across tabs (sqlite, a JSON counter under `flock`) is exact, and anything else between tabs is as fresh as the last notice. The same holds for what a WASI program caches in its own process: a notice, a lock grant or a peer joining or leaving drops all of it, and a late or lost notice leaves an unlocked path stat at most 100 ms behind the kernel's answer; opens, descriptor calls and anything under a lock are exact ([#1017](https://github.com/ai-ecoverse/slicc-kernel/issues/1017)). Emscripten programs add their own rule on top: see "Another tab's writes" above. `program-cache-tabs.test.mjs` holds the rows for programs; it freezes a program's clock through `page/notice/program-clock-worker.js`. `crowded-lookups.test.mjs` and `notice-caches.test.mjs` hold the rows. The `notice-caches` rows freeze one kernel's clock and drop the other's notices, through the kernel workers in `test/integration/page/notice/`.
1101
1101
 
1102
1102
  **Within one kernel** ([#1026](https://github.com/ai-ecoverse/slicc-kernel/issues/1026)): a WASI process keeps the stats it has read, and checks each one against the kernel's change ring at every look. So a process that keeps stat-ing a file, even in a loop that never sleeps, sees another process's change to that path at its next stat. Before #1026's fix, from v1.4.0 on, a stat it had looked up lasted until the process slept, polled, waited for a child or wrote the path itself. The ring names the changed paths only, so in a loop with none of those calls, a stat stays stale until the next sleep, poll, wait or own write when the change was named elsewhere:
1103
1103
  - a write through another hard link of the same file. Only tmpfs mounts support `link`; on the OPFS root and every other mount, a file's link count is always 1;
@@ -890,10 +890,12 @@ var RemoteWatch = class {
890
890
  quiet;
891
891
  crowd;
892
892
  told;
893
+ counted = 0;
893
894
  recount() {
894
895
  const now = this.crowded();
895
- if (now === this.told) return;
896
+ if (now === this.told && this.peers.size === this.counted) return;
896
897
  this.told = now;
898
+ this.counted = this.peers.size;
897
899
  this.crowd?.(now);
898
900
  }
899
901
  constructor(name2, options = {}) {
@@ -7020,7 +7022,7 @@ function satisfies(version, range2) {
7020
7022
  }
7021
7023
 
7022
7024
  // src/version.ts
7023
- var KERNEL_VERSION = true ? "1.130.0" : "0.0.0-dev";
7025
+ var KERNEL_VERSION = true ? "1.130.1" : "0.0.0-dev";
7024
7026
  var OS_RELEASE = "6.0.0-slicc";
7025
7027
  function procVersion() {
7026
7028
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -9414,6 +9416,9 @@ var FileChanges = class {
9414
9416
  foreign(on) {
9415
9417
  Atomics.store(this.view, FOREIGN, on ? 1 : 0);
9416
9418
  }
9419
+ flush() {
9420
+ Atomics.add(this.view, GEN, RING_SIZE + 1);
9421
+ }
9417
9422
  publish(...paths) {
9418
9423
  for (const path of paths) {
9419
9424
  const g = Atomics.add(this.view, GEN, 1) + 1 | 0;
@@ -26569,9 +26574,7 @@ var Launcher = class {
26569
26574
  this.usersMode = usersMode(options.users);
26570
26575
  this.memories = memoriesOf(options.wasmMemories);
26571
26576
  this.rootFs = options.fs;
26572
- const peers = options.fs.peers?.();
26573
- this.locks = kernelLocks(options.fs.rootName, peers);
26574
- peers?.watch((crowded) => this.changes.foreign(crowded));
26577
+ this.locks = kernelLocks(options.fs.rootName, this.flushingPeers(options.fs));
26575
26578
  this.dac = this.usersMode === "off" ? void 0 : new Dac(this.usersMode, options.onDenial);
26576
26579
  if (this.dac) this.dac.tagOf = (pid) => this.tagOf(pid);
26577
26580
  this.clipboard = new ClipboardBroker(
@@ -26657,7 +26660,10 @@ var Launcher = class {
26657
26660
  this.nodes.keepsOnDisk = true;
26658
26661
  this.openFiles.add(this.nodes);
26659
26662
  options.fs.onPeer?.(() => this.flushOpen());
26660
- options.fs.remoteChanges?.((change) => this.rewritten(change.paths));
26663
+ options.fs.remoteChanges?.((change) => {
26664
+ this.changes.flush();
26665
+ this.rewritten(change.paths);
26666
+ });
26661
26667
  const holders = [this.held, this.openFiles];
26662
26668
  this.unlinked = new UnlinkedKeeper(
26663
26669
  fs,
@@ -28427,6 +28433,21 @@ var Launcher = class {
28427
28433
  this.writers ??= this.writerStack();
28428
28434
  return this.writers;
28429
28435
  }
28436
+ flushingPeers(fs) {
28437
+ const peers = fs.peers?.();
28438
+ if (!peers) return void 0;
28439
+ peers.watch((crowded) => {
28440
+ this.changes.flush();
28441
+ this.changes.foreign(crowded);
28442
+ });
28443
+ return {
28444
+ ...peers,
28445
+ fresh: () => {
28446
+ this.changes.flush();
28447
+ peers.fresh?.();
28448
+ }
28449
+ };
28450
+ }
28430
28451
  rewritten(paths) {
28431
28452
  for (const nodes of this.openFiles) for (const path of paths) nodes.rewritten(path);
28432
28453
  this.changes.publish(...paths);