@ai-ecoverse/slicc-kernel 1.124.7 → 1.124.9

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
@@ -735,7 +735,7 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
735
735
 
736
736
  ### Exec
737
737
 
738
- 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 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.
738
+ 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.
739
739
 
740
740
  - **Emscripten:** `Module.sliccKernel.execve(file, argv, env, cwd)` runs `file` as this process's new image on its fds 0, 1 and 2, waits for it, and returns its wait status, or a negative errno when it cannot start. `env` and `cwd` may be `null` for this process's own. An exec shim detects it with `typeof Module.sliccKernel.execve === 'function'` and exits with the status it returns, as `execve` never returns on success. Older shims spawn the program and then call `Module.sliccKernel.execWait(pid)`; that works the same way: a new program asks the kernel for its pid before `main`, and the kernel answers once its parent has made its next call, so an image an older shim execs also reports the pid it replaced (the wait starts once the program is loaded; should the parent make no call within 100 ms after that, the kernel answers anyway, and the image may then report its own pid; `launcher.identities` counts the programs that asked and those answered by that timeout).
741
741
  - **WASIX:** `proc_exec` does this by itself.
@@ -1232,6 +1232,19 @@ A WASIX C program for `test/integration/sig-inherited.test.mjs` (#820), built on
1232
1232
 
1233
1233
  **Build:** `WASIX_SYSROOT_33=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` on `PATH`. `bin/inherittest.wasm` is 39,068 bytes, sha256 `c561a52eb27b3c1b6204b997f4eec967b9f4869c6ab13e7915a5a94b326a710e`.
1234
1234
 
1235
+ ### `sigjmptest`
1236
+
1237
+ A WASIX C program for `test/integration/sigjmp.test.mjs` ([#975](https://github.com/ai-ecoverse/slicc-kernel/issues/975), homescoop#1059), built on wasix-sysroot 2025.9.30-35. Each run takes a mode as its only argument:
1238
+ - `term`: a `SIGTERM` handler with `SIGUSR1` in its `sa_mask` sends itself `SIGUSR1`, which must wait until the handler returns: `term-done usr1`.
1239
+ - `longjmp`: `longjmp` out of a `SIGUSR1` handler that has `SIGUSR2` in `sa_mask`. It prints the mask after the jump, then unblocks everything and raises `SIGUSR1` again with a handler that returns.
1240
+ - `lsleep`, `lnanosleep`, `lread`, `lpoll`: the same with a `SIGALRM` that interrupts `sleep`, `nanosleep`, a `read` of an empty pipe, or `poll`. `sleep`, `nanosleep`, `read`, `poll` (no `l`) and `raise` use `sigsetjmp`/`siglongjmp`.
1241
+ - `direct`, `directmask`: `siglongjmp` with no signal at all. `fork` is `raise` in a forked child.
1242
+ - `hold` holds `flock` on `sigjmp.lock` for 3 seconds.
1243
+
1244
+ On -35, `sigsetjmp` is an ordinary libc function rather than the `setjmp` that LLVM's wasm setjmp/longjmp lowering rewrites, so every `siglongjmp` escapes the program (`[object WebAssembly.Exception]`, exit 70), even `direct`. That is homescoop#1059. The rows use plain `longjmp` until the sysroot fixes it.
1245
+
1246
+ **Build:** `WASIX_SYSROOT_35=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` 133 on `PATH`. Asyncify runs with `asyncify-ignore-indirect`, because functions with wasm `try` blocks can't be flattened. `bin/sigjmptest.wasm` is 52,240 bytes, sha256 `9768c2caafa0bcf55e4b9cd2c97a486d46496a275d4e9bd9d5998e163d630fbb`.
1247
+
1235
1248
  ### `handlertest`
1236
1249
 
1237
1250
  A WASIX C program for `test/integration/handler-defer.test.mjs` (#971), built on wasix-sysroot 2025.9.30-33. Its `SIGTERM` handler counts how often and how deeply it runs. On its first run it sends itself `SIGTERM` again and then writes to stderr. `handlertest` installs it with no flags, and `handlertest nodefer` with `SA_NODEFER`. Either way it prints `the handler ran N times, at most D deep`.
@@ -6625,7 +6625,7 @@ function satisfies(version, range2) {
6625
6625
  }
6626
6626
 
6627
6627
  // src/version.ts
6628
- var KERNEL_VERSION = true ? "1.124.7" : "0.0.0-dev";
6628
+ var KERNEL_VERSION = true ? "1.124.9" : "0.0.0-dev";
6629
6629
  var OS_RELEASE = "6.0.0-slicc";
6630
6630
  function procVersion() {
6631
6631
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -7465,6 +7465,7 @@ var LOOKUP_OPS = [
7465
7465
  "realpath"
7466
7466
  ];
7467
7467
  var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
7468
+ var TOUCHED_MAX = 4096;
7468
7469
  var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
7469
7470
  var lockName = (instance) => `slicc-unlinked:${instance}`;
7470
7471
  var dirOf = (root) => root === "/" ? `/${UNLINKED_DIR}` : `${root}/${UNLINKED_DIR}`;
@@ -7503,6 +7504,9 @@ var UnlinkedKeeper = class {
7503
7504
  queue = Promise.resolve();
7504
7505
  count = 0;
7505
7506
  replacing = /* @__PURE__ */ new Map();
7507
+ gen = 0;
7508
+ floor = 0;
7509
+ touched = /* @__PURE__ */ new Map();
7506
7510
  replaced = 0;
7507
7511
  links = /* @__PURE__ */ new Map();
7508
7512
  linked = /* @__PURE__ */ new Map();
@@ -7529,28 +7533,28 @@ var UnlinkedKeeper = class {
7529
7533
  const { replacing } = this;
7530
7534
  const replace = async (target, op) => {
7531
7535
  const { promise, resolve: resolve2 } = Promise.withResolvers();
7536
+ keeper.touch(target);
7532
7537
  let mine;
7533
7538
  const gated = () => {
7534
7539
  const before = replacing.get(target);
7535
- mine = { done: Promise.all([before?.done, promise]).then(() => void 0) };
7540
+ mine = { done: Promise.all([before?.done, promise]).then(() => void 0), sealed: false };
7536
7541
  replacing.set(target, mine);
7537
7542
  };
7543
+ const sealed = () => {
7544
+ if (mine) mine.sealed = true;
7545
+ };
7538
7546
  try {
7539
- await op(gated);
7547
+ await op(gated, sealed);
7540
7548
  } finally {
7541
7549
  keeper.replaced += 1;
7550
+ keeper.touch(target);
7542
7551
  resolve2();
7543
7552
  if (mine && replacing.get(target) === mine) replacing.delete(target);
7544
7553
  }
7545
7554
  };
7546
7555
  const unreplaced = async (path, op) => {
7547
7556
  const at = fs.resolvePath("/", path);
7548
- const pending = async () => {
7549
- for (let p = at; replacing.size > 0; p = parentOf5(p)) {
7550
- await replacing.get(p)?.done;
7551
- if (p === "/") return;
7552
- }
7553
- };
7557
+ const pending = () => keeper.settled(at);
7554
7558
  for (let attempt = 1; ; attempt++) {
7555
7559
  await pending();
7556
7560
  const seen = keeper.replaced;
@@ -7584,8 +7588,8 @@ var UnlinkedKeeper = class {
7584
7588
  lstat: (path) => unreplaced(path, async () => counted2(await fs.lstat(path))),
7585
7589
  async rm(path, options) {
7586
7590
  const target = fs.resolvePath("/", path);
7587
- await replace(target, async (gated) => {
7588
- const pinned = await keeper.pin(target, gated);
7591
+ await replace(target, async (gated, sealed) => {
7592
+ const pinned = await keeper.pin(target, gated, sealed);
7589
7593
  if (pinned.moved.has(target)) pinned.commit();
7590
7594
  else await keeper.settle(pinned, () => fs.rm(path, options));
7591
7595
  });
@@ -7593,8 +7597,8 @@ var UnlinkedKeeper = class {
7593
7597
  async rename(from, to) {
7594
7598
  const target = fs.resolvePath("/", to);
7595
7599
  if (target === fs.resolvePath("/", from)) return fs.rename(from, to);
7596
- await replace(target, async (gated) => {
7597
- const pinned = await keeper.pin(target, gated);
7600
+ await replace(target, async (gated, sealed) => {
7601
+ const pinned = await keeper.pin(target, gated, sealed);
7598
7602
  await keeper.settle(pinned, () => fs.rename(from, to));
7599
7603
  });
7600
7604
  },
@@ -7602,6 +7606,39 @@ var UnlinkedKeeper = class {
7602
7606
  readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR).map(([name2, st]) => [name2, st && counted2(st)])
7603
7607
  };
7604
7608
  }
7609
+ async settled(path) {
7610
+ for (let p = path; this.replacing.size > 0; p = parentOf5(p)) {
7611
+ await this.replacing.get(p)?.done;
7612
+ if (p === "/") return;
7613
+ }
7614
+ }
7615
+ generation() {
7616
+ return this.gen;
7617
+ }
7618
+ changedSince(path, gen) {
7619
+ if (gen < this.floor) return true;
7620
+ for (let p = path; this.touched.size > 0; p = parentOf5(p)) {
7621
+ if ((this.touched.get(p) ?? 0) > gen) return true;
7622
+ if (p === "/") return false;
7623
+ }
7624
+ return false;
7625
+ }
7626
+ touch(target) {
7627
+ this.gen += 1;
7628
+ this.touched.delete(target);
7629
+ this.touched.set(target, this.gen);
7630
+ if (this.touched.size <= TOUCHED_MAX) return;
7631
+ const [oldest, at] = this.touched.entries().next().value;
7632
+ this.touched.delete(oldest);
7633
+ this.floor = at;
7634
+ }
7635
+ missed(path) {
7636
+ for (let p = path; this.replacing.size > 0; p = parentOf5(p)) {
7637
+ if (this.replacing.get(p)?.sealed) return true;
7638
+ if (p === "/") return false;
7639
+ }
7640
+ return false;
7641
+ }
7605
7642
  async settle(pinned, op) {
7606
7643
  try {
7607
7644
  await op();
@@ -7622,33 +7659,41 @@ var UnlinkedKeeper = class {
7622
7659
  }
7623
7660
  return held;
7624
7661
  }
7625
- async pin(target, gated = () => void 0) {
7662
+ async pin(target, gated = () => void 0, sealed = () => void 0) {
7626
7663
  const paths = [];
7627
7664
  const releases = [];
7628
- for (; ; ) {
7629
- const fresh2 = [...this.heldWithin(target).keys()].filter((path) => !paths.includes(path));
7630
- if (fresh2.length === 0) break;
7631
- fresh2.sort();
7632
- if (paths.some((path) => path > fresh2[0])) {
7633
- for (const release of releases.splice(0)) release();
7634
- fresh2.push(...paths.splice(0));
7665
+ const take = async () => {
7666
+ for (; ; ) {
7667
+ const fresh2 = [...this.heldWithin(target).keys()].filter((path) => !paths.includes(path));
7668
+ if (fresh2.length === 0) return;
7635
7669
  fresh2.sort();
7670
+ if (paths.some((path) => path > fresh2[0])) {
7671
+ for (const release of releases.splice(0)) release();
7672
+ fresh2.push(...paths.splice(0));
7673
+ fresh2.sort();
7674
+ }
7675
+ for (const path of fresh2) {
7676
+ paths.push(path);
7677
+ releases.push(await this.gate.acquire(path));
7678
+ }
7636
7679
  }
7637
- for (const path of fresh2) {
7638
- paths.push(path);
7639
- releases.push(await this.gate.acquire(path));
7640
- }
7641
- }
7680
+ };
7681
+ await take();
7642
7682
  gated();
7643
- const now = this.heldWithin(target);
7644
- const held = new Map(
7645
- paths.flatMap((path) => now.has(path) ? [[path, now.get(path)]] : [])
7646
- );
7683
+ const held = /* @__PURE__ */ new Map();
7647
7684
  const hidden = /* @__PURE__ */ new Map();
7648
- for (const path of held.keys()) {
7649
- const at = await this.hide(path);
7650
- if (at !== void 0) hidden.set(path, at);
7685
+ for (; ; ) {
7686
+ await take();
7687
+ const now = this.heldWithin(target);
7688
+ const next3 = paths.filter((path) => now.has(path) && !held.has(path));
7689
+ if (next3.length === 0) break;
7690
+ for (const path of next3) {
7691
+ held.set(path, now.get(path));
7692
+ const at = await this.hide(path);
7693
+ if (at !== void 0) hidden.set(path, at);
7694
+ }
7651
7695
  }
7696
+ sealed();
7652
7697
  const done4 = () => {
7653
7698
  for (const release of releases) release();
7654
7699
  };
@@ -10314,6 +10359,9 @@ var HeldPaths = class extends Map {
10314
10359
  keptFor(handle) {
10315
10360
  return this.keptHandles.get(handle);
10316
10361
  }
10362
+ holdsHandle(handle) {
10363
+ return this.handles.has(handle);
10364
+ }
10317
10365
  keeps(hidden) {
10318
10366
  for (const file of this.uses.keys()) if (file.hidden === hidden) return true;
10319
10367
  for (const file of this.unlinked.values()) if (file.hidden === hidden) return true;
@@ -11760,10 +11808,16 @@ async function run(entry, path, req) {
11760
11808
  return done;
11761
11809
  case "exists":
11762
11810
  return json2(await fs.exists(path));
11763
- case "stat":
11764
- return json2(statJson(await shownAs(entry, path, await fs.stat(path))));
11765
- case "lstat":
11766
- return json2(statJson(await shownAs(entry, path, await fs.lstat(path))));
11811
+ case "stat": {
11812
+ const gen = entry.replacements?.generation();
11813
+ const st = statJson(await shownAs(entry, path, await fs.stat(path)));
11814
+ return json2(gen === void 0 ? st : { ...st, gen });
11815
+ }
11816
+ case "lstat": {
11817
+ const gen = entry.replacements?.generation();
11818
+ const st = statJson(await shownAs(entry, path, await fs.lstat(path)));
11819
+ return json2(gen === void 0 ? st : { ...st, gen });
11820
+ }
11767
11821
  case "readdir":
11768
11822
  return json2(await fs.readdir(path));
11769
11823
  case "readdir-stat":
@@ -11938,14 +11992,26 @@ async function opening(entry, path, mode) {
11938
11992
  await need(g, "open", path, want, { directory: false });
11939
11993
  }
11940
11994
  }
11995
+ async function stillThere(entry, path, req) {
11996
+ if (req.gen === void 0 || !entry.replacements?.changedSince(path, req.gen)) return;
11997
+ const st = await entry.fs.stat(path).catch(() => void 0);
11998
+ if (!st) throw syncError("ENOENT", path);
11999
+ }
11941
12000
  async function holdOp(entry, path, req) {
11942
12001
  const mode = req.mode ?? 0;
11943
12002
  const [on, open] = [(mode & 1) === 1, (mode & 2) === 2];
11944
12003
  if (on && open) {
11945
- await opening(entry, path, mode);
11946
- const real = await entry.fs.realpath?.(path, true).catch(() => path);
11947
- await entry.source?.open(path, real);
11948
- return json2(entry.hold?.(path, on, open, real) ?? null);
12004
+ for (; ; ) {
12005
+ const r = entry.replacements;
12006
+ await r?.settled(path);
12007
+ const gen = r?.generation() ?? 0;
12008
+ await opening(entry, path, mode);
12009
+ const real = await entry.fs.realpath?.(path, true).catch(() => path);
12010
+ await entry.source?.open(path, real);
12011
+ await stillThere(entry, path, req);
12012
+ if (!r || !r.missed(path) && !r.changedSince(path, gen))
12013
+ return json2(entry.hold?.(path, on, open, real) ?? null);
12014
+ }
11949
12015
  }
11950
12016
  entry.hold?.(path, on, open, void 0, req.handle);
11951
12017
  return done;
@@ -17684,6 +17750,7 @@ var VfsNode = class {
17684
17750
  path;
17685
17751
  orphaned;
17686
17752
  id;
17753
+ ino;
17687
17754
  onDirty;
17688
17755
  onDirtyState;
17689
17756
  counted = false;
@@ -18036,8 +18103,12 @@ var VfsNodes = class {
18036
18103
  if (id2 !== void 0) this.know(node, id2);
18037
18104
  return node;
18038
18105
  }
18039
- open(path, pin, writable2 = false, id2) {
18040
- const node = this.named(path, id2) ?? this.create(path, id2, new VfsNode(this.fs, path, void 0, false, false, pin));
18106
+ open(path, pin, writable2 = false, id2, ino) {
18107
+ const found = this.named(path, id2);
18108
+ const same = !found || ino === void 0 || found.ino === void 0 || found.ino === ino;
18109
+ if (found && !same) this.orphan(found);
18110
+ const node = (same ? found : void 0) ?? this.create(path, id2, new VfsNode(this.fs, path, void 0, false, false, pin));
18111
+ node.ino ??= ino;
18041
18112
  node.opens++;
18042
18113
  if (writable2) node.writers++;
18043
18114
  return node;
@@ -18331,7 +18402,7 @@ function vfsFile(fs, opts, nodes) {
18331
18402
  const access2 = opts.flags & O_ACCMODE;
18332
18403
  const readable2 = access2 !== O_WRONLY;
18333
18404
  const writable2 = access2 === O_WRONLY || access2 === O_RDWR;
18334
- const node = !nodes || opts.orphan ? new VfsNode(fs, opts.path, opts.contents, opts.orphan === true, opts.dirty === true) : opts.contents !== void 0 ? nodes.adopt(opts.path, opts.contents, opts.dirty === true, writable2, opts.id) : nodes.open(opts.path, opts.pin, writable2, opts.id);
18405
+ const node = !nodes || opts.orphan ? new VfsNode(fs, opts.path, opts.contents, opts.orphan === true, opts.dirty === true) : opts.contents !== void 0 ? nodes.adopt(opts.path, opts.contents, opts.dirty === true, writable2, opts.id) : nodes.open(opts.path, opts.pin, writable2, opts.id, opts.ino);
18335
18406
  let openError;
18336
18407
  const atOpen = (op) => {
18337
18408
  void node.serial(op).catch((err) => {
@@ -19101,6 +19172,7 @@ var WasmProcess = class {
19101
19172
  const fd = this.fds.install(heldFile(), 3);
19102
19173
  try {
19103
19174
  const nodes = this.nodes;
19175
+ const gen = this.replacements?.generation() ?? 0;
19104
19176
  const path = await this.openPath(req, marked);
19105
19177
  if (inProc(path)) {
19106
19178
  this.fds.installAt(fd, await this.procFile(req, path));
@@ -19125,7 +19197,15 @@ var WasmProcess = class {
19125
19197
  }
19126
19198
  });
19127
19199
  } else {
19128
- this.fds.installAt(fd, this.openVfsFile(open, await nodes.identify(path), fs));
19200
+ await this.unmissed(
19201
+ () => this.openPath(req, marked),
19202
+ async (at) => {
19203
+ await this.stillOpen(at, req.handle);
19204
+ return nodes.identify(at);
19205
+ },
19206
+ (id2, at) => this.fds.installAt(fd, this.openVfsFile({ ...open, path: at }, id2, fs)),
19207
+ { at: path, gen }
19208
+ );
19129
19209
  }
19130
19210
  if (req.existing && !await present2(fs, path, true)) {
19131
19211
  throw new KernelError("ENOENT");
@@ -19306,7 +19386,8 @@ var WasmProcess = class {
19306
19386
  ...req.truncate ? { truncate: true } : {},
19307
19387
  ...req.create ? { create: true } : {},
19308
19388
  ...req.pin ? { pin: req.pin } : {},
19309
- ...id2 !== void 0 && !req.orphan ? { id: id2 } : {}
19389
+ ...id2 !== void 0 && !req.orphan ? { id: id2 } : {},
19390
+ ...req.ino !== void 0 && !req.orphan ? { ino: req.ino } : {}
19310
19391
  },
19311
19392
  this.nodes
19312
19393
  );
@@ -19477,9 +19558,27 @@ var WasmProcess = class {
19477
19558
  return false;
19478
19559
  }
19479
19560
  keptHandle;
19561
+ holdsHandle;
19562
+ replacements;
19563
+ async unmissed(path, look, make, first) {
19564
+ let given = first;
19565
+ for (; ; ) {
19566
+ const r = this.replacements;
19567
+ const gen = given?.gen ?? r?.generation() ?? 0;
19568
+ const at = given?.at ?? await path();
19569
+ given = void 0;
19570
+ await r?.settled(at);
19571
+ const found = await look(at);
19572
+ if (!r || !r.missed(at) && !r.changedSince(at, gen)) return make(found, at);
19573
+ }
19574
+ }
19575
+ async stillOpen(at, handle) {
19576
+ if (handle === void 0 || this.keptHandle?.(handle) !== void 0) return;
19577
+ if (this.holdsHandle?.(handle) !== false) return;
19578
+ if (!await present2(this.options.fs, at, true)) throw new KernelError("ENOENT");
19579
+ }
19480
19580
  async keptOr(path, handle) {
19481
- const held = handle === void 0 ? void 0 : this.keptHandle?.(handle);
19482
- if (held !== void 0) return held;
19581
+ if (handle !== void 0) return this.keptHandle?.(handle) ?? path;
19483
19582
  const kept = this.keptPath?.(path);
19484
19583
  return kept !== void 0 && !await present2(this.options.fs, path, true) ? kept : path;
19485
19584
  }
@@ -19489,25 +19588,37 @@ var WasmProcess = class {
19489
19588
  await this.fds.dup2Clipboard(req.share, req.fd);
19490
19589
  return;
19491
19590
  }
19492
- if (req.path === void 0) throw new KernelError("EINVAL");
19493
- const path = req.orphan ? req.path : await this.keptOr(await this.nodes.targetKey(req.path), req.handle);
19494
- const id2 = req.orphan ? void 0 : await this.nodes.identify(path);
19495
- if (!this.fds.get(req.fd).file.held) throw new KernelError("EBADF");
19496
- const file = vfsFile(
19497
- this.options.fs,
19498
- {
19499
- path,
19500
- flags: req.flags ?? 0,
19501
- position: req.position ?? 0,
19502
- ...req.contents !== void 0 ? { contents: req.contents } : {},
19503
- ...req.orphan ? { orphan: true } : {},
19504
- ...req.dirty ? { dirty: true } : {},
19505
- ...req.pin ? { pin: req.pin } : {},
19506
- ...id2 !== void 0 ? { id: id2 } : {}
19591
+ const asked = req.path;
19592
+ if (asked === void 0) throw new KernelError("EINVAL");
19593
+ const install = ([path, id2]) => {
19594
+ if (!this.fds.get(req.fd).file.held) throw new KernelError("EBADF");
19595
+ const file = vfsFile(
19596
+ this.options.fs,
19597
+ {
19598
+ path,
19599
+ flags: req.flags ?? 0,
19600
+ position: req.position ?? 0,
19601
+ ...req.contents !== void 0 ? { contents: req.contents } : {},
19602
+ ...req.orphan ? { orphan: true } : {},
19603
+ ...req.dirty ? { dirty: true } : {},
19604
+ ...req.pin ? { pin: req.pin } : {},
19605
+ ...id2 !== void 0 ? { id: id2 } : {},
19606
+ ...req.ino !== void 0 && path === asked ? { ino: req.ino } : {}
19607
+ },
19608
+ this.nodes
19609
+ );
19610
+ this.fds.installAt(req.fd, file);
19611
+ };
19612
+ if (req.orphan) return install([asked, void 0]);
19613
+ await this.unmissed(
19614
+ () => this.nodes.targetKey(asked),
19615
+ async (target) => {
19616
+ const at = await this.keptOr(target, req.handle);
19617
+ await this.stillOpen(at, req.handle);
19618
+ return [at, await this.nodes.identify(at)];
19507
19619
  },
19508
- this.nodes
19620
+ install
19509
19621
  );
19510
- this.fds.installAt(req.fd, file);
19511
19622
  }
19512
19623
  reserve(fd, min = 3, meta) {
19513
19624
  if (fd === void 0) return this.fds.install(heldFile(meta), Math.max(3, min));
@@ -20160,6 +20271,12 @@ function inherited(opts) {
20160
20271
  ...opts.cred ? { cred: opts.cred } : {}
20161
20272
  };
20162
20273
  }
20274
+ function heldBy(process, holds2, replacements) {
20275
+ process.keptPath = (path) => holds2.keptAt(path)?.hidden;
20276
+ process.keptHandle = (handle) => holds2.keptFor(handle)?.hidden;
20277
+ process.holdsHandle = (handle) => holds2.holdsHandle(handle);
20278
+ if (replacements) process.replacements = replacements;
20279
+ }
20163
20280
  function holdingToken(opts, holds2, caller) {
20164
20281
  return mintSyncFsToken({
20165
20282
  ...opts.owners ? caller : { umask: caller.umask },
@@ -20167,6 +20284,7 @@ function holdingToken(opts, holds2, caller) {
20167
20284
  ...opts.links !== void 0 ? { links: opts.links } : {},
20168
20285
  ...opts.fresh ? { fresh: (path) => opts.fresh?.(path, caller.umask()) } : {},
20169
20286
  ...opts.source ? { source: opts.source } : {},
20287
+ ...opts.replacements ? { replacements: opts.replacements } : {},
20170
20288
  fs: opts.fs,
20171
20289
  noLinks: opts.noLinks,
20172
20290
  cwd: opts.cwd,
@@ -20248,6 +20366,7 @@ function threadInit(opts, program, sab, cred, thread, main) {
20248
20366
  cred: copyCred(cred),
20249
20367
  owners: opts.owners === true,
20250
20368
  ...opts.links !== void 0 ? { links: true } : {},
20369
+ ...opts.changes ? { changes: opts.changes } : {},
20251
20370
  ...opts.ppid !== void 0 ? { ppid: opts.ppid } : {},
20252
20371
  thread,
20253
20372
  signals: main
@@ -20305,8 +20424,7 @@ function spawnWasmProcess(opts) {
20305
20424
  created: created3,
20306
20425
  holdsKept: async (path) => path.includes(`/${UNLINKED_DIR}/`) && (holds2.keeps(path) || await process.holds(path))
20307
20426
  });
20308
- process.keptPath = (path) => holds2.keptAt(path)?.hidden;
20309
- process.keptHandle = (handle2) => holds2.keptFor(handle2)?.hidden;
20427
+ heldBy(process, holds2, opts.replacements);
20310
20428
  const worker = opts.createWorker();
20311
20429
  const dying = new Dying();
20312
20430
  const dispatch2 = (req, block) => dying.serve(
@@ -25426,6 +25544,7 @@ var Launcher = class {
25426
25544
  nodes: stack.nodes,
25427
25545
  held: this.held,
25428
25546
  unlinkGate: stack.unlinked.gate,
25547
+ replacements: stack.unlinked,
25429
25548
  statfs: (path) => this.mounts.statfs(path),
25430
25549
  mounts: () => this.mounts.list(),
25431
25550
  mount: (call, signal) => this.processMount(pid, call, signal),