@ai-ecoverse/slicc-kernel 1.167.2 → 1.167.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
@@ -1260,7 +1260,7 @@ Reads retry briefly when a concurrent write has invalidated the OPFS file snapsh
1260
1260
 
1261
1261
  At boot, a kernel drops sidecar records whose file is gone (and symlink records whose name became a real file or whose directory is gone), left behind by a tab that closed mid-change. It does this only when it is the only kernel on the origin (the watch channel's join handshake finds no other), and it drops a record only if it is still the one it found: another tab's live changes made while it looked, such as a placeholder tar replaces with a symlink, are kept. Up to 1.119.2, a tab that booted while another extracted symlinks with tar could drop some of them.
1262
1262
 
1263
- A kernel caches the directory handles it has walked and the sidecar entries it has read. Its own changes keep both caches current, and the other kernels on the origin hear of them through a `BroadcastChannel` named after the database (one for the sidecar, one for directories). A cached directory is checked against its path (`resolve()`) at most once a second before a lookup goes through it, so a directory moved, removed or replaced by another writer, such as a page using the OPFS API, is found at its new place or reported missing within a second. It also keeps the file handles it has looked up (up to 4,096, the least recently used dropped first) for a second after each lookup. Before it uses one, it reads the file through it, which is the read a `stat` needs anyway, and that read serves the handle's uses in the kernel's next 64 lookups, for at most 100 ms ([#1425](https://github.com/ai-ecoverse/slicc-kernel/issues/1425)), so the count doesn't depend on how fast the machine is. So a file another writer removed is missing, and a rewritten one shows its new contents, within 100 ms. The kernel's own writes, truncates, rewrites and renames drop the read at once, and a read that started before an own change isn't kept, so the kernel's own changes always show at once. One replaced by a directory is seen as a directory within a second. Its own changes, and the other kernels' notices, drop the handles they touch at once. A lookup of a directory it has cached and checked within the second is answered from the cache, so a directory replaced by another writer is seen as replaced within a second, as above. A name it has just found missing stays missing for the kernel's next 256 lookups, for at most 100 ms, which serves the several checks a single create makes of a name that isn't there yet (about ten per file in a pnpm install, up to a few hundred lookups apart); its own creates, renames and links in that directory end that at once, so only a file another kernel or a page using the OPFS API makes in that window is seen up to 100 ms late. While another kernel shares the root, both short-lived windows follow the contract below. From [#945](https://github.com/ai-ecoverse/slicc-kernel/issues/945) until [#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984) they were off then, which made every stat and every miss an OPFS call.
1263
+ A kernel caches the directory handles it has walked and the sidecar entries it has read. Its own changes keep both caches current, and the other kernels on the origin hear of them through a `BroadcastChannel` named after the database (one for the sidecar, one for directories). A cached directory is checked against its path (`resolve()`) at most once a second before a lookup goes through it, so a directory moved, removed or replaced by another writer, such as a page using the OPFS API, is found at its new place or reported missing within a second. It also keeps the file handles it has looked up (up to 4,096, the least recently used dropped first) for a second after each lookup. Before it uses one, it reads the file through it, which is the read a `stat` needs anyway, and that read serves the handle's uses in the kernel's next 64 lookups, for at most 100 ms ([#1425](https://github.com/ai-ecoverse/slicc-kernel/issues/1425)), so the count doesn't depend on how fast the machine is. So a file another writer removed is missing, and a rewritten one shows its new contents, within 100 ms. The kernel's own writes, truncates, rewrites and renames drop the read at once, and a read that started before an own change isn't kept, so the kernel's own changes always show at once. One replaced by a directory is seen as a directory within a second. Its own changes, and the other kernels' notices, drop the handles they touch at once. A lookup of a directory it has cached and checked within the second is answered from the cache, so a directory replaced by another writer is seen as replaced within a second, as above. A name it has just found missing stays missing for the kernel's next 256 lookups, for at most 100 ms, which serves the several checks a single create makes of a name that isn't there yet (about ten per file in a pnpm install, up to a few hundred lookups apart); its own creates, renames and links in that directory end that at once, so only a file another kernel or a page using the OPFS API makes in that window is seen up to 100 ms late. A listing of the directory (`ls`, a glob, `readdir`) ends it too for every name it shows, so `ls` and then `cat` agree ([#1693](https://github.com/ai-ecoverse/slicc-kernel/issues/1693)). A writer outside the kernel whose file must open in the very next run, such as a page that writes a file and then calls `kernel.run`, posts a notice for it (`postFsNotice`, below) or waits 100 ms. While another kernel shares the root, both short-lived windows follow the contract below. From [#945](https://github.com/ai-ecoverse/slicc-kernel/issues/945) until [#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984) they were off then, which made every stat and every miss an OPFS call.
1264
1264
 
1265
1265
  **What another tab sees, and when** ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)): one rule covers everything a kernel caches about a shared root.
1266
1266
  - **Notices:** while another kernel shares the root, a kernel posts a notice before each changing call returns. A notice names the sidecar records it changed (owners, modes, links, times, write counts), the directories it created, removed or renamed into, and the files it wrote back ([#962](https://github.com/ai-ecoverse/slicc-kernel/issues/962)).
@@ -2838,6 +2838,10 @@ var OpfsFs = class {
2838
2838
  const children3 = /* @__PURE__ */ new Map();
2839
2839
  this.others++;
2840
2840
  for await (const child of handle.values()) children3.set(child.name, child);
2841
+ if (this.absent.size > 0) {
2842
+ const base = found.path === "/" ? "" : found.path;
2843
+ for (const name2 of children3.keys()) this.absent.delete(`${base}/${name2}`);
2844
+ }
2841
2845
  for (const link2 of await this.meta.links(found.path)) {
2842
2846
  const name2 = baseOf(link2.path);
2843
2847
  if (!children3.has(name2)) children3.set(name2, link2);
@@ -5271,28 +5275,39 @@ async function stickyEntry(g, op, path, dir) {
5271
5275
  if (sticky(g.cred, perm, (await ancestryOf(g.fs, entry)).at(-1))) return;
5272
5276
  if (await present(g.fs, entry, false)) deny(g, op, path, "EPERM");
5273
5277
  }
5278
+ async function unwritable(g, op, path, dir) {
5279
+ if (dir.denied === void 0) return false;
5280
+ if (dir.link) {
5281
+ deny(g, op, dir.denied);
5282
+ return false;
5283
+ }
5284
+ const perm = dir.denied === dir.path ? dir.perm : void 0;
5285
+ if (perm && !await present(g.fs, dir.path, true)) return false;
5286
+ if (perm && permits(g.cred, perm, X_OK)) return true;
5287
+ deny(g, op, path);
5288
+ return false;
5289
+ }
5274
5290
  async function inParent(g, op, path, old = false) {
5275
5291
  await fence(g, op, path);
5276
- if (old === "replaced") {
5277
- const dir2 = await need(g, op, parentOf4(path), W_OK | X_OK, DIR, path);
5278
- return dir2.denied === void 0 ? stickyEntry(g, op, path, dir2) : void 0;
5279
- }
5280
- if (!old) return void await need(g, op, parentOf4(path), W_OK | X_OK, DIR, path);
5281
5292
  const dir = await access(g.fs, g.cred, parentOf4(path), W_OK | X_OK, linked(g, DIR));
5293
+ if (old !== true) {
5294
+ if (await unwritable(g, op, path, dir)) deny(g, op, path);
5295
+ else if (old === "replaced" && dir.denied === void 0) await stickyEntry(g, op, path, dir);
5296
+ return;
5297
+ }
5282
5298
  if (dir.denied === void 0) return stickyEntry(g, op, path, dir);
5283
5299
  if (dir.link) return deny(g, op, dir.denied);
5284
5300
  const lookup2 = dir.denied === dir.path && dir.perm && permits(g.cred, dir.perm, X_OK);
5285
5301
  if (!lookup2 || await present(g.fs, entryIn(dir, path), false)) deny(g, op, path);
5286
5302
  }
5287
- async function makeIn(g, op, path) {
5303
+ async function makeIn(g, op, path, parents = true) {
5288
5304
  for (let dir = parentOf4(path); ; dir = parentOf4(dir)) {
5289
5305
  const walked = await access(g.fs, g.cred, dir, W_OK | X_OK, linked(g, DIR));
5290
- if (walked.link) return deny(g, op, walked.denied);
5291
- if (walked.denied !== void 0) {
5306
+ if (await unwritable(g, op, path, walked)) {
5292
5307
  if (!await present(g.fs, path, true)) deny(g, op, path);
5293
5308
  return;
5294
5309
  }
5295
- if (!walked.missing || dir === "/") return;
5310
+ if (!parents || !walked.missing || dir === "/") return;
5296
5311
  }
5297
5312
  }
5298
5313
  async function tree(g, op, dir, perm) {
@@ -8477,7 +8492,7 @@ function satisfies(version, range2) {
8477
8492
  }
8478
8493
 
8479
8494
  // src/version.ts
8480
- var KERNEL_VERSION = true ? "1.167.2" : "0.0.0-dev";
8495
+ var KERNEL_VERSION = true ? "1.167.4" : "0.0.0-dev";
8481
8496
  var OS_RELEASE = "6.0.0-slicc";
8482
8497
  function procVersion() {
8483
8498
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -15134,7 +15149,7 @@ async function guard(g, path, req, cwd) {
15134
15149
  return { path: w };
15135
15150
  }
15136
15151
  case "mkdir": {
15137
- await makeIn(g, op, path);
15152
+ await makeIn(g, op, path, !req.exclusive);
15138
15153
  return { path: await walkSafe(g, path, false) };
15139
15154
  }
15140
15155
  case "create":
@@ -33755,7 +33770,7 @@ var Launcher = class {
33755
33770
  kept: async (tab, items, paths) => {
33756
33771
  for (const { path, hidden, fid } of items) {
33757
33772
  if (hidden !== void 0 && sharedKept(hidden)) await this.adoptShared(path, hidden, fid);
33758
- else this.adoptKept(path, hidden, fid, ours);
33773
+ else await this.adoptKept(path, hidden, fid, ours);
33759
33774
  }
33760
33775
  await open2(tab, paths);
33761
33776
  },
@@ -33765,14 +33780,33 @@ var Launcher = class {
33765
33780
  }
33766
33781
  };
33767
33782
  }
33768
- async adoptShared(path, hidden, fid) {
33769
- const bound3 = [...this.held].map(
33770
- (holds2) => [holds2, [...holds2.boundAt(path, fid), ...holds2.boundAt(hidden, fid)]]
33771
- );
33772
- const keeping = bound3.filter(([, ids]) => ids.length > 0);
33783
+ async keptIds(hidden, fid) {
33784
+ if (fid !== void 0) return [fid];
33785
+ const st = await this.rootFs.lstat(hidden).catch(() => void 0);
33786
+ return st?.fid === void 0 ? [void 0] : [void 0, st.fid];
33787
+ }
33788
+ boundTo(path, hidden, fids) {
33789
+ const keeping = [...this.held].map(
33790
+ (holds2) => [
33791
+ holds2,
33792
+ fids.flatMap((fid) => [...holds2.boundAt(path, fid), ...holds2.boundAt(hidden, fid)])
33793
+ ]
33794
+ ).filter(([, ids]) => ids.length > 0);
33773
33795
  const nodes = [...this.openFiles].filter(
33774
33796
  (open2) => [path, hidden].some((at) => open2.holdsAt(at))
33775
33797
  );
33798
+ const keep2 = (file) => {
33799
+ const kept = nodes.filter(
33800
+ (open2) => fids.some((fid) => open2.keepBound([hidden, path], fid, file))
33801
+ ).length;
33802
+ for (let i = kept; i < nodes.length; i++) file.release();
33803
+ for (const [holds2, ids] of keeping) holds2.keepBound(ids, path, file);
33804
+ void this.rootFs.announceHolds?.([], [path, hidden], false);
33805
+ };
33806
+ return { keeping, nodes, keep: keep2 };
33807
+ }
33808
+ async adoptShared(path, hidden, fid) {
33809
+ const { keeping, nodes, keep: keep2 } = this.boundTo(path, hidden, await this.keptIds(hidden, fid));
33776
33810
  const refs = keeping.length + nodes.length;
33777
33811
  if (refs === 0) {
33778
33812
  this.held.doubt([path]);
@@ -33787,29 +33821,17 @@ var Launcher = class {
33787
33821
  this.held.doubt([path]);
33788
33822
  return;
33789
33823
  }
33790
- const kept = nodes.filter((open2) => open2.keepBound([hidden, path], fid, file)).length;
33791
- for (let i = kept; i < nodes.length; i++) file.release();
33792
- for (const [holds2, ids] of keeping) holds2.keepBound(ids, path, file);
33793
- void this.rootFs.announceHolds?.([], [path, hidden], false);
33824
+ keep2(file);
33794
33825
  }
33795
- adoptKept(path, hidden, fid, ours) {
33826
+ async adoptKept(path, hidden, fid, ours) {
33796
33827
  if (hidden === void 0 || !ours(hidden)) {
33797
33828
  this.held.doubt([path]);
33798
33829
  return;
33799
33830
  }
33800
- const bound3 = [...this.held].map(
33801
- (holds2) => [holds2, [...holds2.boundAt(path, fid), ...holds2.boundAt(hidden, fid)]]
33802
- );
33803
- const keeping = bound3.filter(([, ids]) => ids.length > 0);
33804
- const nodes = [...this.openFiles].filter(
33805
- (open2) => [path, hidden].some((at) => open2.holdsAt(at))
33806
- );
33831
+ const { keeping, nodes, keep: keep2 } = this.boundTo(path, hidden, await this.keptIds(hidden, fid));
33807
33832
  const file = this.unlinked.adopt(hidden, Math.max(keeping.length + nodes.length, 1));
33808
- const kept = nodes.filter((open2) => open2.keepBound([hidden, path], fid, file)).length;
33809
- for (let i = kept; i < nodes.length; i++) file.release();
33810
33833
  if (keeping.length === 0 && nodes.length === 0) file.release();
33811
- for (const [holds2, ids] of keeping) holds2.keepBound(ids, path, file);
33812
- void this.rootFs.announceHolds?.([], [path, hidden], false);
33834
+ keep2(file);
33813
33835
  }
33814
33836
  openNodes(fs, entries) {
33815
33837
  const nodes = new VfsNodes(fs, (path) => this.released(path), entries);
@@ -33846,6 +33868,7 @@ var Launcher = class {
33846
33868
  this.changes.flush();
33847
33869
  this.changes.foreign(crowded);
33848
33870
  if (crowded) {
33871
+ void this.announceHeld();
33849
33872
  void this.held.bindIds((path) => this.rootFileId(path)).then(
33850
33873
  () => Promise.all([...this.openFiles].map((open2) => open2.bindIds((p) => this.rootFileId(p))))
33851
33874
  ).then(() => this.announceHeld());