@ai-ecoverse/slicc-kernel 1.98.2 → 1.98.3

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
@@ -536,7 +536,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
536
536
  - **Caching:** the kernel caches attributes and listings for those TTLs, and a driver can push `invalidate` for changes made outside.
537
537
  - **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. An `fsa` mount (a picked folder) has no sync access handles, and Chrome copies the whole file into a swap file for every writable stream, so it writes through one stream per open file: the first write back opens it, later runs go into it at their offsets, and it is committed, one copy of the file, when the last process writing the file closes it (or exits), before another process reads what was written, and before a rename, unlink or unmount. Until then other processes see the new size but read the old bytes outside what was written. Elsewhere (`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. Before a program's next call into the kernel (any wait, read of a pipe or terminal, sleep or spawn), its changed pages also go back, so other processes and the embedder see a write as soon as the writer does anything but compute: a daemon's pidfile, a log another process follows. On a whole-file mount that holds only for files of up to 64 KiB; a larger one goes back at close, fsync and exit, as before. Beyond that, 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.
538
538
  - **One file, several processes** ([#665](https://github.com/ai-ecoverse/slicc-kernel/issues/665)): a process that keeps a file open sees what another process writes to it. Every change the kernel applies to a file (a write back, a truncate, an append, a rename or a removal) bumps a change ring the kernel shares with all its processes. Before each read, write or seek to the end, a process's open file checks the ring (one atomic load) and, if its file changed, drops its clean pages and reads the size again, keeping its own unwritten changes. A write by one process is visible to another one's next read once it has gone back to the kernel: at the latest at the writer's next `fsync`, close, exit or call into the kernel (see above), so sqlite, which `fsync`s every commit, sees another connection's commit at its next transaction. A write through a file the kernel holds (a JS program's `ctx.open`, a WASI program's file after a fork or spawn) is in the kernel already, so other processes see it at their next read, before it is written back. A file the kernel itself holds open (a JS program's file, a WASI program's file after a fork or spawn) is the one copy: reads, writes, truncates and appends by path from other processes go to it, and their writes go through to storage at once, so a commit is in storage (and in `kernel.readFile`) when the committing process returns from `fsync`. `O_APPEND` writes go to the kernel at once, and the kernel picks the offset, so appenders in several processes keep every line. A file system without ranged I/O (`hostfs` with an older proxy, a root outside OPFS) rereads a file another process changed, but writes it back whole, so two writers there still need locks. The same file under two names is one file too ([#669](https://github.com/ai-ecoverse/slicc-kernel/issues/669)): a write by path through a symlink reaches the file the kernel holds under its real path (resolved without following links in a `sudo` process's no-links view, so an untrusted link is still refused), a WASI program's open file also watches its real path (resolved once, the first time another file changes), and an open file with more than one hard link checks its size, version and mtime when anything changed, so a write through another name is seen. Limits for now: an alias on a mount that has no `realpath` is still only the name it was opened by; another tab's writes reach this tab's processes only as far as the root's change notifications (OPFS's `BroadcastChannel` watch) carry them.
539
- - **Locks between processes** ([#665](https://github.com/ai-ecoverse/slicc-kernel/issues/665)): an Emscripten program's `fcntl` byte-range locks (`F_SETLK`, `F_SETLKW`, `F_GETLK`) are the kernel's, so sqlite3 connections in different processes exclude each other: a second writer gets `database is locked` while the first holds its transaction, or waits with a busy timeout. The kernel keeps one table keyed by file identity (device and inode, so every name of a hard link is the same file), with POSIX semantics: locks belong to the process (an `exec`'d program keeps them under its pid; a forked child doesn't get them), a conflicting `F_SETLK` fails with `EAGAIN`, `F_GETLK` reports the holder's pid and range, `F_SETLKW` waits in the kernel without holding up anything else and a signal ends the wait with `EINTR`, a wait that would close a cycle between waiting processes fails with `EDEADLK`, a write lock on a descriptor not open for writing (or a read lock on one not open for reading) is `EBADF`, closing any descriptor of the file drops the process's locks on it, and exit releases them all. Before an unlock, the process writes back what it changed, so the next holder reads it. An open file's directory entry is checked against the change ring too: a name another process removed or replaced is looked up again, so sqlite's check for a hot journal sees the other connection's commit. A WASI imports module's `tryLock`/`unlock` keeps its behaviour (whole file, per process, keyed by path). Limits: locks are per kernel, not across tabs ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)); and a WASIX program's `fcntl` locks (python's `sqlite3`) are no-ops in its libc that never reach the kernel, until wasix-libc gets a kernel import for them ([homescoop#407](https://github.com/ai-ecoverse/homescoop/issues/407)), so python's sqlite doesn't exclude other processes yet.
539
+ - **Locks between processes** ([#665](https://github.com/ai-ecoverse/slicc-kernel/issues/665)): an Emscripten program's `fcntl` byte-range locks (`F_SETLK`, `F_SETLKW`, `F_GETLK`) are the kernel's, so sqlite3 connections in different processes exclude each other: a second writer gets `database is locked` while the first holds its transaction, or waits with a busy timeout. The kernel keeps one table keyed by file identity (device and inode, so every name of a hard link is the same file), with POSIX semantics: locks belong to the process (an `exec`'d program keeps them under its pid; a forked child doesn't get them), a conflicting `F_SETLK` fails with `EAGAIN`, `F_GETLK` reports the holder's pid and range, `F_SETLKW` waits in the kernel without holding up anything else and a signal ends the wait with `EINTR`, a wait that would close a cycle between waiting processes fails with `EDEADLK`, a write lock on a descriptor not open for writing (or a read lock on one not open for reading) is `EBADF`, closing any descriptor of the file drops the process's locks on it, and exit releases them all. Before an unlock, the process writes back what it changed, so the next holder reads it. An open file's directory entry is checked against the change ring too: a name another process removed or replaced is looked up again, so sqlite's check for a hot journal sees the other connection's commit. A WASI imports module's `tryLock`/`unlock` keeps its behaviour (whole file, per process, keyed by path). Limits: locks are per kernel, not across tabs ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)); and a WASIX program's `fcntl` locks (python's `sqlite3`) are no-ops in its libc that never reach the kernel, until wasix-libc gets a kernel import for them ([homescoop#407](https://github.com/ai-ecoverse/homescoop/issues/407)), so python's sqlite doesn't exclude other processes yet. An Emscripten program's `flock()` is a stub compiled into its libc that never reaches the kernel either, until a homescoop shim routes it to a kernel import ([#680](https://github.com/ai-ecoverse/slicc-kernel/issues/680)). A lock belongs to the file its descriptor was opened on: a file renamed over the name later, or the name's removal, doesn't move it.
540
540
  - **Rules on a mount:**
541
541
  - a rename across mounts is `EXDEV`, so `mv` copies;
542
542
  - a symlink on the root file system that leads onto a mount, such as `~/proj` → `/mnt/drive/proj`, or a chain from a mount to the root and back, is followed onto the mount by every operation, in every runtime: reads, writes, `stat`, listings, `mkdir`, removals, renames, `chmod`, `chown` and times. Removing the link itself removes the link. Before [#547](https://github.com/ai-ecoverse/slicc-kernel/issues/547) was fixed, the kernel resolved such a link against the directory under the mount point for JS and WASI programs, the embedder's `fs` and attached clients (Emscripten programs weren't affected): reads failed with `ENOENT`, and writes went into that hidden directory, which shows again once the mount is gone;
@@ -12082,7 +12082,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
12082
12082
  var EARLY_WHOLE_FILE = 64 * 1024;
12083
12083
 
12084
12084
  // src/version.ts
12085
- var KERNEL_VERSION = true ? "1.98.2" : "0.0.0-dev";
12085
+ var KERNEL_VERSION = true ? "1.98.3" : "0.0.0-dev";
12086
12086
  var OS_RELEASE = "6.0.0-slicc";
12087
12087
  function procVersion() {
12088
12088
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -15521,6 +15521,11 @@ function clipboardLine(text) {
15521
15521
  }
15522
15522
  return out.length > 200 ? `${out.slice(0, 200)}\u2026` : out;
15523
15523
  }
15524
+ var nextOpenKey = 1;
15525
+ var fileKeys = /* @__PURE__ */ new WeakMap();
15526
+ function identityKey(st, path) {
15527
+ return st.ino === void 0 ? path : `${st.dev ?? 0}:${st.ino}`;
15528
+ }
15524
15529
  var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
15525
15530
  var HOSTS_ONLY = new Resolver({ routes: new Routes() });
15526
15531
  var WasmProcess = class {
@@ -15766,10 +15771,11 @@ var WasmProcess = class {
15766
15771
  this.lockOwners.add(owner);
15767
15772
  return owner;
15768
15773
  }
15774
+ pathKeys = /* @__PURE__ */ new Map();
15769
15775
  async lockPath(target, want) {
15770
15776
  if (target.fd !== void 0) {
15771
15777
  const st = await this.fds.get(target.fd).file.stat?.();
15772
- if (!st || st.orphan) throw new KernelError("EBADF");
15778
+ if (!st) throw new KernelError("EBADF");
15773
15779
  return st.path;
15774
15780
  }
15775
15781
  if (target.path === void 0) throw new KernelError("EINVAL");
@@ -15784,11 +15790,22 @@ var WasmProcess = class {
15784
15790
  return path;
15785
15791
  }
15786
15792
  async lockKey(target, kind) {
15793
+ const file = target.fd === void 0 ? void 0 : this.fds.get(target.fd);
15794
+ const known = file ? fileKeys.get(file) : this.pathKeys.get(target.path ?? "");
15795
+ if (kind === "close" && !file && target.path !== void 0) this.pathKeys.delete(target.path);
15787
15796
  const want = kind === "read" ? R_OK : kind === "write" ? W_OK : 0;
15788
15797
  const path = await this.lockPath(target, want);
15789
- const st = await this.options.fs.stat?.(path);
15790
- if (!st) throw new KernelError("ENOENT");
15791
- return st.ino === void 0 ? path : `${st.dev ?? 0}:${st.ino}`;
15798
+ if (known !== void 0) return known;
15799
+ const st = await this.options.fs.stat?.(path).catch((err) => {
15800
+ if (!file) throw err;
15801
+ return void 0;
15802
+ });
15803
+ if (!st && !file) throw new KernelError("ENOENT");
15804
+ const key = st ? identityKey(st, path) : `open:${nextOpenKey++}`;
15805
+ if (kind === "close") return key;
15806
+ if (file) fileKeys.set(file, key);
15807
+ else if (this.pathKeys.size < 4096) this.pathKeys.set(path, key);
15808
+ return key;
15792
15809
  }
15793
15810
  async dropRecordLocks(fd) {
15794
15811
  const { locks } = this.options;