@ai-ecoverse/slicc-kernel 1.146.3 → 1.146.4

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
@@ -754,6 +754,10 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
754
754
 
755
755
  **Renaming a directory on the OPFS root** ([#1122](https://github.com/ai-ecoverse/slicc-kernel/issues/1122)): Chrome's OPFS can move a file but not a directory. So the kernel renames a directory by creating the new tree and moving each file into it (copying a file only if it can't move, e.g. while it's open elsewhere), then removing the old tree. Within the kernel the rename is atomic to readers: a lookup or listing under the old or the new name, or of either parent, waits until the whole tree has moved. So a stage-then-rename updater (pnpm's, an atomic config write) is never seen half there. Another tab sharing the root sees it all at once too (see below). A root whose directories can move (the Node memory root) renames them in one step. **If the page reloads or crashes mid-rename**, some files are already in the new tree and the rest are still in the old one. So before the first move, the kernel writes the rename's intent (`{ from, to }`) to its metadata store, and clears it after the last. At the next boot, when no other tab shares the root (the same rule as for the boot's other repair of stale metadata), the kernel finishes every recorded rename: it moves what's left into the new tree, removes the old name, and clears the record. The moved directory and its files keep their owner, mode and inode, as after a rename that wasn't stopped. While another tab shares the root, a kernel finishes only renames whose kernel is gone ([#1141](https://github.com/ai-ecoverse/slicc-kernel/issues/1141)): the renaming kernel holds a Web Lock (`slicc-rename:<id>`, named in the record) for the whole rename, and the browser releases it when that tab closes or its worker ends. A kernel that boots beside a peer, and every kernel that sees a peer go, finishes each recorded rename whose lock is free, taking that lock while it does, so two survivors never replay the same rename and a live kernel's rename is never touched. A record without a lock (written by an older kernel, or where Web Locks are missing) waits for a boot that's alone, as before. While another tab finishes a rename, this kernel's readers wait as for its own, and so do the other tabs'. A kernel whose wait for another tab's rename ends with the lock free and the record still there finishes the rename itself, under that lock, and a boot that's alone takes a record's lock too, so a third tab that boots meanwhile waits for it.
756
756
 
757
+ **An open file stays bound to the file it opened** ([#1394](https://github.com/ai-ecoverse/slicc-kernel/issues/1394)): as on Linux, a program's open file keeps reaching the file it opened when its name is later renamed over, by a file or a symlink, by the program itself or by another process.
758
+ - **The kernel keeps it:** the program keeps the file's contents before its own rename over the name, as it already did before its own unlink, and a rename by another process sets the held file aside, as an unlink does. Reads and writes through the descriptor reach the old file, and `fstat` keeps its inode.
759
+ - **Another tab:** a tab doesn't know which files another tab holds open, so a rename over the name there can't keep the file. The descriptor then fails with `ESTALE`; it never reads or writes the file the name now names. `/proc/sys/kernel/slicc_stale_fds` counts these.
760
+
757
761
  **A rename by another process** ([#1257](https://github.com/ai-ecoverse/slicc-kernel/issues/1257)): as on Linux, where a descriptor names the file and not its path, a program's open file follows a rename of that file, or of a directory above it, by another process.
758
762
  - **The journal:** the kernel keeps the last 4096 renames it applied, each from and to a path, and counts them in the change buffer it shares with its processes. A rename in another tab on the same root reaches it through the sidecar's move notice.
759
763
  - **Catching up:** before a WASI or Emscripten program uses an open file's path (a read, a write, a seek, an `fsync`, a close, an open), it reads that count once. If another process renamed something since, and the program holds files open, it asks the kernel once for the renames of the paths it holds and moves its open files to their new names. The kernel answers only about the paths the program sent, and, under `users: 'enforce'`, drops every rename whose old or new name the program couldn't look up (search permission on each directory above it), so no process learns names in a directory it can't search. A program's own rename moves the count without asking, when nothing else came in between.
@@ -7728,7 +7728,7 @@ function satisfies(version, range2) {
7728
7728
  }
7729
7729
 
7730
7730
  // src/version.ts
7731
- var KERNEL_VERSION = true ? "1.146.3" : "0.0.0-dev";
7731
+ var KERNEL_VERSION = true ? "1.146.4" : "0.0.0-dev";
7732
7732
  var OS_RELEASE = "6.0.0-slicc";
7733
7733
  function procVersion() {
7734
7734
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -10631,16 +10631,16 @@ var FreshModules = class {
10631
10631
  if (size < MAGIC.length && this.fresh.has(normalizePath(path))) await decide(path);
10632
10632
  };
10633
10633
  }
10634
- const bound2 = /* @__PURE__ */ new WeakMap();
10634
+ const bound3 = /* @__PURE__ */ new WeakMap();
10635
10635
  return new Proxy(fs, {
10636
10636
  get(target, key, receiver) {
10637
10637
  if (Object.hasOwn(overrides, key)) return overrides[key];
10638
10638
  const value2 = Reflect.get(target, key, receiver);
10639
10639
  if (typeof value2 !== "function") return value2;
10640
- let method = bound2.get(value2);
10640
+ let method = bound3.get(value2);
10641
10641
  if (method === void 0) {
10642
10642
  method = value2.bind(target);
10643
- bound2.set(value2, method);
10643
+ bound3.set(value2, method);
10644
10644
  }
10645
10645
  return method;
10646
10646
  }
@@ -11880,6 +11880,12 @@ var HeldSet = class extends Set {
11880
11880
  holds2.onIdle = this.idle;
11881
11881
  return super.add(holds2);
11882
11882
  }
11883
+ doubt(paths, over) {
11884
+ for (const holds2 of this) {
11885
+ for (const path of paths) holds2.doubt(path, true);
11886
+ if (over !== void 0) holds2.doubt(over);
11887
+ }
11888
+ }
11883
11889
  delete(holds2) {
11884
11890
  const removed = super.delete(holds2);
11885
11891
  for (const path of /* @__PURE__ */ new Set([...holds2.keys(), ...holds2.opens.keys()])) this.idle(path);
@@ -11901,6 +11907,9 @@ var HeldPaths = class extends Map {
11901
11907
  uses = /* @__PURE__ */ new Map();
11902
11908
  lastHandle = 0;
11903
11909
  owning;
11910
+ bound = /* @__PURE__ */ new Map();
11911
+ doubted = /* @__PURE__ */ new Map();
11912
+ doubts = 0;
11904
11913
  onIdle;
11905
11914
  keyOf(path) {
11906
11915
  return this.aliases.get(path) ?? path;
@@ -11921,6 +11930,21 @@ var HeldPaths = class extends Map {
11921
11930
  handlesAt(path) {
11922
11931
  return [...this.handles].filter(([, key]) => key === path).map(([id2]) => id2);
11923
11932
  }
11933
+ doubt(over, exact = false) {
11934
+ if (this.bound.size === 0) return;
11935
+ for (const [handle, key] of this.handles) {
11936
+ if (!this.bound.has(handle) || this.keptHandles.has(handle)) continue;
11937
+ if (exact ? key === over : within6(key, over)) this.doubted.set(handle, ++this.doubts);
11938
+ }
11939
+ }
11940
+ boundIno(handle) {
11941
+ const doubt = this.doubted.get(handle);
11942
+ const ino = this.bound.get(handle);
11943
+ return doubt === void 0 || ino === void 0 ? void 0 : { ino, doubt };
11944
+ }
11945
+ trust(handle, doubt) {
11946
+ if (this.doubted.get(handle) === doubt) this.doubted.delete(handle);
11947
+ }
11924
11948
  keptFor(handle) {
11925
11949
  return this.keptHandles.get(handle);
11926
11950
  }
@@ -11957,8 +11981,16 @@ var HeldPaths = class extends Map {
11957
11981
  for (const file of [...this.uses.keys()]) this.letGo(file, true);
11958
11982
  this.keptHandles.clear();
11959
11983
  }
11960
- hold(path, on, open2 = false, real, handle) {
11961
- if (open2) return this.holdOpen(path, on, real, handle);
11984
+ hold(path, on, open2 = false, real, handle, ino) {
11985
+ if (open2) {
11986
+ const made2 = this.holdOpen(path, on, real, handle);
11987
+ if (handle !== void 0) {
11988
+ this.bound.delete(handle);
11989
+ this.doubted.delete(handle);
11990
+ }
11991
+ if (made2 !== void 0 && ino !== void 0) this.bound.set(made2, ino);
11992
+ return made2;
11993
+ }
11962
11994
  this.holdWrite(path, on);
11963
11995
  return void 0;
11964
11996
  }
@@ -13637,7 +13669,7 @@ async function holdOp(entry, path, req) {
13637
13669
  await stillThere(entry, path, req);
13638
13670
  if (req.nofollow && !await ownEntry(entry, path, real ?? path)) continue;
13639
13671
  if (!r || !r.missed(path) && !r.changedSince(path, gen) && !r.inFlight?.(path))
13640
- return json2(entry.hold?.(path, on, open2, real) ?? null);
13672
+ return json2(entry.hold?.(path, on, open2, real, void 0, req.ino) ?? null);
13641
13673
  }
13642
13674
  }
13643
13675
  entry.hold?.(path, on, open2, void 0, req.handle);
@@ -13787,10 +13819,26 @@ async function linkedElsewhere(fs, path, from) {
13787
13819
  return !(source && source.ino === st.ino && source.dev === st.dev);
13788
13820
  }
13789
13821
  async function keptOr(entry, path, handle) {
13790
- if (handle !== void 0) return entry.keptFor?.(handle)?.hidden ?? path;
13822
+ if (handle !== void 0) {
13823
+ const hidden = entry.keptFor?.(handle)?.hidden;
13824
+ if (hidden !== void 0) return hidden;
13825
+ await bound(entry, path, handle);
13826
+ return path;
13827
+ }
13791
13828
  const kept = entry.unlinked?.(path);
13792
13829
  return kept && !await entry.fs.exists(path) ? kept.hidden : path;
13793
13830
  }
13831
+ async function bound(entry, path, handle) {
13832
+ const bound3 = entry.boundIno?.(handle);
13833
+ if (bound3 === void 0) return;
13834
+ const now = await entry.fs.stat(path).catch(() => void 0);
13835
+ if (now?.ino === bound3.ino) {
13836
+ entry.trust?.(handle, bound3.doubt);
13837
+ return;
13838
+ }
13839
+ staleFds++;
13840
+ throw syncError("ESTALE", path);
13841
+ }
13794
13842
  async function keptStat(entry, req) {
13795
13843
  const hidden = entry.keptFor?.(req.handle ?? -1)?.hidden;
13796
13844
  if (hidden === void 0)
@@ -13817,6 +13865,10 @@ var served = 0;
13817
13865
  function servedFsCalls() {
13818
13866
  return served;
13819
13867
  }
13868
+ var staleFds = 0;
13869
+ function staleFdCount() {
13870
+ return staleFds;
13871
+ }
13820
13872
  async function dispatchSyncFs(req) {
13821
13873
  served++;
13822
13874
  const entry = resolveSyncFsToken(req.token);
@@ -14592,6 +14644,8 @@ var SYS_TREE = {
14592
14644
  slicc_cwd_checks: (listing) => `${listing.cwdChecks ?? 0}
14593
14645
  `,
14594
14646
  slicc_rename_ack_busy: (listing) => `${listing.renameAcks?.busy ?? 0}
14647
+ `,
14648
+ slicc_stale_fds: (listing) => `${listing.staleFds ?? 0}
14595
14649
  `,
14596
14650
  slicc_rename_acks: (listing) => {
14597
14651
  const samples = [...listing.renameAcks?.ackMs ?? []].sort((a, b) => a - b);
@@ -19264,7 +19318,7 @@ async function writeAll3(file, bytes) {
19264
19318
  const write = file.write;
19265
19319
  for (let at = 0; at < bytes.length; ) at += await write.call(file, bytes.subarray(at));
19266
19320
  }
19267
- function bound(file, key) {
19321
+ function bound2(file, key) {
19268
19322
  const method = file[key];
19269
19323
  return typeof method === "function" ? { [key]: method.bind(file) } : {};
19270
19324
  }
@@ -19356,9 +19410,9 @@ function scrubbed(inner, scrubber, idleMs = SCRUB_IDLE_MS) {
19356
19410
  later(stream.idle());
19357
19411
  };
19358
19412
  return new OpenFile({
19359
- ...bound(file, "read"),
19360
- ...bound(file, "peek"),
19361
- ...bound(file, "stat"),
19413
+ ...bound2(file, "read"),
19414
+ ...bound2(file, "peek"),
19415
+ ...bound2(file, "stat"),
19362
19416
  poll: () => {
19363
19417
  const state = pollFile(file);
19364
19418
  return quiet2() ? state : { ...state, writable: false };
@@ -25077,7 +25131,9 @@ function holdingToken(opts, holds2, caller) {
25077
25131
  ...opts.statfs ? { statfs: opts.statfs } : {},
25078
25132
  ...opts.renames ? { renames: opts.renames } : {},
25079
25133
  ...opts.renameSeq ? { renameSeq: opts.renameSeq } : {},
25080
- hold: (path, on, open2, real, handle) => holds2.hold(path, on, open2, real, handle),
25134
+ hold: (path, on, open2, real, handle, ino) => holds2.hold(path, on, open2, real, handle, ino),
25135
+ boundIno: holds2.boundIno.bind(holds2),
25136
+ trust: holds2.trust.bind(holds2),
25081
25137
  keptFor: (handle) => holds2.keptFor(handle),
25082
25138
  unlinked: (path) => holds2.keptAt(path),
25083
25139
  gate: (path, op) => opts.unlinkGate && holds2.holding(path) ? opts.unlinkGate.run(holds2.keyOf(path), op) : op(),
@@ -29128,6 +29184,7 @@ var Launcher = class {
29128
29184
  this.openFiles.add(this.nodes);
29129
29185
  options.fs.onPeer?.(() => this.flushOpen());
29130
29186
  options.fs.remoteChanges?.((change) => {
29187
+ this.held.doubt(change.paths, change.moved?.to);
29131
29188
  if (change.moved) this.moved(change.moved.from, change.moved.to);
29132
29189
  this.changes.flush();
29133
29190
  this.cwds.touched(change.paths, change.trees);
@@ -30140,7 +30197,8 @@ var Launcher = class {
30140
30197
  opfsCalls: this.rootFs.opfsCalls?.() ?? 0,
30141
30198
  opfsOps: this.rootFs.opfsOps?.() ?? 0,
30142
30199
  cwdChecks: this.cwds.checks,
30143
- renameAcks: this.rootFs.renameWaits?.()
30200
+ renameAcks: this.rootFs.renameWaits?.(),
30201
+ staleFds: staleFdCount()
30144
30202
  }),
30145
30203
  sudo: (call, signal) => this.sudo(pid, call, signal),
30146
30204
  crontab: (call) => this.crontab(pid, call),
@@ -31118,6 +31176,7 @@ var Launcher = class {
31118
31176
  this.renameLog.add(from, to);
31119
31177
  if (source) return;
31120
31178
  for (const nodes of this.openFiles) nodes.renamed(from, to);
31179
+ for (const holds2 of this.held) holds2.renamed(from, to);
31121
31180
  this.watchers.moved(from, to);
31122
31181
  }
31123
31182
  processChain(fs, nodes, unlinked) {