@ai-ecoverse/slicc-kernel 1.37.0 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -418,7 +418,7 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
418
418
 
419
419
  ### Exec
420
420
 
421
- 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: the kernel keeps one copy for all holders, shared by their descriptors (writes through one are seen through the others), and frees it when the last one closes. 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.
421
+ 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: the kernel keeps one copy for all holders, shared by their descriptors (writes through one are seen through the others), and frees it when the last one closes. That holds for files a program opened in its own worker too: every open, read-only ones included, is known to the kernel. When another process removes such a file, or renames another file over it, 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 that holder's reads and writes of the old path from there 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. 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. Where Web Locks are missing (the Node entry), nothing is swept. 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.
422
422
 
423
423
  - **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).
424
424
  - **WASIX:** `proc_exec` does this by itself.
@@ -4878,13 +4878,193 @@ function withCommandDirs(fs, names) {
4878
4878
  };
4879
4879
  }
4880
4880
 
4881
+ // src/fs/unlinked.ts
4882
+ var UNLINKED_DIR = ".slicc-unlinked";
4883
+ function webLocks() {
4884
+ return globalThis.navigator?.locks;
4885
+ }
4886
+ var within3 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
4887
+ var parentOf3 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
4888
+ var lockName = (instance) => `slicc-unlinked:${instance}`;
4889
+ var dirOf = (root) => root === "/" ? `/${UNLINKED_DIR}` : `${root}/${UNLINKED_DIR}`;
4890
+ var PathGate = class {
4891
+ tails = /* @__PURE__ */ new Map();
4892
+ async acquire(path) {
4893
+ const prev = this.tails.get(path) ?? Promise.resolve();
4894
+ const { promise, resolve } = Promise.withResolvers();
4895
+ const tail = prev.then(() => promise);
4896
+ this.tails.set(path, tail);
4897
+ await prev;
4898
+ return () => {
4899
+ resolve();
4900
+ if (this.tails.get(path) === tail) this.tails.delete(path);
4901
+ };
4902
+ }
4903
+ async run(path, op) {
4904
+ const release = await this.acquire(path);
4905
+ try {
4906
+ return await op();
4907
+ } finally {
4908
+ release();
4909
+ }
4910
+ }
4911
+ };
4912
+ var UnlinkedKeeper = class {
4913
+ fs;
4914
+ holders;
4915
+ rootOf;
4916
+ locks;
4917
+ busy;
4918
+ waiting = /* @__PURE__ */ new Set();
4919
+ instance = crypto.randomUUID();
4920
+ gate = new PathGate();
4921
+ queue = Promise.resolve();
4922
+ count = 0;
4923
+ constructor(fs, holders, rootOf, locks, busy = () => false) {
4924
+ this.fs = fs;
4925
+ this.holders = holders;
4926
+ this.rootOf = rootOf;
4927
+ this.locks = locks;
4928
+ this.busy = busy;
4929
+ void locks?.request(lockName(this.instance), () => new Promise(() => void 0));
4930
+ }
4931
+ serial(op) {
4932
+ const next = this.queue.then(op);
4933
+ this.queue = next.catch(() => void 0);
4934
+ return next;
4935
+ }
4936
+ wrap() {
4937
+ const fs = this.fs;
4938
+ const keeper = this;
4939
+ const visible = (names) => names.filter((name2) => name2 !== UNLINKED_DIR);
4940
+ return {
4941
+ ...fs,
4942
+ async rm(path, options) {
4943
+ const target = fs.resolvePath("/", path);
4944
+ const pinned = await keeper.pin(target);
4945
+ if (pinned.moved.has(target)) pinned.commit();
4946
+ else await keeper.settle(pinned, () => fs.rm(path, options));
4947
+ },
4948
+ async rename(from, to) {
4949
+ const pinned = await keeper.pin(fs.resolvePath("/", to));
4950
+ await keeper.settle(pinned, () => fs.rename(from, to));
4951
+ },
4952
+ readdir: async (path) => visible(await fs.readdir(path)),
4953
+ readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR)
4954
+ };
4955
+ }
4956
+ async settle(pinned, op) {
4957
+ try {
4958
+ await op();
4959
+ } catch (err) {
4960
+ await pinned.rollback();
4961
+ throw err;
4962
+ }
4963
+ pinned.commit();
4964
+ }
4965
+ async pin(target) {
4966
+ const held = /* @__PURE__ */ new Map();
4967
+ for (const holder of this.holders) {
4968
+ if (holder.owns(target)) continue;
4969
+ for (const path of holder.opens.keys()) {
4970
+ if (!within3(path, target) || holder.unlinked.has(path)) continue;
4971
+ held.set(path, [...held.get(path) ?? [], holder]);
4972
+ }
4973
+ }
4974
+ const paths = [...held.keys()].sort();
4975
+ const releases = [];
4976
+ for (const path of paths) releases.push(await this.gate.acquire(path));
4977
+ const hidden = /* @__PURE__ */ new Map();
4978
+ for (const path of paths) {
4979
+ const at = await this.hide(path);
4980
+ if (at !== void 0) hidden.set(path, at);
4981
+ }
4982
+ const done4 = () => {
4983
+ for (const release of releases) release();
4984
+ };
4985
+ return {
4986
+ moved: new Set(hidden.keys()),
4987
+ commit: () => {
4988
+ for (const [path, at] of hidden) {
4989
+ const holders = held.get(path);
4990
+ const file = this.keptFile(at, { refs: holders.length });
4991
+ for (const holder of holders) holder.unlinked.set(path, file);
4992
+ }
4993
+ done4();
4994
+ },
4995
+ rollback: async () => {
4996
+ for (const [path, at] of hidden) {
4997
+ await this.serial(async () => {
4998
+ await Promise.allSettled([this.fs.rename(at, path)]);
4999
+ await this.tidy(parentOf3(at));
5000
+ });
5001
+ }
5002
+ done4();
5003
+ }
5004
+ };
5005
+ }
5006
+ keptFile(hidden, entry) {
5007
+ return {
5008
+ hidden,
5009
+ release: () => {
5010
+ if (--entry.refs === 0) void this.serial(() => this.drop(hidden));
5011
+ }
5012
+ };
5013
+ }
5014
+ hide(path) {
5015
+ return this.serial(async () => {
5016
+ const dir = dirOf(this.rootOf(path));
5017
+ const hidden = `${dir}/${this.instance}.${++this.count}`;
5018
+ try {
5019
+ await this.fs.mkdir(dir, { recursive: true });
5020
+ await this.fs.rename(path, hidden);
5021
+ return hidden;
5022
+ } catch {
5023
+ await this.tidy(dir);
5024
+ return void 0;
5025
+ }
5026
+ });
5027
+ }
5028
+ closed(path) {
5029
+ if (this.waiting.delete(path)) void this.serial(() => this.drop(path));
5030
+ }
5031
+ async drop(hidden) {
5032
+ if (this.busy(hidden)) {
5033
+ this.waiting.add(hidden);
5034
+ return;
5035
+ }
5036
+ await this.fs.rm(hidden).catch(() => void 0);
5037
+ await this.tidy(parentOf3(hidden));
5038
+ }
5039
+ async tidy(dir) {
5040
+ const left = await this.fs.readdir(dir).catch(() => [UNLINKED_DIR]);
5041
+ if (left.length === 0) await this.fs.rm(dir, { recursive: true }).catch(() => void 0);
5042
+ }
5043
+ sweep(root) {
5044
+ const query = this.locks?.query?.bind(this.locks);
5045
+ if (!query) return Promise.resolve();
5046
+ return this.serial(async () => {
5047
+ const dir = dirOf(root);
5048
+ const names = await this.fs.readdir(dir).catch(() => void 0);
5049
+ if (!names) return;
5050
+ const held = new Set(((await query()).held ?? []).map((lock) => lock.name));
5051
+ for (const name2 of names) {
5052
+ const owner = name2.slice(0, name2.lastIndexOf("."));
5053
+ if (owner === this.instance || held.has(lockName(owner))) continue;
5054
+ await this.fs.rm(`${dir}/${name2}`, { recursive: true }).catch(() => void 0);
5055
+ }
5056
+ await this.tidy(dir);
5057
+ });
5058
+ }
5059
+ };
5060
+
4881
5061
  // src/fs/watch.ts
4882
5062
  var WATCH_BATCH_LIMIT = 1e3;
4883
5063
  var under2 = (path, dir) => dir === "/" || path.startsWith(`${dir}/`);
4884
- var parentOf3 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
5064
+ var parentOf4 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
4885
5065
  function touches(watcher, changed) {
4886
5066
  return watcher.paths.some(
4887
- (watched) => changed === watched || under2(watched, changed) || (watcher.recursive ? under2(changed, watched) : parentOf3(changed) === watched)
5067
+ (watched) => changed === watched || under2(watched, changed) || (watcher.recursive ? under2(changed, watched) : parentOf4(changed) === watched)
4888
5068
  );
4889
5069
  }
4890
5070
  var FsWatchers = class {
@@ -5266,7 +5446,7 @@ function parseSize(text) {
5266
5446
  if (!match) throw errnoError("EINVAL", `not a size: ${text}`);
5267
5447
  return Math.floor(Number(match[1]) * UNITS[match[2].toLowerCase()]);
5268
5448
  }
5269
- var within3 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
5449
+ var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
5270
5450
  var HeldSet = class extends Set {
5271
5451
  idle;
5272
5452
  constructor(idle) {
@@ -5279,18 +5459,47 @@ var HeldSet = class extends Set {
5279
5459
  }
5280
5460
  delete(holds) {
5281
5461
  const removed = super.delete(holds);
5282
- for (const path of holds.keys()) this.idle(path);
5462
+ for (const path of /* @__PURE__ */ new Set([...holds.keys(), ...holds.opens.keys()])) this.idle(path);
5463
+ for (const kept of holds.unlinked.values()) kept.release();
5464
+ holds.unlinked.clear();
5283
5465
  return removed;
5284
5466
  }
5285
5467
  };
5286
5468
  function heldUnder(held, target) {
5287
- for (const paths of held) for (const path of paths.keys()) if (within3(path, target)) return true;
5469
+ for (const paths of held) for (const path of paths.keys()) if (within4(path, target)) return true;
5288
5470
  return false;
5289
5471
  }
5290
5472
  var HeldPaths = class extends Map {
5291
5473
  revoked = /* @__PURE__ */ new Set();
5474
+ opens = /* @__PURE__ */ new Map();
5475
+ unlinked = /* @__PURE__ */ new Map();
5476
+ aliases = /* @__PURE__ */ new Map();
5477
+ owning;
5292
5478
  onIdle;
5293
- hold(path, on) {
5479
+ keyOf(path) {
5480
+ return this.aliases.get(path) ?? path;
5481
+ }
5482
+ keptAt(path) {
5483
+ return this.unlinked.get(this.keyOf(path));
5484
+ }
5485
+ holding(path) {
5486
+ return this.opens.has(this.keyOf(path));
5487
+ }
5488
+ hold(path, on, open = false, real) {
5489
+ if (open) {
5490
+ const key = real ?? this.keyOf(path);
5491
+ if (key !== path) this.aliases.set(path, key);
5492
+ const count2 = (this.opens.get(key) ?? 0) + (on ? 1 : -1);
5493
+ if (count2 > 0) this.opens.set(key, count2);
5494
+ else {
5495
+ this.opens.delete(key);
5496
+ this.unlinked.get(key)?.release();
5497
+ this.unlinked.delete(key);
5498
+ for (const [alias, target] of this.aliases) if (target === key) this.aliases.delete(alias);
5499
+ this.onIdle?.(key);
5500
+ }
5501
+ return;
5502
+ }
5294
5503
  const count = (this.get(path) ?? 0) + (on ? 1 : -1);
5295
5504
  if (count > 0) this.set(path, count);
5296
5505
  else {
@@ -5299,23 +5508,40 @@ var HeldPaths = class extends Map {
5299
5508
  this.onIdle?.(path);
5300
5509
  }
5301
5510
  }
5511
+ async own(path, op) {
5512
+ this.owning = path;
5513
+ try {
5514
+ return await op();
5515
+ } finally {
5516
+ this.owning = void 0;
5517
+ }
5518
+ }
5519
+ owns(path) {
5520
+ return this.owning === path;
5521
+ }
5302
5522
  revoke(prefix) {
5303
- for (const path of this.keys()) if (within3(path, prefix)) this.revoked.add(path);
5523
+ for (const path of this.keys()) if (within4(path, prefix)) this.revoked.add(path);
5304
5524
  }
5305
5525
  renamed(from, to) {
5306
5526
  for (const [path, count] of [...this]) {
5307
- if (!within3(path, from)) continue;
5527
+ if (!within4(path, from)) continue;
5308
5528
  const moved2 = to + path.slice(from.length);
5309
5529
  this.delete(path);
5310
5530
  this.set(moved2, (this.get(moved2) ?? 0) + count);
5311
5531
  if (this.revoked.delete(path)) this.revoked.add(moved2);
5312
5532
  }
5533
+ for (const [path, count] of [...this.opens]) {
5534
+ if (!within4(path, from)) continue;
5535
+ const moved2 = to + path.slice(from.length);
5536
+ this.opens.delete(path);
5537
+ this.opens.set(moved2, (this.opens.get(moved2) ?? 0) + count);
5538
+ }
5313
5539
  }
5314
5540
  isRevoked(path) {
5315
5541
  return this.revoked.has(path);
5316
5542
  }
5317
5543
  };
5318
- var parentOf4 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
5544
+ var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
5319
5545
  var readonly = (mount) => mount.caps.readonly === true || mount.options.ro !== void 0;
5320
5546
  function statOf(attr, path, mount) {
5321
5547
  const mtime = new Date(attr.mtime);
@@ -5404,7 +5630,7 @@ var MountTable = class {
5404
5630
  locate(path) {
5405
5631
  let best;
5406
5632
  for (const mount of this.mounts.values()) {
5407
- if (within3(path, mount.target) && (!best || mount.target.length > best.target.length))
5633
+ if (within4(path, mount.target) && (!best || mount.target.length > best.target.length))
5408
5634
  best = mount;
5409
5635
  }
5410
5636
  if (!best) return void 0;
@@ -5543,7 +5769,7 @@ var MountTable = class {
5543
5769
  async commitUnder(prefix) {
5544
5770
  const at = normalizePath(prefix);
5545
5771
  await Promise.all(
5546
- [...this.sessions.keys()].filter((p) => within3(p, at)).map((p) => this.commit(p))
5772
+ [...this.sessions.keys()].filter((p) => within4(p, at)).map((p) => this.commit(p))
5547
5773
  );
5548
5774
  }
5549
5775
  async commitOverlapping(real, offset, length2) {
@@ -5577,10 +5803,10 @@ var MountTable = class {
5577
5803
  this.deps.changed?.(...full);
5578
5804
  }
5579
5805
  forget(mount, path) {
5580
- for (const key of [...mount.attrs.keys()]) if (within3(key, path)) mount.attrs.delete(key);
5806
+ for (const key of [...mount.attrs.keys()]) if (within4(key, path)) mount.attrs.delete(key);
5581
5807
  mount.lists.delete(path);
5582
- mount.lists.delete(parentOf4(path));
5583
- for (const key of [...mount.lists.keys()]) if (within3(key, path)) mount.lists.delete(key);
5808
+ mount.lists.delete(parentOf5(path));
5809
+ for (const key of [...mount.lists.keys()]) if (within4(key, path)) mount.lists.delete(key);
5584
5810
  }
5585
5811
  async getattr(mount, path, rel2) {
5586
5812
  if (mount.conn.failure) throw mount.conn.failure;
@@ -5645,7 +5871,7 @@ var MountFs = class {
5645
5871
  if (attr.kind !== "symlink") return real;
5646
5872
  if (hops >= MAX_LINKS4) throw errnoError("ELOOP", path);
5647
5873
  const target = await found.mount.conn.call({ op: "readlink", path: found.rel });
5648
- return this.follow(this.base.resolvePath(parentOf4(real), target), hops + 1, create);
5874
+ return this.follow(this.base.resolvePath(parentOf5(real), target), hops + 1, create);
5649
5875
  }
5650
5876
  async open(mount, rel2, write) {
5651
5877
  return await mount.conn.call({
@@ -6159,6 +6385,8 @@ async function ranged(fs, path, req) {
6159
6385
  else await ops.truncate(path, req.length ?? 0);
6160
6386
  return done;
6161
6387
  }
6388
+ var UNLINKED_OPS = /* @__PURE__ */ new Set(["read", "pread", "pwrite", "truncate"]);
6389
+ var OWN_OPS = /* @__PURE__ */ new Set(["rm", "unlink", "rmdir", "rename"]);
6162
6390
  async function dispatchSyncFs(req) {
6163
6391
  const entry = resolveSyncFsToken(req.token);
6164
6392
  if (!entry) return { ok: false, errno: "EACCES", message: "sync-fs: unknown or revoked token" };
@@ -6167,11 +6395,22 @@ async function dispatchSyncFs(req) {
6167
6395
  const path = fs.resolvePath(cwd, req.path);
6168
6396
  if (req.op === "statfs") return json2(await entry.statfs?.(path) ?? await storageUsage());
6169
6397
  if (req.op === "hold") {
6170
- entry.hold?.(path, req.mode === 1);
6398
+ const [on, open] = [((req.mode ?? 0) & 1) === 1, ((req.mode ?? 0) & 2) === 2];
6399
+ const real = on && open ? await fs.realpath?.(path, true).catch(() => path) : void 0;
6400
+ entry.hold?.(path, on, open, real);
6171
6401
  return done;
6172
6402
  }
6173
6403
  if (entry.revoked?.(path)) return { ok: false, errno: "EIO", message: `${path} was unmounted` };
6174
- return await run(entry, path, req);
6404
+ if (req.op === "write" && req.mode === 1 || UNLINKED_OPS.has(req.op)) {
6405
+ const serve = async () => {
6406
+ const kept = entry.unlinked?.(path);
6407
+ return run(entry, kept && !await fs.exists(path) ? kept.hidden : path, req);
6408
+ };
6409
+ return await (entry.gate ? entry.gate(path, serve) : serve());
6410
+ }
6411
+ if (!entry.own || !OWN_OPS.has(req.op)) return await run(entry, path, req);
6412
+ const target = req.op === "rename" ? fs.resolvePath(cwd, req.arg2 ?? "") : path;
6413
+ return await entry.own(target, () => run(entry, path, req));
6175
6414
  } catch (err) {
6176
6415
  return toErrno(err);
6177
6416
  }
@@ -7559,7 +7798,7 @@ function isMissing(err) {
7559
7798
  if (err?.code === "ENOENT") return true;
7560
7799
  return err instanceof Error && err.message.startsWith("ENOENT");
7561
7800
  }
7562
- function within4(path, root) {
7801
+ function within5(path, root) {
7563
7802
  return path === root || path.startsWith(root.endsWith("/") ? root : `${root}/`);
7564
7803
  }
7565
7804
  var VfsNode = class {
@@ -7764,7 +8003,7 @@ var VfsNodes = class {
7764
8003
  return run2;
7765
8004
  }
7766
8005
  async entryKey(path) {
7767
- const parent = parentOf5(path);
8006
+ const parent = parentOf6(path);
7768
8007
  const dir = await this.real(parent, true, () => realDir(this.fs, parent));
7769
8008
  return joinName(dir, path.slice(path.lastIndexOf("/") + 1));
7770
8009
  }
@@ -7812,12 +8051,12 @@ var VfsNodes = class {
7812
8051
  return node;
7813
8052
  }
7814
8053
  holds(prefix) {
7815
- for (const path of this.byPath.keys()) if (within4(path, prefix)) return true;
8054
+ for (const path of this.byPath.keys()) if (within5(path, prefix)) return true;
7816
8055
  return false;
7817
8056
  }
7818
8057
  writes(prefix) {
7819
8058
  for (const [path, node] of this.byPath)
7820
- if (node.writers > 0 && within4(path, prefix)) return true;
8059
+ if (node.writers > 0 && within5(path, prefix)) return true;
7821
8060
  return false;
7822
8061
  }
7823
8062
  closed(node, writable = false) {
@@ -7831,24 +8070,24 @@ var VfsNodes = class {
7831
8070
  this.onClosed?.(node.path);
7832
8071
  }
7833
8072
  async flush(path) {
7834
- for (const [p, node] of this.byPath) if (within4(p, path)) await node.serial(() => node.flush());
8073
+ for (const [p, node] of this.byPath) if (within5(p, path)) await node.serial(() => node.flush());
7835
8074
  }
7836
8075
  async unlinking(path) {
7837
8076
  for (const [held, node] of [...this.byPath]) {
7838
- if (!within4(held, path)) continue;
8077
+ if (!within5(held, path)) continue;
7839
8078
  await node.serial(async () => void await Promise.all([node.keep(), node.limit()]));
7840
8079
  }
7841
8080
  }
7842
8081
  revoke(prefix) {
7843
8082
  for (const [path, node] of this.byPath) {
7844
- if (!within4(path, prefix)) continue;
8083
+ if (!within5(path, prefix)) continue;
7845
8084
  node.revoke();
7846
8085
  this.byPath.delete(path);
7847
8086
  }
7848
8087
  }
7849
8088
  unlinked(path) {
7850
8089
  for (const [held, node] of [...this.byPath]) {
7851
- if (!within4(held, path)) continue;
8090
+ if (!within5(held, path)) continue;
7852
8091
  node.orphaned = true;
7853
8092
  this.byPath.delete(held);
7854
8093
  }
@@ -7857,8 +8096,8 @@ var VfsNodes = class {
7857
8096
  if (from === to) return;
7858
8097
  const moved2 = [];
7859
8098
  for (const [p, node] of this.byPath) {
7860
- if (within4(p, from)) moved2.push(node);
7861
- else if (within4(p, to)) {
8099
+ if (within5(p, from)) moved2.push(node);
8100
+ else if (within5(p, to)) {
7862
8101
  node.orphaned = true;
7863
8102
  this.byPath.delete(p);
7864
8103
  }
@@ -7881,7 +8120,7 @@ async function present(fs, path, follow = false) {
7881
8120
  }
7882
8121
  }
7883
8122
  var MAX_LINKS5 = 40;
7884
- function parentOf5(path) {
8123
+ function parentOf6(path) {
7885
8124
  return path.slice(0, path.lastIndexOf("/")) || "/";
7886
8125
  }
7887
8126
  async function linkOf(fs, path) {
@@ -7894,7 +8133,7 @@ async function realDir(fs, dir, budget = { hops: 0 }) {
7894
8133
  const part = queue.shift();
7895
8134
  if (part === ".") continue;
7896
8135
  if (part === "..") {
7897
- at = parentOf5(at);
8136
+ at = parentOf6(at);
7898
8137
  continue;
7899
8138
  }
7900
8139
  const next = at === "/" ? `/${part}` : `${at}/${part}`;
@@ -7918,7 +8157,7 @@ function joinName(dir, name2) {
7918
8157
  }
7919
8158
  async function realTarget(fs, path) {
7920
8159
  const budget = { hops: 0 };
7921
- let dir = await realDir(fs, parentOf5(path), budget);
8160
+ let dir = await realDir(fs, parentOf6(path), budget);
7922
8161
  let name2 = path.slice(path.lastIndexOf("/") + 1);
7923
8162
  for (; ; ) {
7924
8163
  const link2 = await linkOf(fs, joinName(dir, name2));
@@ -7926,13 +8165,13 @@ async function realTarget(fs, path) {
7926
8165
  if (budget.hops >= MAX_LINKS5) throw fsError("ELOOP", path);
7927
8166
  budget.hops++;
7928
8167
  const target = link2.startsWith("/") ? link2 : joinName(dir, link2);
7929
- dir = await realDir(fs, parentOf5(target), budget);
8168
+ dir = await realDir(fs, parentOf6(target), budget);
7930
8169
  name2 = target.slice(target.lastIndexOf("/") + 1);
7931
8170
  }
7932
8171
  return joinName(dir, name2);
7933
8172
  }
7934
8173
  async function targetDir(fs, path) {
7935
- return parentOf5(await realTarget(fs, path).catch(() => path));
8174
+ return parentOf6(await realTarget(fs, path).catch(() => path));
7936
8175
  }
7937
8176
  async function refuseReadonly(fs, path, flags, opts = {}) {
7938
8177
  const writes = (flags & O_ACCMODE) !== 0 || opts.truncate === true;
@@ -8321,7 +8560,7 @@ var WasmProcess = class {
8321
8560
  const fd = this.fds.install(heldFile(), 3);
8322
8561
  try {
8323
8562
  const nodes = this.nodes;
8324
- const path = req.orphan ? req.path : req.exclusive ? await nodes.entryKey(req.path) : await nodes.targetKey(req.path);
8563
+ const path = req.orphan ? req.path : req.exclusive ? await nodes.entryKey(req.path) : req.create ? await nodes.targetKey(req.path) : await this.keptOr(await nodes.targetKey(req.path));
8325
8564
  const open = { ...req, path };
8326
8565
  if (req.exclusive) {
8327
8566
  await nodes.createExclusive(path);
@@ -8477,6 +8716,11 @@ var WasmProcess = class {
8477
8716
  return { ok: true, kind: "json", json: await file.stat() };
8478
8717
  }
8479
8718
  }
8719
+ keptPath;
8720
+ async keptOr(path) {
8721
+ const kept = this.keptPath?.(path);
8722
+ return kept !== void 0 && !await present(this.options.fs, path, true) ? kept : path;
8723
+ }
8480
8724
  async promote(req) {
8481
8725
  if (!this.fds.get(req.fd).file.held) throw new KernelError("EBADF");
8482
8726
  if (req.share !== void 0) {
@@ -8484,7 +8728,7 @@ var WasmProcess = class {
8484
8728
  return;
8485
8729
  }
8486
8730
  if (req.path === void 0) throw new KernelError("EINVAL");
8487
- const path = req.orphan ? req.path : await this.nodes.targetKey(req.path);
8731
+ const path = req.orphan ? req.path : await this.keptOr(await this.nodes.targetKey(req.path));
8488
8732
  if (!this.fds.get(req.fd).file.held) throw new KernelError("EBADF");
8489
8733
  const file = vfsFile(
8490
8734
  this.options.fs,
@@ -8843,6 +9087,19 @@ function inherited(opts) {
8843
9087
  ...opts.umask !== void 0 ? { umask: opts.umask } : {}
8844
9088
  };
8845
9089
  }
9090
+ function holdingToken(opts, holds) {
9091
+ return mintSyncFsToken({
9092
+ fs: opts.fs,
9093
+ cwd: opts.cwd,
9094
+ ...opts.statfs ? { statfs: opts.statfs } : {},
9095
+ hold: (path, on, open, real) => holds.hold(path, on, open, real),
9096
+ unlinked: (path) => holds.keptAt(path),
9097
+ gate: (path, op) => opts.unlinkGate && holds.holding(path) ? opts.unlinkGate.run(holds.keyOf(path), op) : op(),
9098
+ own: (path, op) => holds.own(path, op),
9099
+ revoked: (path) => holds.isRevoked(path),
9100
+ renamed: (from, to) => holds.renamed(from, to)
9101
+ });
9102
+ }
8846
9103
  function spawnWasmProcess(opts) {
8847
9104
  const sab = new SharedArrayBuffer(SAB_HEADER_BYTES + SAB_DEFAULT_WINDOW_BYTES);
8848
9105
  const header2 = new Int32Array(sab, 0, SAB_HEADER_I32);
@@ -8887,14 +9144,8 @@ function spawnWasmProcess(opts) {
8887
9144
  });
8888
9145
  const holds = new HeldPaths();
8889
9146
  opts.held?.add(holds);
8890
- const token = mintSyncFsToken({
8891
- fs: opts.fs,
8892
- cwd: opts.cwd,
8893
- ...opts.statfs ? { statfs: opts.statfs } : {},
8894
- hold: (path, on) => holds.hold(path, on),
8895
- revoked: (path) => holds.isRevoked(path),
8896
- renamed: (from, to) => holds.renamed(from, to)
8897
- });
9147
+ const token = holdingToken(opts, holds);
9148
+ process.keptPath = (path) => holds.keptAt(path)?.hidden;
8898
9149
  const worker = opts.createWorker();
8899
9150
  const dying = new Dying();
8900
9151
  const dispatch2 = (req) => dying.serving(() => isWasmSyscall(req) ? process.syscall(req) : dispatchSyncFs(req));
@@ -10534,6 +10785,7 @@ var Launcher = class {
10534
10785
  deciding = /* @__PURE__ */ new Map();
10535
10786
  identities = { asked: 0, timedOut: 0 };
10536
10787
  nodes;
10788
+ unlinked;
10537
10789
  held = new HeldSet((path) => this.released(path));
10538
10790
  processMounts;
10539
10791
  booting = new AbortController();
@@ -10578,7 +10830,15 @@ var Launcher = class {
10578
10830
  const fs = withCommandDirs(this.base, async () => new Set((await this.commands()).keys()));
10579
10831
  this.nodes = new VfsNodes(fs, (path) => this.released(path));
10580
10832
  this.openFiles.add(this.nodes);
10581
- this.fs = keepingOpen(fs, this.nodes);
10833
+ this.unlinked = new UnlinkedKeeper(
10834
+ fs,
10835
+ this.held,
10836
+ (path) => this.mountRoot(path),
10837
+ webLocks(),
10838
+ (path) => [...this.openFiles].some((nodes) => nodes.holds(path))
10839
+ );
10840
+ this.fs = keepingOpen(this.unlinked.wrap(), this.nodes);
10841
+ void this.unlinked.sweep("/");
10582
10842
  this.watchers.watch([this.modulesDir, this.pnpmHome], { recursive: true }, () => {
10583
10843
  this.catalog = void 0;
10584
10844
  this.binfmts = void 0;
@@ -10788,6 +11048,7 @@ var Launcher = class {
10788
11048
  openFiles: this.openFiles,
10789
11049
  nodes: this.nodes,
10790
11050
  held: this.held,
11051
+ unlinkGate: this.unlinked.gate,
10791
11052
  statfs: (path) => this.mounts.statfs(path),
10792
11053
  mounts: () => this.mounts.list(),
10793
11054
  mount: (call, signal) => this.processMount(pid, call, signal),
@@ -10945,6 +11206,7 @@ var Launcher = class {
10945
11206
  }
10946
11207
  released(path) {
10947
11208
  const at = normalizePath(path);
11209
+ this.unlinked.closed(at);
10948
11210
  const open = [...this.openFiles].some((nodes) => nodes.writes(at)) || heldUnder(this.held, at);
10949
11211
  if (!open) void this.mounts.commit(at).catch(() => void 0);
10950
11212
  }
@@ -11040,10 +11302,23 @@ var Launcher = class {
11040
11302
  }
11041
11303
  };
11042
11304
  }
11043
- mount(spec) {
11044
- if (spec.type !== "fsa" || spec.source.startsWith("fsa:"))
11045
- return this.mounts.mount(spec, this.fs);
11046
- return this.mounts.mount({ ...spec, source: `fsa:${crypto.randomUUID()}` }, this.fs);
11305
+ async mount(spec) {
11306
+ const fsa = spec.type === "fsa" && !spec.source.startsWith("fsa:");
11307
+ const entry = await this.mounts.mount(
11308
+ fsa ? { ...spec, source: `fsa:${crypto.randomUUID()}` } : spec,
11309
+ this.fs
11310
+ );
11311
+ void this.unlinked.sweep(entry.target);
11312
+ return entry;
11313
+ }
11314
+ mountRoot(path) {
11315
+ let root = "/";
11316
+ for (const { target } of this.mounts.list()) {
11317
+ if (target.length > root.length && (path === target || path.startsWith(`${target}/`))) {
11318
+ root = target;
11319
+ }
11320
+ }
11321
+ return root;
11047
11322
  }
11048
11323
  async insert(target, handle, source) {
11049
11324
  const at = normalizePath(target);
@@ -11057,6 +11332,7 @@ var Launcher = class {
11057
11332
  await this.media.put(slot.id, handle);
11058
11333
  slot.medium.insert(handle);
11059
11334
  slot.served.invalidate(true);
11335
+ void this.unlinked.sweep(at);
11060
11336
  }
11061
11337
  umount(target, detach = false) {
11062
11338
  const at = normalizePath(target);