@ai-ecoverse/slicc-kernel 1.98.0 → 1.98.2

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
@@ -535,7 +535,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
535
535
  - **Capabilities:** `readonly`, `symlinks`, `links` (hard links: the driver answers `link(from, to)`, reports `nlink` in its attributes, and gives every attribute a stable `ino`, the same for all names of a file; a mount whose driver offers `links` but answers an attribute without `ino` loses them: `link` is `EPERM` there and `st_nlink` is 1), `chmod`, `owners`, `ranges`, `maxIo` (the largest read or write per message), `maxFile`, `listingStats`, `sessions`, `linkTimes`, and `attrTtl`/`entryTtl` (in ms, default 1000). `sessions` promises that a handle opened for writing keeps its writes to itself until it is released, so the kernel keeps one open per file being written instead of one per write (the `fsa` driver has it). `linkTimes` promises that `setattr` with `mtime` on a symlink sets the link's own time. Without it, setting a link's own times (`touch -h`, `utimensat` with `AT_SYMLINK_NOFOLLOW`) fails with `EOPNOTSUPP` and never reaches the target. tmpfs has it; hostfs does not. Setting times through a link follows it in the kernel, so `setattr` gets the target. `ranges` (driver protocol 1.1) promises that a write lands in the file at once, with no copy of the whole file on `open` or `release`, and that `setattr` takes `size`. `owners` (driver protocol 1.2) promises that the driver stores owners: its attributes carry `uid` and `gid`, `open` with `create`, `mkdir` and `symlink` may carry the new path's `uid`, `gid` and `mode` (the `owner` of the open flags, the second or third handler argument), a new path without them takes its directory's owner, and `setattr` takes `uid` and `gid`. tmpfs has it.
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
- - **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. Limits for now: the same file under two names (a symlink in the path, a hard link, an alias on a mount without `realpath`) isn't kept coherent yet, so processes sharing a file should name it by the same path ([#669](https://github.com/ai-ecoverse/slicc-kernel/issues/669)); another tab's writes reach this tab's processes only as far as the root's change notifications (OPFS's `BroadcastChannel` watch) carry them.
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
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.
540
540
  - **Rules on a mount:**
541
541
  - a rename across mounts is `EXDEV`, so `mv` copies;
@@ -917,7 +917,7 @@ WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh
917
917
 
918
918
  - `WASIX_SYSROOT_28` is the unpacked sysroot package.
919
919
  - `wasixcc` (0.4.7, WASIX clang 21.1.2) comes from `PATH`, or is installed by `install-wasixcc.sh` as for `dlfork`.
920
- - The build is reproducible: `bin/epolltest.wasm` is 78,672 bytes, sha256 `f1ff262e2d90080c37c968187ade45c7d3adf81b49dd0dc0109428ef9c7e1df8`.
920
+ - The build is reproducible: `bin/epolltest.wasm` is 78,799 bytes, sha256 `9ff8d336a261ebb9076b5b9c760e9ba50fa17053b1d66ab89cfe05395c8e1deb`.
921
921
 
922
922
  ## License
923
923
 
@@ -8843,9 +8843,19 @@ function touches(watcher, changed) {
8843
8843
  (watched) => changed === watched || under3(watched, changed) || (watcher.recursive ? under3(changed, watched) : parentOf6(changed) === watched)
8844
8844
  );
8845
8845
  }
8846
+ var REALS_KEPT = 4096;
8846
8847
  var FsWatchers = class {
8847
8848
  watchers = /* @__PURE__ */ new Set();
8848
8849
  taps = [];
8850
+ reals = /* @__PURE__ */ new Map();
8851
+ reshapes = [];
8852
+ onReshape(listener) {
8853
+ this.reshapes.push(listener);
8854
+ }
8855
+ reshaped() {
8856
+ this.reals.clear();
8857
+ for (const listener of this.reshapes) listener();
8858
+ }
8849
8859
  tap(listener) {
8850
8860
  this.taps.push(listener);
8851
8861
  }
@@ -8889,8 +8899,41 @@ var FsWatchers = class {
8889
8899
  this.changed(...changed(...args));
8890
8900
  }
8891
8901
  };
8902
+ const reals = this.reals;
8903
+ const realOf = async (path) => {
8904
+ const at = normalizePath(path);
8905
+ const known = reals.get(at);
8906
+ if (known !== void 0) return known;
8907
+ if (!fs.realpath) return at;
8908
+ let real;
8909
+ try {
8910
+ real = await fs.realpath(at, true);
8911
+ } catch {
8912
+ return at;
8913
+ }
8914
+ if (reals.size >= REALS_KEPT) reals.clear();
8915
+ reals.set(at, real);
8916
+ return real;
8917
+ };
8918
+ const content = (run2) => async (...args) => {
8919
+ try {
8920
+ await run2(...args);
8921
+ } finally {
8922
+ const at = normalizePath(args[0]);
8923
+ const real = await realOf(at);
8924
+ this.changed(...real === at ? [at] : [at, real]);
8925
+ }
8926
+ };
8927
+ const reshaping = (run2) => async (...args) => {
8928
+ this.reshaped();
8929
+ try {
8930
+ await run2(...args);
8931
+ } finally {
8932
+ this.reshaped();
8933
+ }
8934
+ };
8892
8935
  const overrides = {
8893
- writeFile: after(fs.writeFile.bind(fs), (path) => [path]),
8936
+ writeFile: content(fs.writeFile.bind(fs)),
8894
8937
  mkdir: async (path, options) => {
8895
8938
  const created3 = [];
8896
8939
  if (options?.recursive) {
@@ -8906,13 +8949,15 @@ var FsWatchers = class {
8906
8949
  this.changed(...created3, path);
8907
8950
  }
8908
8951
  },
8909
- rm: after(fs.rm.bind(fs), (path) => [path]),
8910
- rename: after(fs.rename.bind(fs), (from, to) => [from, to]),
8911
- link: after(
8912
- (from, to, follow2) => fs.link(from, to, follow2),
8913
- (_from, to) => [to]
8952
+ rm: reshaping(after(fs.rm.bind(fs), (path) => [path])),
8953
+ rename: reshaping(after(fs.rename.bind(fs), (from, to) => [from, to])),
8954
+ link: reshaping(
8955
+ after(
8956
+ (from, to, follow2) => fs.link(from, to, follow2),
8957
+ (_from, to) => [to]
8958
+ )
8914
8959
  ),
8915
- symlink: after(fs.symlink.bind(fs), (_target, path) => [path]),
8960
+ symlink: reshaping(after(fs.symlink.bind(fs), (_target, path) => [path])),
8916
8961
  chmod: after(fs.chmod.bind(fs), (path) => [path]),
8917
8962
  ...fs.chown ? { chown: after(fs.chown.bind(fs), (path) => [path]) } : {},
8918
8963
  utimes: after(fs.utimes.bind(fs), (path) => [path]),
@@ -8923,8 +8968,8 @@ var FsWatchers = class {
8923
8968
  };
8924
8969
  const ranged2 = rangedOps(fs);
8925
8970
  if (ranged2) {
8926
- overrides.pwrite = after(ranged2.pwrite, (path) => [path]);
8927
- overrides.truncate = after(ranged2.truncate, (path) => [path]);
8971
+ overrides.pwrite = content(ranged2.pwrite);
8972
+ overrides.truncate = content(ranged2.truncate);
8928
8973
  }
8929
8974
  return new Proxy(fs, {
8930
8975
  get(target, key, receiver) {
@@ -9692,6 +9737,7 @@ var MountTable = class {
9692
9737
  };
9693
9738
  mounted = mount;
9694
9739
  this.mounts.set(target, mount);
9740
+ this.deps.reshaped?.();
9695
9741
  this.notes.delete(target);
9696
9742
  return this.list().find((m) => m.target === target);
9697
9743
  }
@@ -9706,6 +9752,7 @@ var MountTable = class {
9706
9752
  if (!detach && this.deps.busy(at)) throw errnoError("EBUSY", `${at} has open files`);
9707
9753
  this.notes.delete(at);
9708
9754
  this.mounts.delete(at);
9755
+ this.deps.reshaped?.();
9709
9756
  const waits = [...this.sessions].filter(([, s]) => s.mount === mount).map(([real]) => this.commit(real));
9710
9757
  for (const c of this.committing.values()) if (c.mount === mount) waits.push(c.done);
9711
9758
  const close = () => {
@@ -12035,7 +12082,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
12035
12082
  var EARLY_WHOLE_FILE = 64 * 1024;
12036
12083
 
12037
12084
  // src/version.ts
12038
- var KERNEL_VERSION = true ? "1.98.0" : "0.0.0-dev";
12085
+ var KERNEL_VERSION = true ? "1.98.2" : "0.0.0-dev";
12039
12086
  var OS_RELEASE = "6.0.0-slicc";
12040
12087
  function procVersion() {
12041
12088
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -12628,15 +12675,19 @@ var EpollFile = class _EpollFile {
12628
12675
  return out;
12629
12676
  }
12630
12677
  async wait(fds, max, timeoutMs, signal) {
12631
- const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
12678
+ const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : performance.now() + timeoutMs;
12632
12679
  for (; ; ) {
12633
12680
  const events = this.collect(fds, max);
12634
- if (events.length > 0 || Date.now() >= deadline) return events;
12681
+ if (events.length > 0 || performance.now() >= deadline) return events;
12635
12682
  const round = new AbortController();
12636
12683
  const woken = this.changes.wait(AbortSignal.any([round.signal, signal]));
12637
12684
  woken.catch(() => void 0);
12638
12685
  let timer;
12639
- const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => timer = setTimeout(resolve2, deadline - Date.now()))];
12686
+ const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [
12687
+ new Promise(
12688
+ (resolve2) => timer = setTimeout(resolve2, deadline - performance.now())
12689
+ )
12690
+ ];
12640
12691
  try {
12641
12692
  await Promise.race([woken, ...expiry]);
12642
12693
  } finally {
@@ -14431,7 +14482,7 @@ function interruption(signal) {
14431
14482
  return { promise, done: () => signal.removeEventListener("abort", fail2) };
14432
14483
  }
14433
14484
  async function selectFds(fds, read, write, timeoutMs, interrupt) {
14434
- const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
14485
+ const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : performance.now() + timeoutMs;
14435
14486
  const interrupted3 = interruption(interrupt);
14436
14487
  interrupted3.promise.catch(() => {
14437
14488
  });
@@ -14444,14 +14495,18 @@ async function selectFds(fds, read, write, timeoutMs, interrupt) {
14444
14495
  async function waitReady(fds, read, write, deadline, interrupted3) {
14445
14496
  for (; ; ) {
14446
14497
  const now = ready(fds, read, write);
14447
- if (now.read.length > 0 || now.write.length > 0 || Date.now() >= deadline) return now;
14498
+ if (now.read.length > 0 || now.write.length > 0 || performance.now() >= deadline) return now;
14448
14499
  const round = new AbortController();
14449
14500
  const changes = [.../* @__PURE__ */ new Set([...read, ...write])].flatMap((fd) => {
14450
14501
  const file = fds.get(fd).file;
14451
14502
  return file.changed === void 0 ? [] : [file.changed(round.signal).catch(() => void 0)];
14452
14503
  });
14453
14504
  let timer;
14454
- const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => timer = setTimeout(resolve2, deadline - Date.now()))];
14505
+ const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [
14506
+ new Promise(
14507
+ (resolve2) => timer = setTimeout(resolve2, deadline - performance.now())
14508
+ )
14509
+ ];
14455
14510
  try {
14456
14511
  await Promise.race([...changes, ...expiry, interrupted3]);
14457
14512
  } finally {
@@ -14891,6 +14946,7 @@ var VfsNodes = class {
14891
14946
  if (!this.byPath.has(to)) this.byPath.set(to, node);
14892
14947
  }
14893
14948
  know(node, id2) {
14949
+ this.reshaped();
14894
14950
  node.id = id2;
14895
14951
  if (!this.byId.has(id2)) this.byId.set(id2, node);
14896
14952
  }
@@ -14900,6 +14956,7 @@ var VfsNodes = class {
14900
14956
  return out;
14901
14957
  }
14902
14958
  forget(node) {
14959
+ this.reshaped();
14903
14960
  for (const p of this.namesOf(node)) this.byPath.delete(p);
14904
14961
  if (node.id !== void 0 && this.byId.get(node.id) === node) this.byId.delete(node.id);
14905
14962
  }
@@ -14914,6 +14971,38 @@ var VfsNodes = class {
14914
14971
  if (node.path === path) node.renamedTo(others[0]);
14915
14972
  return true;
14916
14973
  }
14974
+ holdsAny() {
14975
+ return this.byPath.size > 0;
14976
+ }
14977
+ epoch = 0;
14978
+ plain = /* @__PURE__ */ new Map();
14979
+ reshaped() {
14980
+ this.epoch++;
14981
+ this.plain.clear();
14982
+ }
14983
+ plainAt(path) {
14984
+ return this.plain.get(path) === this.epoch;
14985
+ }
14986
+ get shape() {
14987
+ return this.epoch;
14988
+ }
14989
+ notePlain(path, shape) {
14990
+ if (shape !== this.epoch) return;
14991
+ if (this.plain.size >= 4096) this.plain.clear();
14992
+ this.plain.set(path, shape);
14993
+ }
14994
+ holdsName(name2) {
14995
+ for (const path of this.byPath.keys())
14996
+ if (path.slice(path.lastIndexOf("/") + 1) === name2) return true;
14997
+ return false;
14998
+ }
14999
+ holdsLinked() {
15000
+ return this.byId.size > 0;
15001
+ }
15002
+ holdingId(id2) {
15003
+ const node = this.byId.get(id2);
15004
+ return node && !node.orphaned ? node : void 0;
15005
+ }
14917
15006
  holding(path) {
14918
15007
  if (this.byPath.size === 0) return void 0;
14919
15008
  const node = this.byPath.get(path);
@@ -14925,6 +15014,7 @@ var VfsNodes = class {
14925
15014
  return node;
14926
15015
  }
14927
15016
  create(path, id2, node) {
15017
+ this.reshaped();
14928
15018
  node.onDirty = () => this.changed?.(node.path);
14929
15019
  this.byPath.set(path, node);
14930
15020
  if (id2 !== void 0) this.know(node, id2);
@@ -15066,11 +15156,35 @@ async function refuseReadonly(fs, path, flags2, opts = {}) {
15066
15156
  const st = own ?? await stat(await targetDir(fs, path));
15067
15157
  if (st?.readonly) throw new KernelError("EROFS");
15068
15158
  }
15159
+ var baseName = (path) => path.slice(path.lastIndexOf("/") + 1);
15160
+ async function byRealPath(fs, nodes, at) {
15161
+ const real = await fs.realpath?.(at, true).catch(() => void 0);
15162
+ return real === void 0 || real === at ? void 0 : nodes.holding(real);
15163
+ }
15164
+ async function aliasOf(fs, nodes, at) {
15165
+ if (nodes.holdsName(baseName(at))) {
15166
+ const named = await byRealPath(fs, nodes, at);
15167
+ if (named) return named;
15168
+ }
15169
+ const st = await fs.lstat(at).catch(() => void 0);
15170
+ if (!st) return void 0;
15171
+ if (st.isSymbolicLink) return byRealPath(fs, nodes, at);
15172
+ if (st.ino === void 0 || (st.nlink ?? 1) < 2 || !nodes.holdsLinked()) return void 0;
15173
+ return nodes.holdingId(`${st.dev ?? 0}:${st.ino}`);
15174
+ }
15175
+ async function heldAt(fs, nodes, at) {
15176
+ const held = nodes.holding(at);
15177
+ if (held || !nodes.holdsAny() || nodes.plainAt(at)) return held;
15178
+ const shape = nodes.shape;
15179
+ const found = await aliasOf(fs, nodes, at);
15180
+ if (!found) nodes.notePlain(at, shape);
15181
+ return found;
15182
+ }
15069
15183
  function heldIo(fs, nodes, changed) {
15070
15184
  const ranged2 = rangedOps(fs);
15071
- const node = (path) => nodes.holding(fs.resolvePath("/", path));
15185
+ const node = (path) => heldAt(fs, nodes, fs.resolvePath("/", path));
15072
15186
  const write = async (path, op, fallback) => {
15073
- const held = node(path);
15187
+ const held = await node(path);
15074
15188
  if (!held) return fallback();
15075
15189
  const out = await held.serial(async () => {
15076
15190
  const done4 = await op(held);
@@ -15078,6 +15192,7 @@ function heldIo(fs, nodes, changed) {
15078
15192
  return done4;
15079
15193
  });
15080
15194
  changed(fs.resolvePath("/", path));
15195
+ if (held.path !== fs.resolvePath("/", path)) changed(held.path);
15081
15196
  return out;
15082
15197
  };
15083
15198
  return {
@@ -15102,17 +15217,17 @@ function heldIo(fs, nodes, changed) {
15102
15217
  ),
15103
15218
  async stat(path) {
15104
15219
  const st = await fs.stat(path);
15105
- const held = st.isFile ? node(path) : void 0;
15220
+ const held = st.isFile ? await node(path) : void 0;
15106
15221
  return held ? { ...st, size: await held.serial(() => held.size()) } : st;
15107
15222
  },
15108
15223
  async readFileBuffer(path) {
15109
- const held = node(path);
15224
+ const held = await node(path);
15110
15225
  if (!held) return fs.readFileBuffer(path);
15111
15226
  return held.serial(async () => held.pread(await held.size(), 0));
15112
15227
  },
15113
15228
  ...ranged2 ? {
15114
- pread: (path, at, n, version) => {
15115
- const held = node(path);
15229
+ pread: async (path, at, n, version) => {
15230
+ const held = await node(path);
15116
15231
  return held ? held.serial(() => held.pread(n, at)) : ranged2.pread(path, at, n, version);
15117
15232
  },
15118
15233
  pwrite: (path, at, bytes, transfer) => write(
@@ -15160,9 +15275,9 @@ function keepingOpen(fs, nodes, changed = () => {
15160
15275
  nodes.renamed(a, b);
15161
15276
  });
15162
15277
  },
15163
- writeFile: (path, content, owner) => {
15278
+ writeFile: async (path, content, owner) => {
15164
15279
  const at = fs.resolvePath("/", path);
15165
- const held = nodes.holding(at);
15280
+ const held = await heldAt(fs, nodes, at);
15166
15281
  if (!held) return nodes.onEntry(at, () => fs.writeFile(path, content, owner));
15167
15282
  const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
15168
15283
  return held.serial(async () => {
@@ -15170,6 +15285,7 @@ function keepingOpen(fs, nodes, changed = () => {
15170
15285
  await held.pwrite(bytes, 0);
15171
15286
  await held.flush();
15172
15287
  changed(at);
15288
+ if (held.path !== at) changed(held.path);
15173
15289
  });
15174
15290
  },
15175
15291
  createExclusive: async (path, owner) => nodes.createExclusive(await nodes.entryKey(fs.resolvePath("/", path)), owner),
@@ -20212,7 +20328,7 @@ function usersMode(mode = "off") {
20212
20328
  function tagOption(tag) {
20213
20329
  if (tag !== void 0 && typeof tag !== "string") throw new TypeError("a tag must be a string");
20214
20330
  }
20215
- function baseName(path) {
20331
+ function baseName2(path) {
20216
20332
  return path.slice(path.lastIndexOf("/") + 1);
20217
20333
  }
20218
20334
  function packageRootOf(path, roots) {
@@ -20255,7 +20371,7 @@ function appendedRoots(given) {
20255
20371
  }
20256
20372
  function plainEnv(words2) {
20257
20373
  const next2 = words2[1];
20258
- if (baseName(words2[0] ?? "") !== "env" || next2 === void 0) return false;
20374
+ if (baseName2(words2[0] ?? "") !== "env" || next2 === void 0) return false;
20259
20375
  return next2 === "-S" || !next2.startsWith("-") && !next2.includes("=");
20260
20376
  }
20261
20377
  function shebangCommand(words2) {
@@ -20271,9 +20387,9 @@ function shebangCommand(words2) {
20271
20387
  function interpreter(head) {
20272
20388
  const words2 = shebang(head);
20273
20389
  if (!words2) return "node";
20274
- if (baseName(words2[0]) === "env") words2.shift();
20390
+ if (baseName2(words2[0]) === "env") words2.shift();
20275
20391
  if (words2[0] === "-S") words2.shift();
20276
- return baseName(words2[0] ?? "");
20392
+ return baseName2(words2[0] ?? "");
20277
20393
  }
20278
20394
  function expandDefaults(defaults, env) {
20279
20395
  const out = {};
@@ -20447,6 +20563,9 @@ var Launcher = class {
20447
20563
  this.sealed.userOf = async (uid) => (await this.users.lookup(uid))?.name;
20448
20564
  if (this.owners) options.fs.trackOwners?.();
20449
20565
  this.watchers.tap((paths) => this.changes.publish(...paths));
20566
+ this.watchers.onReshape(() => {
20567
+ for (const nodes of this.openFiles) nodes.reshaped();
20568
+ });
20450
20569
  this.mounts = new MountTable({
20451
20570
  open: (type, spec) => this.driver(type, spec),
20452
20571
  busy: (target) => [...this.openFiles].some((nodes) => nodes.holds(target)) || heldUnder(this.held, target),
@@ -20454,6 +20573,7 @@ var Launcher = class {
20454
20573
  if (paths.some((path) => this.pnpmChain.has(path))) this.pnpmStale();
20455
20574
  this.watchers.changed(...paths);
20456
20575
  },
20576
+ reshaped: () => this.watchers.reshaped(),
20457
20577
  owners: this.owners
20458
20578
  });
20459
20579
  this.mounted = this.mounts.wrap(options.fs);
@@ -20711,22 +20831,22 @@ var Launcher = class {
20711
20831
  const head = await this.head(glue);
20712
20832
  if (!head) return void 0;
20713
20833
  if (isWasm(head)) {
20714
- const owner2 = await this.wasmOwner(glue, baseName(file).replace(/\.wasm$/, ""), scope);
20834
+ const owner2 = await this.wasmOwner(glue, baseName2(file).replace(/\.wasm$/, ""), scope);
20715
20835
  if (owner2) return { ...targetOf(owner2), ...asked };
20716
20836
  return {
20717
20837
  abi: "wasi",
20718
20838
  glue,
20719
20839
  wasm: glue,
20720
- argv0: baseName(argv0 || file).replace(/\.wasm$/, ""),
20840
+ argv0: baseName2(argv0 || file).replace(/\.wasm$/, ""),
20721
20841
  ...asked
20722
20842
  };
20723
20843
  }
20724
20844
  if (interpreter(head) !== "node") return void 0;
20725
20845
  const wasm = modulePath(glue);
20726
20846
  if (await this.base.exists(wasm)) {
20727
- return { abi: "emscripten", glue, wasm, argv0: baseName(argv0 || file), ...asked };
20847
+ return { abi: "emscripten", glue, wasm, argv0: baseName2(argv0 || file), ...asked };
20728
20848
  }
20729
- const owner = await this.glueOwner(glue, baseName(file), scope);
20849
+ const owner = await this.glueOwner(glue, baseName2(file), scope);
20730
20850
  return owner && { ...targetOf(owner), ...asked };
20731
20851
  }
20732
20852
  async glueOwner(glue, name2, scope) {
@@ -20736,11 +20856,11 @@ var Launcher = class {
20736
20856
  return owners.find((c) => c.name === name2) ?? owners[0];
20737
20857
  }
20738
20858
  async wasmOwner(wasm, name2, scope) {
20739
- const leaf = baseName(wasm);
20859
+ const leaf = baseName2(wasm);
20740
20860
  const real = await physicalPathAsync(this.base, wasm);
20741
20861
  const owners = [];
20742
20862
  for (const command of (await this.commands(scope)).values()) {
20743
- if (command.abi !== "wasi" || command.script || baseName(command.wasm) !== leaf) continue;
20863
+ if (command.abi !== "wasi" || command.script || baseName2(command.wasm) !== leaf) continue;
20744
20864
  if (command.wasm === wasm || await physicalPathAsync(this.base, command.wasm) === real)
20745
20865
  owners.push(command);
20746
20866
  }
@@ -21804,7 +21924,7 @@ var Launcher = class {
21804
21924
  const path = normalizePath(given);
21805
21925
  if (!await this.base.exists(path)) return void 0;
21806
21926
  if (path === "/") return path;
21807
- return joinName(await realDir(this.base, parentOf4(path)), baseName(path));
21927
+ return joinName(await realDir(this.base, parentOf4(path)), baseName2(path));
21808
21928
  }
21809
21929
  pnpmReady() {
21810
21930
  if (this.pnpmMade !== void 0) return this.pnpmMade;