@ai-ecoverse/slicc-kernel 1.133.3 → 1.133.5
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 +26 -2
- package/dist/kernel-worker.js +208 -17
- package/dist/kernel-worker.js.map +4 -4
- package/dist/meta.js.map +2 -2
- package/dist/node-process-worker.js +238 -131
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +208 -17
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +226 -119
- package/dist/process-worker.js.map +4 -4
- package/dist/types/fs/changes.d.ts +1 -0
- package/dist/types/fs/meta.d.ts +7 -0
- package/dist/types/fs/opfs.d.ts +3 -0
- package/dist/types/fs/remote.d.ts +1 -0
- package/dist/types/kernel/held-cwd.d.ts +65 -0
- package/dist/types/kernel/host.d.ts +9 -0
- package/dist/types/kernel/process.d.ts +8 -0
- package/dist/types/kernel/protocol.d.ts +7 -0
- package/dist/types/launcher.d.ts +5 -0
- package/dist/types/process/wasi/wasi-fds.d.ts +7 -0
- package/dist/types/process/wasi/wasi-files.d.ts +2 -1
- package/dist/types/process/wasi/wasi-host.d.ts +2 -0
- package/dist/types/realm/live-vfs-fs.d.ts +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -709,7 +709,7 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
|
|
|
709
709
|
|
|
710
710
|
`/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. Files directly in `/`, and directories created there after a process started, are OPFS as well, the same for Emscripten and WASI programs; only `/dev` and `/proc` live in each process's memory. A program's `mkdir` (WASI `path_create_directory`, Emscripten's `mkdir`) is exclusive, as on Linux: it fails with `EEXIST` when the name exists, and of several processes or threads making one name at once exactly one succeeds, so a directory works as a lock. Under a missing directory it fails with `ENOENT`, as on Linux (`mkdir -p` and `os.makedirs` make the parents in the program); a JS program's `ctx.fs.mkdir` makes parents itself. The kernel makes the system `PNPM_HOME` (`/usr/local/share/pnpm`, or the embedder's) when it starts, since pnpm makes only its `bin` directory there. A full origin storage (`QuotaExceededError`, which also comes early in a browser's private or off-the-record contexts, whatever `navigator.storage.estimate()` says) is `ENOSPC`, never retried. A read that meets `NotFoundError` while a writer outside the kernel swaps the file in with `createWritable` (and a `stat`, `lstat` or `utimes` that meets it) looks the name up again, up to three times, before it fails ([#1075](https://github.com/ai-ecoverse/slicc-kernel/issues/1075)), so a program whose package is overwritten in place keeps starting. An OPFS or IndexedDB failure that has no POSIX counterpart is `EIO`; its message names the DOM error and the operation (`lookup`, `read`, `write`, `commit` or `sidecar`), and the kernel worker logs the first of each kind to its console. File contents are buffered per open file and written back on close, `fsync` and exit, so a refused write (`ENOSPC`, `EIO`) surfaces at that close or `fsync`, and a program that ignores close errors doesn't see it (`echo x > f` can exit 0 with `f` empty, as with any buffered write-back). A write-back error a close has already reported to a WASI program isn't raised again when it exits, so the program keeps its own exit status. Metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
|
|
711
711
|
|
|
712
|
-
**Renaming a directory on the OPFS root** ([#1122](https://github.com/ai-ecoverse/slicc-kernel/issues/1122)): Chrome's OPFS can move a file but not a directory. So the kernel renames a directory by creating the new tree and moving each file into it (copying a file only if it can't move, e.g. while it's open elsewhere), then removing the old tree. Within the kernel the rename is atomic to readers: a lookup or listing under the old or the new name, or of either parent, waits until the whole tree has moved. So a stage-then-rename updater (pnpm's, an atomic config write) is never seen half there. Another tab sharing the root reads OPFS directly and can see the tree mid-move. A root whose directories can move (the Node memory root) renames them in one step. **If the page reloads or crashes mid-rename**, some files are already in the new tree and the rest are still in the old one. So before the first move, the kernel writes the rename's intent (`{ from, to }`) to its metadata store, and clears it after the last. At the next boot, when no other tab shares the root (the same rule as for the boot's other repair of stale metadata), the kernel finishes every recorded rename: it moves what's left into the new tree, removes the old name, and clears the record. While another tab shares the root,
|
|
712
|
+
**Renaming a directory on the OPFS root** ([#1122](https://github.com/ai-ecoverse/slicc-kernel/issues/1122)): Chrome's OPFS can move a file but not a directory. So the kernel renames a directory by creating the new tree and moving each file into it (copying a file only if it can't move, e.g. while it's open elsewhere), then removing the old tree. Within the kernel the rename is atomic to readers: a lookup or listing under the old or the new name, or of either parent, waits until the whole tree has moved. So a stage-then-rename updater (pnpm's, an atomic config write) is never seen half there. Another tab sharing the root reads OPFS directly and can see the tree mid-move. A root whose directories can move (the Node memory root) renames them in one step. **If the page reloads or crashes mid-rename**, some files are already in the new tree and the rest are still in the old one. So before the first move, the kernel writes the rename's intent (`{ from, to }`) to its metadata store, and clears it after the last. At the next boot, when no other tab shares the root (the same rule as for the boot's other repair of stale metadata), the kernel finishes every recorded rename: it moves what's left into the new tree, removes the old name, and clears the record. The moved directory and its files keep their owner, mode and inode, as after a rename that wasn't stopped. While another tab shares the root, a kernel finishes only renames whose kernel is gone ([#1141](https://github.com/ai-ecoverse/slicc-kernel/issues/1141)): the renaming kernel holds a Web Lock (`slicc-rename:<id>`, named in the record) for the whole rename, and the browser releases it when that tab closes or its worker ends. A kernel that boots beside a peer, and every kernel that sees a peer go, finishes each recorded rename whose lock is free, taking that lock while it does, so two survivors never replay the same rename and a live kernel's rename is never touched. A record without a lock (written by an older kernel, or where Web Locks are missing) waits for a boot that's alone, as before. While another tab finishes a rename, this kernel's readers wait as for its own; the other tabs can see the tree mid-move, as during any rename.
|
|
713
713
|
|
|
714
714
|
A lookup follows at most 40 symlinks, as Linux does: the 41st is `ELOOP`, for WASI programs, JS programs and the kernel API alike, with or without `..` in a link's target. Emscripten programs resolve paths in their glue, which stops one link earlier, at 40.
|
|
715
715
|
|
|
@@ -846,6 +846,24 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
|
|
|
846
846
|
- **Errors** are Linux's: `ENODEV` for an unknown type (or `hostfs` without a hook), `ENOENT` and `ENOTDIR` for the target, `EBUSY` for a target that is mounted already, and `EINVAL` for unmounting what is not a mount point.
|
|
847
847
|
- **`/proc/mounts`** escapes spaces and backslashes as Linux does, and adds `nomedium` or `failed` to the options of a mount in that state.
|
|
848
848
|
|
|
849
|
+
### The working directory
|
|
850
|
+
|
|
851
|
+
A process's working directory is the directory it changed into, not just its path ([#1164](https://github.com/ai-ecoverse/slicc-kernel/issues/1164)). The kernel records the directory's inode when a process starts and at every `chdir`. A forked child shares its parent's, and so does a spawned child unless the spawn changes directory.
|
|
852
|
+
- **When the directory is replaced:** if it's removed, or something else takes its name (`rm -r d; mkdir d`, `rm -r d; mv n d`, or `mv d x; mv n d`, from this kernel or from another tab on the same root), the process keeps the removed directory:
|
|
853
|
+
- a wasip1 program's `.` (its `.` preopen) can't be opened or listed (`ENOENT`, an empty listing for its preopen descriptor);
|
|
854
|
+
- its relative lookups and creates are `ENOENT`;
|
|
855
|
+
- `..` still reaches the parent.
|
|
856
|
+
- **Detection costs nothing in the steady state:**
|
|
857
|
+
- each held directory has a counter in memory the kernel shares with the process. It moves only when this kernel, another tab, or a `postFsNotice` reports a change at that directory or above it: the kernel's own watcher paths, and every path and tree of a peer change or notice.
|
|
858
|
+
- a process checks the counter, a memory read, before each relative lookup. Only after it moved does it ask the kernel once (`cwd-check`), and the kernel compares the inode at the path with the one it recorded.
|
|
859
|
+
- **The `rm` record:** so that `rm -r d; mkdir d` can be told apart, removing a directory leaves the same small record a rename leaves at the name it vacates (see [Metadata](#metadata)).
|
|
860
|
+
- **Limits:**
|
|
861
|
+
- a WASIX program's C library keeps the working directory as a string and turns relative paths into absolute ones before they reach the kernel. So a WASIX program still sees the new directory through relative paths, and `getcwd` still returns the path, until wasix-sysroot resolves relative paths against the working directory's descriptor ([#1164](https://github.com/ai-ecoverse/slicc-kernel/issues/1164) stays open for that).
|
|
862
|
+
- Emscripten and JS programs: [#1164](https://github.com/ai-ecoverse/slicc-kernel/issues/1164), next.
|
|
863
|
+
- a removal between the check and the lookup is a race, as with any path, and the lookup sees the new directory.
|
|
864
|
+
- on a `hostfs` folder the inode is the host's, which a host file system may reuse for the new directory (ext4 can; APFS doesn't). There the replacement isn't noticed.
|
|
865
|
+
- with more than 1023 distinct working directories held at once, the rest are not tracked.
|
|
866
|
+
|
|
849
867
|
### Exec
|
|
850
868
|
|
|
851
869
|
An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux, whether the descriptors live in the kernel or a program opened the file in its own worker: every open, read-only ones included, is known to the kernel, and all holders share the one file (writes through one are seen through the others). The kernel does not copy it: as NFS does with its "silly rename", it moves the file into a hidden directory, `.slicc-unlinked/`, at the root of the mount it lives on (or of the root file system), and serves the holders' reads and writes from there (a worker's I/O on the old path while the path is gone). No file data is held in memory for it. The file is deleted when the last holder closes it or exits, and the directory goes as soon as it is empty. A file created at the path afterwards is the new file for everyone, also for the holder; a holder whose file another process renames (rather than removes) still uses the old path. A rename over a held file stays atomic for everyone else, as `rename(2)` is: moving the held file aside and the new one into its place are two steps, so a lookup of that name (`open`, `stat`, a read) that comes while they run waits for both, and one that started before them and missed the name in between looks again (also when it spelled the name through a symlinked parent, which the wait doesn't match by path: a miss is retried whenever any replacement finished meanwhile, up to three tries). Every open finds the old file or the new one, never no file (#903). A read through a held descriptor and the rename take the file's turn in order: lookups start to wait for the rename only once it has its turn on every held file it moves aside, so a read that came first finishes, and a rename over a file other processes keep reading never stalls them or itself (#980). A process that opens the file while the rename waits for its turn is a holder too: the rename looks for holders again once it has its turn, so that process keeps the old file as well, and a holder that closed meanwhile leaves nothing to move aside. The same holds for a directory that is removed or renamed over while programs hold files in it: the removal keeps looking for holders until nothing new appears, moving each held file aside, and lookups inside the directory wait for it. An open, a hand-over of a program's descriptor to the kernel (at a fork) or a hold that was under way while the directory went looks its path up again, so it gets the kept old file or ENOENT, never an empty file at the old name (#1013). An open of a path that is gone, with a handle the process no longer holds, is ENOENT rather than a descriptor for a missing file. The OPFS root does the same for its own two steps: a rename onto an existing name replaces the file with `move()` (or, in a browser without it, a copy into the name and a removal of the source), and a lookup that loses that race to a stale handle looks again. A `tmpfs` mount replaces the name in one step; on `fsa` and `hostfs` mounts it is the real file system's `rename`. Listings in the kernel leave `.slicc-unlinked` out, but on an `fsa` or `hostfs` mount it is visible on the real machine while a removed file is held (add `.slicc-unlinked/` to a `.gitignore` there). Each kernel names its entries after itself and holds a Web Lock for that name while it runs; when a mount is set up (and the root at start), the kernel removes the entries whose owner holds no lock, so a tab that died while holding a file leaves nothing behind for long, and entries of other live tabs on the same origin are left alone. Under the Node entry, which has no Web Locks, entries are named after the kernel's process id instead, and an entry is removed when that process is gone (`kill(pid, 0)` fails with `ESRCH`); a reused pid leaves the entry alone. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
|
|
@@ -1087,7 +1105,7 @@ OPFS stores names, bytes, sizes and modification times, nothing else. Everything
|
|
|
1087
1105
|
|
|
1088
1106
|
- mode bits (`chmod`; a `chmod` to the mode a file or directory already has writes nothing and leaves its ctime as it was, which saves a sidecar write for every installer that sets the mode a file was made with);
|
|
1089
1107
|
- access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again: any write or truncate moves it to the time of the write, also in Chrome, whose OPFS doesn't move a file's `lastModified` for writes through a sync access handle, so the kernel records the write's time itself, and it reaches the metadata store up to 100 ms after the write, so a tab closed inside that window keeps the old mtime on disk; the time follows a rename and is dropped with the file); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
|
|
1090
|
-
- inode numbers that stay stable across renames. A file with no record has an inode derived from its path, and a rename keeps it on the file; the rename leaves a small record at the name it vacated with a fresh inode, so a file, symlink or directory made there later doesn't share the renamed one's ([#954](https://github.com/ai-ecoverse/slicc-kernel/issues/954)). An exclusive create doesn't count that record as a taken name, and the boot reconcile keeps it only while a live record still carries the inode it guards;
|
|
1108
|
+
- inode numbers that stay stable across renames. A file with no record has an inode derived from its path, and a rename keeps it on the file; the rename leaves a small record at the name it vacated with a fresh inode, so a file, symlink or directory made there later doesn't share the renamed one's ([#954](https://github.com/ai-ecoverse/slicc-kernel/issues/954)). Removing a directory leaves the same record, so a directory made at its name has a new inode, which is how a process's [working directory](#the-working-directory) notices the swap. The record becomes the new entry's own when something is created there; one with nothing at its name is pruned by the next boot that's alone on the root. An exclusive create doesn't count that record as a taken name, and the boot reconcile keeps it only while a live record still carries the inode it guards;
|
|
1091
1109
|
- symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar, with their own times: `touch -h`, `lutimes` and `utimensat` with `AT_SYMLINK_NOFOLLOW` set the link's, never the target's. A link's own mode can't be changed, as on Linux: `chmod -h`, `lchmod` and `fchmodat` with `AT_SYMLINK_NOFOLLOW` fail with `EOPNOTSUPP` (which `chmod -h` and `tar` ignore for links) and never follow it, and `open` with `O_PATH | O_NOFOLLOW` gives a descriptor for the link itself.
|
|
1092
1110
|
|
|
1093
1111
|
OPFS stays authoritative: an entry is consulted only for a path that exists in OPFS (symlinks only for paths that don't), so files written straight to OPFS, for example by the BIOS installer or a page, get the defaults: directories `0755`, files `0644`, and files under `node_modules/*/bin/` `0755`. A file deleted through the OPFS API disappears at once; its leftover entry is ignored and removed when the next kernel starts. Changes go to OPFS first and then to the sidecar, so a crash in between at worst loses the metadata of that one change. Each change is a read-modify-write inside one IndexedDB transaction, so two kernels (two tabs) on the same origin don't lose each other's updates.
|
|
@@ -1235,6 +1253,12 @@ Not part of the run yet: the rest of bios's setup (the `rest` shard, the upgrade
|
|
|
1235
1253
|
|
|
1236
1254
|
The WASIX fixtures of `test/integration/epoll.test.mjs` (#674), under `test/integration/fixtures/`, are committed with their sources, build scripts and built wasm.
|
|
1237
1255
|
|
|
1256
|
+
### `heldcwd`
|
|
1257
|
+
|
|
1258
|
+
`test/integration/fixtures/heldcwd`: a Rust wasm32-wasip1 program, built by `build.sh` with rustup's wasm32-wasip1 target, for the [working directory](#the-working-directory) rows (#1164).
|
|
1259
|
+
- `heldcwd`: lists `.`, waits for a line on stdin, then lists `.` again, tries a relative create and stat, and lists `..`.
|
|
1260
|
+
- `heldcwd loop N`: stats `old` N times, reads a line, then stats it N more times, for the count of `cwd-check` calls.
|
|
1261
|
+
|
|
1238
1262
|
### `tokiotest`
|
|
1239
1263
|
|
|
1240
1264
|
A small tokio program built for WASIX (`wasm32-wasmer-wasi`), the fixture for `test/integration/epoll.test.mjs` (#674). It runs on tokio's current-thread runtime, or on the multi-thread one (2 workers) with `--multi` first:
|
package/dist/kernel-worker.js
CHANGED
|
@@ -900,6 +900,7 @@ var RemoteWatch = class {
|
|
|
900
900
|
settled = false;
|
|
901
901
|
quiet;
|
|
902
902
|
crowd;
|
|
903
|
+
gone;
|
|
903
904
|
told;
|
|
904
905
|
counted = 0;
|
|
905
906
|
recount() {
|
|
@@ -989,6 +990,7 @@ var RemoteWatch = class {
|
|
|
989
990
|
this.peers.delete(peer);
|
|
990
991
|
this.rebuild();
|
|
991
992
|
this.recount();
|
|
993
|
+
this.gone?.();
|
|
992
994
|
for (const wait2 of this.waiting.values()) {
|
|
993
995
|
if (wait2.left.delete(peer) && wait2.left.size === 0) wait2.done();
|
|
994
996
|
}
|
|
@@ -1024,14 +1026,16 @@ var RemoteWatch = class {
|
|
|
1024
1026
|
async prune() {
|
|
1025
1027
|
if (!this.locks) return;
|
|
1026
1028
|
const held = new Set(((await this.locks.query()).held ?? []).map((lock) => lock.name));
|
|
1029
|
+
let left = false;
|
|
1027
1030
|
for (const tab of /* @__PURE__ */ new Set([...this.others.keys(), ...this.peers])) {
|
|
1028
1031
|
if (!held.has(this.lockOf(tab))) {
|
|
1032
|
+
if (this.peers.delete(tab)) left = true;
|
|
1029
1033
|
this.others.delete(tab);
|
|
1030
|
-
this.peers.delete(tab);
|
|
1031
1034
|
}
|
|
1032
1035
|
}
|
|
1033
1036
|
this.rebuild();
|
|
1034
1037
|
this.recount();
|
|
1038
|
+
if (left) this.gone?.();
|
|
1035
1039
|
}
|
|
1036
1040
|
watched() {
|
|
1037
1041
|
return this.union.size > 0;
|
|
@@ -1121,6 +1125,7 @@ var REPLACED_ATTEMPTS = 4;
|
|
|
1121
1125
|
var RECONCILE_LISTINGS = 8;
|
|
1122
1126
|
var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
|
|
1123
1127
|
var RENAMES = "/.slicc-renames";
|
|
1128
|
+
var RENAME_LOCK = "slicc-rename:";
|
|
1124
1129
|
function movable(handle) {
|
|
1125
1130
|
return typeof handle.move === "function";
|
|
1126
1131
|
}
|
|
@@ -1287,8 +1292,11 @@ var OpfsFs = class {
|
|
|
1287
1292
|
this.lockPrefix = `slicc-excl:${channel ?? ""}:`;
|
|
1288
1293
|
this.rootName = channel;
|
|
1289
1294
|
this.channel = channel === void 0 ? void 0 : new BroadcastChannel(channel);
|
|
1290
|
-
this.remote = channel === void 0 ? void 0 : new RemoteWatch(`${channel}:watch
|
|
1291
|
-
if (this.remote)
|
|
1295
|
+
this.remote = channel === void 0 ? void 0 : new RemoteWatch(`${channel}:watch`, { locks: this.locks ?? null });
|
|
1296
|
+
if (this.remote) {
|
|
1297
|
+
this.remote.quiet = () => this.quiet().then(() => this.settle?.());
|
|
1298
|
+
this.remote.gone = () => void this.orphans();
|
|
1299
|
+
}
|
|
1292
1300
|
if (this.channel) {
|
|
1293
1301
|
const remote = this.remote;
|
|
1294
1302
|
meta.listen?.((c) => {
|
|
@@ -2299,6 +2307,9 @@ var OpfsFs = class {
|
|
|
2299
2307
|
}
|
|
2300
2308
|
}
|
|
2301
2309
|
await this.meta.remove([found.path]);
|
|
2310
|
+
if (found.handle?.kind === "directory") {
|
|
2311
|
+
await this.meta.update(found.path, (entry) => entry ?? vacatedAt(found.path));
|
|
2312
|
+
}
|
|
2302
2313
|
this.touch(Date.now(), parentOf3(found.path));
|
|
2303
2314
|
}
|
|
2304
2315
|
links() {
|
|
@@ -2361,14 +2372,17 @@ var OpfsFs = class {
|
|
|
2361
2372
|
const dir = parent.handle;
|
|
2362
2373
|
const name2 = baseOf(destination);
|
|
2363
2374
|
const swap = targetStat?.isDirectory === true;
|
|
2375
|
+
let lock;
|
|
2364
2376
|
const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
|
|
2365
2377
|
const copying = sourceStat.isDirectory && !movable(source.handle);
|
|
2366
2378
|
const staged = resolvePath(parent.path, staging);
|
|
2367
2379
|
const place = async () => {
|
|
2368
2380
|
this.forget(destination);
|
|
2369
2381
|
const timed2 = await this.explicitTimes(source.path);
|
|
2370
|
-
const
|
|
2371
|
-
const
|
|
2382
|
+
const root = { ino: inodeOf(source.path), ...moving };
|
|
2383
|
+
const owned4 = lock ? { lock } : {};
|
|
2384
|
+
const first = copying ? await this.intend({ from: source.path, to: staged, root, ...owned4 }) : void 0;
|
|
2385
|
+
const second = copying && swap ? await this.intend({ from: staged, to: destination, replace: true, root, ...owned4 }) : void 0;
|
|
2372
2386
|
const moved2 = await this.replacingTarget(
|
|
2373
2387
|
targetStat !== null,
|
|
2374
2388
|
() => this.place(source.handle, dir, staging)
|
|
@@ -2377,7 +2391,7 @@ var OpfsFs = class {
|
|
|
2377
2391
|
throw err;
|
|
2378
2392
|
});
|
|
2379
2393
|
if (swap) await this.rm(destination, { recursive: true });
|
|
2380
|
-
if (second) await this.intend({ from: staged, to: destination }, second);
|
|
2394
|
+
if (second) await this.intend({ from: staged, to: destination, root, ...owned4 }, second);
|
|
2381
2395
|
await this.meta.move(source.path, destination, carry, left);
|
|
2382
2396
|
this.movePending(source.path, destination);
|
|
2383
2397
|
if (!moved2) {
|
|
@@ -2402,7 +2416,11 @@ var OpfsFs = class {
|
|
|
2402
2416
|
const entry = { from: source.path, to: destination, done: promise };
|
|
2403
2417
|
this.renamingDirs.add(entry);
|
|
2404
2418
|
try {
|
|
2405
|
-
|
|
2419
|
+
const locks = this.locks;
|
|
2420
|
+
if (locks) {
|
|
2421
|
+
lock = `${RENAME_LOCK}${crypto.randomUUID()}`;
|
|
2422
|
+
await locks.request(lock, place);
|
|
2423
|
+
} else await place();
|
|
2406
2424
|
} finally {
|
|
2407
2425
|
this.renamingDirs.delete(entry);
|
|
2408
2426
|
resolve2();
|
|
@@ -2434,12 +2452,40 @@ var OpfsFs = class {
|
|
|
2434
2452
|
} catch {
|
|
2435
2453
|
}
|
|
2436
2454
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2455
|
+
replaying = Promise.resolve();
|
|
2456
|
+
orphans() {
|
|
2457
|
+
this.replaying = this.replaying.then(() => this.finishRenames(true)).catch(() => 0);
|
|
2458
|
+
return this.replaying;
|
|
2459
|
+
}
|
|
2460
|
+
async finishRenames(orphaned = false) {
|
|
2461
|
+
const locks = this.locks;
|
|
2462
|
+
const held = /* @__PURE__ */ new Set();
|
|
2463
|
+
if (orphaned && locks) {
|
|
2464
|
+
const { held: h = [], pending = [] } = await locks.query();
|
|
2465
|
+
for (const lock of [...h, ...pending]) held.add(lock.name);
|
|
2466
|
+
}
|
|
2467
|
+
const intents = (await this.meta.under(RENAMES)).filter(
|
|
2468
|
+
(entry) => entry.rename !== void 0 && (!orphaned || locks !== void 0 && entry.rename.lock !== void 0 && !held.has(entry.rename.lock))
|
|
2469
|
+
).sort(
|
|
2439
2470
|
(a, b) => a.rename.at - b.rename.at
|
|
2440
2471
|
);
|
|
2441
2472
|
for (const entry of intents) {
|
|
2442
|
-
const
|
|
2473
|
+
const lock = entry.rename.lock;
|
|
2474
|
+
if (!orphaned || !locks || !lock) await this.finishRename(entry);
|
|
2475
|
+
else await locks.request(lock, () => this.finishRename(entry));
|
|
2476
|
+
}
|
|
2477
|
+
return intents.length;
|
|
2478
|
+
}
|
|
2479
|
+
async finishRename(entry) {
|
|
2480
|
+
const [current] = await this.meta.under(entry.path);
|
|
2481
|
+
if (current?.rename === void 0) return;
|
|
2482
|
+
const { from, to, replace, root } = current.rename;
|
|
2483
|
+
const { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
2484
|
+
const barrier = { from, to, done: promise };
|
|
2485
|
+
this.renamingDirs.add(barrier);
|
|
2486
|
+
this.forget(from);
|
|
2487
|
+
this.forget(to);
|
|
2488
|
+
try {
|
|
2443
2489
|
const left = await this.real(from);
|
|
2444
2490
|
if (left && replace && await this.real(to)) await this.rm(to, { recursive: true });
|
|
2445
2491
|
if (left) {
|
|
@@ -2456,11 +2502,23 @@ var OpfsFs = class {
|
|
|
2456
2502
|
this.forget(to);
|
|
2457
2503
|
}
|
|
2458
2504
|
if ((await this.meta.under(from)).length > 0) {
|
|
2459
|
-
await this.meta.move(
|
|
2505
|
+
await this.meta.move(
|
|
2506
|
+
from,
|
|
2507
|
+
to,
|
|
2508
|
+
(moved2) => ({
|
|
2509
|
+
...occupied(moved2),
|
|
2510
|
+
...root,
|
|
2511
|
+
ino: occupied(moved2).ino ?? root?.ino ?? inodeOf(from),
|
|
2512
|
+
path: to
|
|
2513
|
+
}),
|
|
2514
|
+
void 0
|
|
2515
|
+
);
|
|
2460
2516
|
}
|
|
2461
2517
|
await this.meta.update(entry.path, () => void 0);
|
|
2518
|
+
} finally {
|
|
2519
|
+
this.renamingDirs.delete(barrier);
|
|
2520
|
+
resolve2();
|
|
2462
2521
|
}
|
|
2463
|
-
return intents.length;
|
|
2464
2522
|
}
|
|
2465
2523
|
async settledRename(path) {
|
|
2466
2524
|
if (this.renamingDirs.size === 0 && this.copying.size === 0 || typeof path !== "string")
|
|
@@ -2744,7 +2802,10 @@ var OpfsFs = class {
|
|
|
2744
2802
|
};
|
|
2745
2803
|
}
|
|
2746
2804
|
async reconcile() {
|
|
2747
|
-
if (this.remote && await this.remote.peered())
|
|
2805
|
+
if (this.remote && await this.remote.peered()) {
|
|
2806
|
+
await this.orphans();
|
|
2807
|
+
return 0;
|
|
2808
|
+
}
|
|
2748
2809
|
await this.finishRenames();
|
|
2749
2810
|
let removed = 0;
|
|
2750
2811
|
const entries = await this.meta.all();
|
|
@@ -7034,7 +7095,7 @@ function satisfies(version, range2) {
|
|
|
7034
7095
|
}
|
|
7035
7096
|
|
|
7036
7097
|
// src/version.ts
|
|
7037
|
-
var KERNEL_VERSION = true ? "1.133.
|
|
7098
|
+
var KERNEL_VERSION = true ? "1.133.5" : "0.0.0-dev";
|
|
7038
7099
|
var OS_RELEASE = "6.0.0-slicc";
|
|
7039
7100
|
function procVersion() {
|
|
7040
7101
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -10727,6 +10788,88 @@ var FifoTable = class {
|
|
|
10727
10788
|
}
|
|
10728
10789
|
};
|
|
10729
10790
|
|
|
10791
|
+
// src/kernel/held-cwd.ts
|
|
10792
|
+
var CWD_SLOTS = 1024;
|
|
10793
|
+
var UNTRACKED = CWD_SLOTS - 1;
|
|
10794
|
+
var under4 = (path, changed) => changed === "/" || path === changed || path.startsWith(`${changed}/`);
|
|
10795
|
+
var CwdTable = class {
|
|
10796
|
+
sab = new SharedArrayBuffer(CWD_SLOTS * 4);
|
|
10797
|
+
view = new Int32Array(this.sab);
|
|
10798
|
+
held = /* @__PURE__ */ new Map();
|
|
10799
|
+
free = [];
|
|
10800
|
+
next = 0;
|
|
10801
|
+
checks = 0;
|
|
10802
|
+
inodeOf;
|
|
10803
|
+
constructor(inodeOf2) {
|
|
10804
|
+
this.inodeOf = inodeOf2;
|
|
10805
|
+
}
|
|
10806
|
+
async hold(path, from) {
|
|
10807
|
+
const shared = from === void 0 ? void 0 : this.held.get(from);
|
|
10808
|
+
if (shared && shared.path === path) return this.share(from);
|
|
10809
|
+
const ino = await this.inodeOf(path).catch(() => void 0);
|
|
10810
|
+
const slot2 = this.free.pop() ?? (this.next < UNTRACKED ? this.next++ : UNTRACKED);
|
|
10811
|
+
if (slot2 === UNTRACKED) return slot2;
|
|
10812
|
+
this.held.set(slot2, {
|
|
10813
|
+
path,
|
|
10814
|
+
ino,
|
|
10815
|
+
refs: 1,
|
|
10816
|
+
dead: false,
|
|
10817
|
+
verified: Atomics.load(this.view, slot2)
|
|
10818
|
+
});
|
|
10819
|
+
return slot2;
|
|
10820
|
+
}
|
|
10821
|
+
share(slot2) {
|
|
10822
|
+
const held = slot2 === void 0 ? void 0 : this.held.get(slot2);
|
|
10823
|
+
if (!held) return UNTRACKED;
|
|
10824
|
+
held.refs++;
|
|
10825
|
+
return slot2;
|
|
10826
|
+
}
|
|
10827
|
+
release(slot2) {
|
|
10828
|
+
const held = slot2 === void 0 ? void 0 : this.held.get(slot2);
|
|
10829
|
+
if (!held || --held.refs > 0) return;
|
|
10830
|
+
this.held.delete(slot2);
|
|
10831
|
+
Atomics.add(this.view, slot2, 1);
|
|
10832
|
+
this.free.push(slot2);
|
|
10833
|
+
}
|
|
10834
|
+
stateOf(slot2) {
|
|
10835
|
+
const held = this.held.get(slot2);
|
|
10836
|
+
return { seen: held?.verified ?? Atomics.load(this.view, slot2), dead: held?.dead === true };
|
|
10837
|
+
}
|
|
10838
|
+
touched(paths, trees = []) {
|
|
10839
|
+
if (this.held.size === 0) return;
|
|
10840
|
+
for (const [slot2, held] of this.held) {
|
|
10841
|
+
if (held.dead) continue;
|
|
10842
|
+
if (paths.some((p) => under4(held.path, p)) || trees.some((t) => under4(held.path, t))) {
|
|
10843
|
+
Atomics.add(this.view, slot2, 1);
|
|
10844
|
+
}
|
|
10845
|
+
}
|
|
10846
|
+
}
|
|
10847
|
+
async check(slot2) {
|
|
10848
|
+
this.checks++;
|
|
10849
|
+
const gen = Atomics.load(this.view, slot2);
|
|
10850
|
+
const held = this.held.get(slot2);
|
|
10851
|
+
if (!held) return { gen, alive: true };
|
|
10852
|
+
if (!held.dead) {
|
|
10853
|
+
const ino = await this.inodeOf(held.path).catch(() => null);
|
|
10854
|
+
held.dead = ino === null || held.ino !== void 0 && ino !== held.ino;
|
|
10855
|
+
held.verified = gen;
|
|
10856
|
+
}
|
|
10857
|
+
return { gen, alive: !held.dead };
|
|
10858
|
+
}
|
|
10859
|
+
};
|
|
10860
|
+
function heldInit(o) {
|
|
10861
|
+
if (!o.cwds || o.cwdSlot === void 0) return {};
|
|
10862
|
+
const state = o.cwdState ?? { seen: 0, dead: false };
|
|
10863
|
+
return { held: { sab: o.cwds, slot: o.cwdSlot, ...state } };
|
|
10864
|
+
}
|
|
10865
|
+
function heldHooks(o) {
|
|
10866
|
+
if (!o.cwdCheck || !o.cwdChdir) return {};
|
|
10867
|
+
return { cwdCheck: o.cwdCheck, cwdChdir: o.cwdChdir };
|
|
10868
|
+
}
|
|
10869
|
+
function inheritedCwd(chdir, parent) {
|
|
10870
|
+
return chdir ? void 0 : parent;
|
|
10871
|
+
}
|
|
10872
|
+
|
|
10730
10873
|
// src/fs/copy.ts
|
|
10731
10874
|
async function present2(fs, path) {
|
|
10732
10875
|
return fs.lstat(path).then(
|
|
@@ -21084,6 +21227,8 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
|
|
|
21084
21227
|
"fd-list",
|
|
21085
21228
|
"proc-alarm",
|
|
21086
21229
|
"proc-umask",
|
|
21230
|
+
"cwd-check",
|
|
21231
|
+
"cwd-chdir",
|
|
21087
21232
|
"proc-cred",
|
|
21088
21233
|
"proc-setcred",
|
|
21089
21234
|
"dl-log",
|
|
@@ -22409,6 +22554,14 @@ var WasmProcess = class {
|
|
|
22409
22554
|
return { ok: true, kind: "void" };
|
|
22410
22555
|
case "proc-umask":
|
|
22411
22556
|
return { ok: true, kind: "json", json: this.setUmask(req.mask) };
|
|
22557
|
+
case "cwd-check":
|
|
22558
|
+
return {
|
|
22559
|
+
ok: true,
|
|
22560
|
+
kind: "json",
|
|
22561
|
+
json: await this.options.cwdCheck?.() ?? { gen: 0, alive: true }
|
|
22562
|
+
};
|
|
22563
|
+
case "cwd-chdir":
|
|
22564
|
+
return { ok: true, kind: "json", json: await this.options.cwdChdir?.(req.path) ?? {} };
|
|
22412
22565
|
case "proc-cred":
|
|
22413
22566
|
return { ok: true, kind: "json", json: copyCred(this.cred) };
|
|
22414
22567
|
case "proc-setcred":
|
|
@@ -22958,6 +23111,7 @@ function processInit(opts, sab, umask, cred, lane) {
|
|
|
22958
23111
|
umask,
|
|
22959
23112
|
cred,
|
|
22960
23113
|
...opts.changes ? { changes: opts.changes } : {},
|
|
23114
|
+
...heldInit(opts),
|
|
22961
23115
|
...opts.owners ? { owners: true } : {},
|
|
22962
23116
|
...opts.links !== void 0 ? { links: true } : {},
|
|
22963
23117
|
...opts.ppid !== void 0 ? { ppid: opts.ppid } : {}
|
|
@@ -23031,6 +23185,7 @@ function processOptions(opts, header3, hooks, created4) {
|
|
|
23031
23185
|
...opts.identity ? { identity: opts.identity } : {},
|
|
23032
23186
|
...opts.onSyscall ? { onSyscall: opts.onSyscall } : {},
|
|
23033
23187
|
...opts.settling ? { settling: opts.settling } : {},
|
|
23188
|
+
...heldHooks(opts),
|
|
23034
23189
|
spawner: opts.spawner,
|
|
23035
23190
|
forker: opts.forker,
|
|
23036
23191
|
...opts.fork?.dl ? { dl: opts.fork.dl } : {},
|
|
@@ -23081,6 +23236,7 @@ function threadInit(opts, program, sab, cred, thread, main) {
|
|
|
23081
23236
|
owners: opts.owners === true,
|
|
23082
23237
|
...opts.links !== void 0 ? { links: true } : {},
|
|
23083
23238
|
...opts.changes ? { changes: opts.changes } : {},
|
|
23239
|
+
...heldInit(opts),
|
|
23084
23240
|
...opts.ppid !== void 0 ? { ppid: opts.ppid } : {},
|
|
23085
23241
|
thread,
|
|
23086
23242
|
signals: main
|
|
@@ -26712,6 +26868,11 @@ var Launcher = class {
|
|
|
26712
26868
|
hostname;
|
|
26713
26869
|
watchers = new FsWatchers();
|
|
26714
26870
|
changes = new FileChanges();
|
|
26871
|
+
cwds = new CwdTable(async (path) => {
|
|
26872
|
+
const { dev, ino } = await this.base.stat(path);
|
|
26873
|
+
return `${dev ?? 0}:${ino}`;
|
|
26874
|
+
});
|
|
26875
|
+
cwdSlots = /* @__PURE__ */ new Map();
|
|
26715
26876
|
pnpmHome;
|
|
26716
26877
|
pnpmChain;
|
|
26717
26878
|
pnpmMade;
|
|
@@ -26768,7 +26929,10 @@ var Launcher = class {
|
|
|
26768
26929
|
this.sealed.tickets.normalise = (path) => physicalPathAsync(this.base, path);
|
|
26769
26930
|
this.sealed.userOf = async (uid) => (await this.users.lookup(uid))?.name;
|
|
26770
26931
|
if (this.owners) options.fs.trackOwners?.();
|
|
26771
|
-
this.watchers.tap((paths) =>
|
|
26932
|
+
this.watchers.tap((paths) => {
|
|
26933
|
+
this.changes.publish(...paths);
|
|
26934
|
+
this.cwds.touched(paths);
|
|
26935
|
+
});
|
|
26772
26936
|
this.watchers.onReshape(() => {
|
|
26773
26937
|
for (const nodes of this.openFiles) nodes.reshaped();
|
|
26774
26938
|
});
|
|
@@ -26838,6 +27002,7 @@ var Launcher = class {
|
|
|
26838
27002
|
options.fs.onPeer?.(() => this.flushOpen());
|
|
26839
27003
|
options.fs.remoteChanges?.((change) => {
|
|
26840
27004
|
this.changes.flush();
|
|
27005
|
+
this.cwds.touched(change.paths, change.trees);
|
|
26841
27006
|
this.rewritten(change.paths, change.trees);
|
|
26842
27007
|
});
|
|
26843
27008
|
const holders = [this.held, this.openFiles];
|
|
@@ -27620,11 +27785,12 @@ var Launcher = class {
|
|
|
27620
27785
|
}
|
|
27621
27786
|
const done5 = this.heldUrl(program);
|
|
27622
27787
|
try {
|
|
27788
|
+
const cwdSlot = await this.cwds.hold(cwd, req.cwdFrom);
|
|
27623
27789
|
const handle = await this.admitted(
|
|
27624
27790
|
program.abi !== "js",
|
|
27625
27791
|
req.fds,
|
|
27626
|
-
(fds, quietOom) => this.start({ ...req, fds, quietOom, cwd, program, argv0: target.argv0, args, env })
|
|
27627
|
-
);
|
|
27792
|
+
(fds, quietOom) => this.start({ ...req, fds, quietOom, cwd, program, argv0: target.argv0, args, env, cwdSlot })
|
|
27793
|
+
).finally(() => this.cwds.release(cwdSlot));
|
|
27628
27794
|
void handle.exited.finally(done5);
|
|
27629
27795
|
return handle;
|
|
27630
27796
|
} catch (err) {
|
|
@@ -27711,6 +27877,8 @@ var Launcher = class {
|
|
|
27711
27877
|
throw new SpawnError("ESRCH");
|
|
27712
27878
|
}
|
|
27713
27879
|
const pid = req.pid ?? this.nextPid++;
|
|
27880
|
+
const cwdSlot = this.cwds.share(req.cwdSlot);
|
|
27881
|
+
this.cwdSlots.set(pid, cwdSlot);
|
|
27714
27882
|
const scrubs = this.treeScrubs(req);
|
|
27715
27883
|
const stack = this.stackFor(req.tag);
|
|
27716
27884
|
const terminal = req.fds.stdioTerminal();
|
|
@@ -27745,6 +27913,7 @@ var Launcher = class {
|
|
|
27745
27913
|
...this.dac ? { dac: this.dac } : {},
|
|
27746
27914
|
program: req.program,
|
|
27747
27915
|
changes: this.changes.sab,
|
|
27916
|
+
...this.heldFor(pid, cwdSlot),
|
|
27748
27917
|
argv0: req.argv0,
|
|
27749
27918
|
args: req.args,
|
|
27750
27919
|
env: req.env,
|
|
@@ -27819,6 +27988,8 @@ var Launcher = class {
|
|
|
27819
27988
|
(state) => state === "stopped" ? this.stopped.add(pid) : this.stopped.delete(pid)
|
|
27820
27989
|
);
|
|
27821
27990
|
void handle.exited.then(() => {
|
|
27991
|
+
this.cwds.release(this.cwdSlots.get(pid));
|
|
27992
|
+
this.cwdSlots.delete(pid);
|
|
27822
27993
|
this.freshModules.forget(pid);
|
|
27823
27994
|
const spent = handle.times?.();
|
|
27824
27995
|
if (spent) this.spent.set(pid, spent);
|
|
@@ -28050,6 +28221,7 @@ var Launcher = class {
|
|
|
28050
28221
|
const handle = await this.launch(planned, {
|
|
28051
28222
|
...deciding ? { decided: deciding.promise } : {},
|
|
28052
28223
|
cwd: req.cwd,
|
|
28224
|
+
cwdFrom: inheritedCwd(req.chdir, this.cwdSlots.get(ppid)),
|
|
28053
28225
|
...redeemed,
|
|
28054
28226
|
fds,
|
|
28055
28227
|
report,
|
|
@@ -28111,6 +28283,24 @@ var Launcher = class {
|
|
|
28111
28283
|
for (const resolve2 of waiting) resolve2(true);
|
|
28112
28284
|
}, 0);
|
|
28113
28285
|
}
|
|
28286
|
+
heldFor(pid, slot2) {
|
|
28287
|
+
return {
|
|
28288
|
+
cwds: this.cwds.sab,
|
|
28289
|
+
cwdSlot: slot2,
|
|
28290
|
+
cwdState: this.cwds.stateOf(slot2),
|
|
28291
|
+
cwdCheck: async () => {
|
|
28292
|
+
const current = this.cwdSlots.get(pid);
|
|
28293
|
+
return { ...await this.cwds.check(current), slot: current };
|
|
28294
|
+
},
|
|
28295
|
+
cwdChdir: (path) => this.chdirHeld(pid, path)
|
|
28296
|
+
};
|
|
28297
|
+
}
|
|
28298
|
+
async chdirHeld(pid, path) {
|
|
28299
|
+
this.cwds.release(this.cwdSlots.get(pid));
|
|
28300
|
+
const slot2 = await this.cwds.hold(path);
|
|
28301
|
+
this.cwdSlots.set(pid, slot2);
|
|
28302
|
+
return { slot: slot2, ...this.cwds.stateOf(slot2) };
|
|
28303
|
+
}
|
|
28114
28304
|
forker(ppid, parent) {
|
|
28115
28305
|
const { exec: _exec, pgid: _pgid, group: _group, pid: _pid, scrub: _scrub, ...image } = parent;
|
|
28116
28306
|
return async (state, given) => {
|
|
@@ -28118,6 +28308,7 @@ var Launcher = class {
|
|
|
28118
28308
|
...image,
|
|
28119
28309
|
quietOom,
|
|
28120
28310
|
cwd: state.cwd ?? parent.cwd,
|
|
28311
|
+
cwdSlot: this.cwdSlots.get(ppid),
|
|
28121
28312
|
fds,
|
|
28122
28313
|
ppid,
|
|
28123
28314
|
fork: state,
|