@ai-ecoverse/slicc-kernel 1.166.5 → 1.167.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
@@ -1256,7 +1256,7 @@ The database is `slicc-kernel` (or the `metadata` option), schema version 1, wit
1256
1256
 
1257
1257
  Not covered: hard links (OPFS cannot share contents between names; they work only within a mount whose driver offers `links`, such as `tmpfs`). File owners are stored only with `users: 'report'` or `'enforce'` (see [File owners](#file-owners)). With `users: 'off'`, an Emscripten process built against the kernel's credential calls (its glue imports `slicc_cred_js`, as homescoop's H1 builds do) sees every file as owned by its own effective uid and gid: root's 0 by default, a user's own ids when it runs as one, and the new ids after a `setuid`. Owner checks such as git's `safe.directory`, ssh's key checks and gnupg's home directory check then pass, as WASIX programs already see through their libc. An older program, whose libc says it is uid 1000 whoever runs it, sees every file as owned by 1000:1000 as before, so its own checks pass too. The kernel decides this once, when it loads the program. Emscripten also doesn't enforce permission bits, so a file runs whether or not its exec bit is set; the bits are what `ls`, `stat` and `test` report. With `users: 'off'` a file or directory with no permission bits at all shows as `0666` or `0777` to Emscripten programs; with owners on they see the real mode (`0`), and the kernel, not Emscripten's own checks, decides who may read, write or enter it (`cd` asks the kernel for `x` on the directory), so root still enters a `000` directory.
1258
1258
 
1259
- Reads retry briefly when a concurrent write has invalidated the OPFS file snapshot (`NotReadableError`).
1259
+ Reads retry briefly when a concurrent write has invalidated the OPFS file snapshot (`NotReadableError`). A positioned read uses the snapshot the kernel's last look at the file took (below) when it is still current and covers the whole range, so a run of reads through a descriptor makes no `getFile` of its own beyond the one each window's look takes ([#1626](https://github.com/ai-ecoverse/slicc-kernel/issues/1626)). A read past the snapshot's end, an empty read, or a snapshot that can't be read any more takes a fresh one. In Chrome the snapshot holds no bytes of its own: it reads the file as it is now, so a write in place (a sync access handle) shows at once, and a file another writer replaced, truncated or removed fails the snapshot (`NotReadableError`, `NotFoundError`), which sends the read to a fresh one. Holding a snapshot blocks no other writer: a sync access handle, `createWritable`, `move` and `removeEntry` all go ahead.
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
 
@@ -1312,6 +1312,7 @@ var RemoteWatch = class {
1312
1312
  };
1313
1313
  const left = /* @__PURE__ */ new Set([to]);
1314
1314
  const timer = setTimeout(done5, cap);
1315
+ timer.unref?.();
1315
1316
  this.waiting.set(seq2, { left, done: done5 });
1316
1317
  });
1317
1318
  }
@@ -1353,6 +1354,9 @@ var RemoteWatch = class {
1353
1354
  this.keeps?.left(tab);
1354
1355
  this.ackers.delete(tab);
1355
1356
  this.holders.delete(tab);
1357
+ for (const wait2 of [...this.waiting.values()]) {
1358
+ if (wait2.left.delete(tab) && wait2.left.size === 0) wait2.done();
1359
+ }
1356
1360
  }
1357
1361
  }
1358
1362
  this.rebuild();
@@ -1598,9 +1602,9 @@ async function unlocked(name2, op) {
1598
1602
  return await op();
1599
1603
  } catch (err) {
1600
1604
  if (!LOCKED.has(err?.name)) throw err;
1601
- const delay = SYNC_BACKOFF_MS[attempt];
1602
- if (delay === void 0) throw fsError("EBUSY", name2);
1603
- await new Promise((resolve2) => setTimeout(resolve2, delay));
1605
+ const delay2 = SYNC_BACKOFF_MS[attempt];
1606
+ if (delay2 === void 0) throw fsError("EBUSY", name2);
1607
+ await new Promise((resolve2) => setTimeout(resolve2, delay2));
1604
1608
  }
1605
1609
  }
1606
1610
  }
@@ -1611,10 +1615,10 @@ async function openSync(handle) {
1611
1615
  try {
1612
1616
  return await sync.call(handle, { mode: "readwrite-unsafe" });
1613
1617
  } catch (err) {
1614
- const delay = SYNC_BACKOFF_MS[attempt];
1618
+ const delay2 = SYNC_BACKOFF_MS[attempt];
1615
1619
  if (!LOCKED.has(err?.name)) throw err;
1616
- if (delay === void 0) return void 0;
1617
- await new Promise((resolve2) => setTimeout(resolve2, delay));
1620
+ if (delay2 === void 0) return void 0;
1621
+ await new Promise((resolve2) => setTimeout(resolve2, delay2));
1618
1622
  }
1619
1623
  }
1620
1624
  }
@@ -2552,6 +2556,15 @@ var OpfsFs = class {
2552
2556
  }
2553
2557
  async pread(path, offset, length2) {
2554
2558
  return this.reading(path, async (handle) => {
2559
+ const seen = this.glanced(handle);
2560
+ if (seen && length2 > 0 && offset + length2 <= seen.size) {
2561
+ const bytes = await this.io(seen.slice(offset, offset + length2).arrayBuffer()).then(
2562
+ (buffer) => new Uint8Array(buffer),
2563
+ () => void 0
2564
+ );
2565
+ if (bytes?.length === length2) return bytes;
2566
+ this.glances.delete(handle);
2567
+ }
2555
2568
  const file = await this.io(handle.getFile());
2556
2569
  await this.refuseNode(path, file.size === 0);
2557
2570
  return new Uint8Array(await file.slice(offset, offset + length2).arrayBuffer());
@@ -3623,9 +3636,9 @@ var OpfsFs = class {
3623
3636
  if (!failed3) return true;
3624
3637
  const error = failed3.err?.name;
3625
3638
  if (handle.kind !== "file" || !LOCKED.has(error)) return false;
3626
- const delay = SYNC_BACKOFF_MS[attempt];
3627
- if (delay === void 0) throw fsError("EBUSY", name2);
3628
- await new Promise((resolve2) => setTimeout(resolve2, delay));
3639
+ const delay2 = SYNC_BACKOFF_MS[attempt];
3640
+ if (delay2 === void 0) throw fsError("EBUSY", name2);
3641
+ await new Promise((resolve2) => setTimeout(resolve2, delay2));
3629
3642
  }
3630
3643
  }
3631
3644
  async copy(handle, dir, name2, copies, rest2 = "") {
@@ -8464,7 +8477,7 @@ function satisfies(version, range2) {
8464
8477
  }
8465
8478
 
8466
8479
  // src/version.ts
8467
- var KERNEL_VERSION = true ? "1.166.5" : "0.0.0-dev";
8480
+ var KERNEL_VERSION = true ? "1.167.1" : "0.0.0-dev";
8468
8481
  var OS_RELEASE = "6.0.0-slicc";
8469
8482
  function procVersion() {
8470
8483
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -9415,8 +9428,41 @@ var within4 = (path, target) => path === target || target === "/" || path.starts
9415
9428
  var TOUCHED_MAX = 4096;
9416
9429
  var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
9417
9430
  var PEER_RM_MS = 1e3;
9431
+ var SHARED_ACK_MS = 24 * 60 * 60 * 1e3;
9432
+ var delay = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
9418
9433
  var PEER_RENAME_MS = 1e4;
9419
9434
  var PEER_GATE_MS = PEER_RENAME_MS + 5e3;
9435
+ var SHARED_PREFIX = "shared.";
9436
+ var keptLock = (hidden) => `slicc-kept:${hidden}`;
9437
+ function sharedKept(hidden) {
9438
+ const at = `/${UNLINKED_DIR}/${SHARED_PREFIX}`;
9439
+ return hidden.startsWith(at) && /^[0-9a-f]+$/.test(hidden.slice(at.length));
9440
+ }
9441
+ function holdShared(locks, name2) {
9442
+ if (!locks) return Promise.resolve(async () => void 0);
9443
+ return new Promise((resolve2) => {
9444
+ const settled = locks.request(name2, { mode: "shared" }, () => {
9445
+ const { promise, resolve: done5 } = Promise.withResolvers();
9446
+ resolve2(async () => {
9447
+ done5();
9448
+ await settled.catch(() => void 0);
9449
+ });
9450
+ return promise;
9451
+ });
9452
+ });
9453
+ }
9454
+ async function whenLast(locks, name2, op) {
9455
+ if (!locks) {
9456
+ await op();
9457
+ return true;
9458
+ }
9459
+ let free = false;
9460
+ await locks.request(name2, { mode: "exclusive", ifAvailable: true }, async (lock) => {
9461
+ free = lock !== null;
9462
+ if (free) await op();
9463
+ });
9464
+ return free;
9465
+ }
9420
9466
  var lockName = (instance) => `slicc-unlinked:${instance}`;
9421
9467
  var dirOf = (root) => root === "/" ? `/${UNLINKED_DIR}` : `${root}/${UNLINKED_DIR}`;
9422
9468
  var PathGate = class {
@@ -9732,52 +9778,124 @@ var UnlinkedKeeper = class {
9732
9778
  void peers.ask(tab, { kept: { items: [], paths } }, PEER_RM_MS);
9733
9779
  throw fsError("EBUSY", `${target}: open in another tab that did not answer`);
9734
9780
  }
9735
- const { whole, sends } = await this.setAside(target, asked);
9736
- return { whole, undoing: (op) => this.undoing(op, peers, sends, heard) };
9781
+ const { whole, sends, shared } = await this.setAside(target, asked);
9782
+ return { whole, undoing: (op) => this.undoing(op, peers, sends, heard, shared) };
9737
9783
  }
9738
9784
  async setAside(target, asked) {
9739
9785
  const mine = this.heldWithin(target);
9740
- const taken = /* @__PURE__ */ new Set();
9741
- let whole = false;
9786
+ const tabsOf = /* @__PURE__ */ new Map();
9787
+ for (const { holders } of asked)
9788
+ for (const { path } of holders) tabsOf.set(path, (tabsOf.get(path) ?? 0) + 1);
9789
+ const done5 = /* @__PURE__ */ new Map();
9790
+ const shared = [];
9742
9791
  const sends = [];
9743
9792
  for (const { tab, holders, paths } of asked) {
9744
9793
  const items = [];
9745
9794
  for (const holder of holders) {
9746
- const free = !mine.has(holder.path) && !taken.has(holder.path);
9747
- const hidden = free ? await this.asideFor(holder.path, holder.instance) : void 0;
9748
- if (hidden !== void 0) {
9749
- taken.add(holder.path);
9750
- if (holder.path === target) whole = true;
9795
+ if (!done5.has(holder.path)) {
9796
+ const many = mine.has(holder.path) || (tabsOf.get(holder.path) ?? 0) > 1;
9797
+ done5.set(holder.path, await this.asideOne(holder, many, mine, shared));
9751
9798
  }
9752
- items.push({ path: holder.path, hidden, fid: holder.fid });
9799
+ items.push({ path: holder.path, hidden: done5.get(holder.path), fid: holder.fid });
9753
9800
  }
9754
9801
  sends.push({ tab, items, paths });
9755
9802
  }
9756
- return { whole, sends };
9803
+ return { whole: done5.get(target) !== void 0, sends, shared };
9757
9804
  }
9758
- async undoing(op, peers, sends, heard) {
9805
+ async asideOne(holder, many, mine, shared) {
9806
+ if (!many) return this.asideFor(holder.path, holder.instance);
9807
+ const one = await this.sharedAside(holder.path, mine);
9808
+ if (one) shared.push(one);
9809
+ return one?.hidden;
9810
+ }
9811
+ async sharedAside(path, mine) {
9812
+ const hidden = `${dirOf("/")}/${SHARED_PREFIX}${crypto.randomUUID().replaceAll("-", "")}`;
9813
+ const release = await holdShared(this.sharedLocks(), keptLock(hidden));
9814
+ const moved2 = await this.serial(async () => {
9815
+ try {
9816
+ const owner = this.dirOwner;
9817
+ await this.fs.mkdir(dirOf("/"), { recursive: true, ...owner ? { owner } : {} });
9818
+ await this.fs.rename(path, hidden);
9819
+ return true;
9820
+ } catch {
9821
+ return false;
9822
+ }
9823
+ });
9824
+ if (!moved2) {
9825
+ await release();
9826
+ return void 0;
9827
+ }
9828
+ const local2 = mine.get(path) ?? [];
9829
+ const file = local2.length > 0 ? await this.sharedFile(hidden, local2.length) : void 0;
9830
+ if (file) for (const holder of local2) holder.keep(path, file);
9831
+ return { path, hidden, release, local: local2, file };
9832
+ }
9833
+ async sharedFile(hidden, refs) {
9834
+ const locks = this.sharedLocks();
9835
+ const mine = await holdShared(locks, keptLock(hidden));
9836
+ let left = refs;
9837
+ return {
9838
+ hidden,
9839
+ release: () => {
9840
+ if (--left > 0) return;
9841
+ void mine().then(() => this.dropIfLast(hidden));
9842
+ }
9843
+ };
9844
+ }
9845
+ dropIfLast(hidden) {
9846
+ return this.serial(
9847
+ () => whenLast(this.sharedLocks(), keptLock(hidden), () => this.drop(hidden))
9848
+ );
9849
+ }
9850
+ sharedLocks() {
9851
+ return this.locks;
9852
+ }
9853
+ async undoing(op, peers, sends, heard, shared = []) {
9759
9854
  let failed3;
9760
9855
  let out;
9761
9856
  try {
9762
9857
  out = await op();
9763
9858
  } catch (err) {
9764
9859
  failed3 = err ?? new Error("failed");
9765
- for (const send2 of sends) {
9766
- for (const item of send2.items) {
9767
- const { hidden, path } = item;
9768
- if (hidden !== void 0 && await this.moveBack(hidden, path)) item.hidden = void 0;
9769
- }
9770
- }
9860
+ await this.rollBackAsides(sends, shared);
9771
9861
  }
9862
+ const hiddenShared = new Set(shared.map((one) => one.hidden));
9863
+ const answers2 = sends.map(({ tab, items, paths }) => {
9864
+ const keeps = items.some((item) => item.hidden && hiddenShared.has(item.hidden));
9865
+ return {
9866
+ keeps,
9867
+ told: peers.ask(tab, { kept: { items, paths } }, keeps ? SHARED_ACK_MS : PEER_RM_MS)
9868
+ };
9869
+ });
9772
9870
  await Promise.all(
9773
- sends.map(({ tab, items, paths }, i) => {
9774
- const told = peers.ask(tab, { kept: { items, paths } }, PEER_RM_MS);
9775
- return heard[i] ? told : void 0;
9776
- })
9871
+ answers2.map(({ told }, i) => heard[i] ? Promise.race([told, delay(PEER_RM_MS)]) : void 0)
9777
9872
  );
9873
+ for (const one of shared) {
9874
+ const waits = answers2.filter(
9875
+ ({ keeps }, i) => keeps && sends[i]?.items.some((item) => item.hidden === one.hidden)
9876
+ );
9877
+ const done5 = () => one.release().then(() => this.dropIfLast(one.hidden));
9878
+ void Promise.all(waits.map(({ told }) => told)).then(done5, done5);
9879
+ }
9778
9880
  if (failed3 !== void 0) throw failed3;
9779
9881
  return out;
9780
9882
  }
9883
+ async rollBackAsides(sends, shared) {
9884
+ const back = /* @__PURE__ */ new Map();
9885
+ for (const item of sends.flatMap((send2) => send2.items)) {
9886
+ const { hidden, path } = item;
9887
+ if (hidden === void 0) continue;
9888
+ if (!back.has(hidden)) back.set(hidden, await this.moveBack(hidden, path));
9889
+ if (back.get(hidden)) item.hidden = void 0;
9890
+ }
9891
+ for (const { hidden, path, local: local2, file } of shared) {
9892
+ if (!back.get(hidden) || !file) continue;
9893
+ for (const holder of local2) {
9894
+ holder.unkeep(path, file);
9895
+ file.release();
9896
+ }
9897
+ }
9898
+ }
9781
9899
  moveBack(hidden, path) {
9782
9900
  return this.serial(async () => {
9783
9901
  if (await this.present(path)) return false;
@@ -9886,6 +10004,15 @@ var UnlinkedKeeper = class {
9886
10004
  for (const name2 of names2) {
9887
10005
  const id2 = name2.slice(0, name2.lastIndexOf("."));
9888
10006
  const at = id2.indexOf("~");
10007
+ if (name2.startsWith(SHARED_PREFIX)) {
10008
+ const at2 = `${dir}/${name2}`;
10009
+ await whenLast(
10010
+ this.sharedLocks(),
10011
+ keptLock(at2),
10012
+ () => this.fs.rm(at2, { recursive: true }).catch(() => void 0)
10013
+ );
10014
+ continue;
10015
+ }
9889
10016
  const dead = id2 !== this.instance && (at < 0 ? held !== void 0 && !held.has(lockName(id2)) : owner?.alive(id2.slice(0, at)) === false);
9890
10017
  if (dead) await this.fs.rm(`${dir}/${name2}`, { recursive: true }).catch(() => void 0);
9891
10018
  }
@@ -24598,13 +24725,13 @@ var VfsNode = class {
24598
24725
  this.recount();
24599
24726
  if (!this.orphaned) this.onDirty?.();
24600
24727
  if (this.writeBack !== void 0 || this.orphaned) return;
24601
- const delay = Math.max(WRITEBACK_MS, this.writeBackCost * WRITEBACK_COST_FACTOR);
24728
+ const delay2 = Math.max(WRITEBACK_MS, this.writeBackCost * WRITEBACK_COST_FACTOR);
24602
24729
  this.writeBack = setTimeout(() => {
24603
24730
  this.writeBack = void 0;
24604
24731
  this.serial(() => this.flush()).catch(() => {
24605
24732
  this.failed = true;
24606
24733
  });
24607
- }, delay);
24734
+ }, delay2);
24608
24735
  }
24609
24736
  ensure(need2) {
24610
24737
  const cur = this.data ?? new Uint8Array(0);
@@ -33572,7 +33699,10 @@ var Launcher = class {
33572
33699
  await Promise.all(mine);
33573
33700
  },
33574
33701
  kept: async (tab, items, paths) => {
33575
- for (const { path, hidden, fid } of items) this.adoptKept(path, hidden, fid, ours);
33702
+ for (const { path, hidden, fid } of items) {
33703
+ if (hidden !== void 0 && sharedKept(hidden)) await this.adoptShared(path, hidden, fid);
33704
+ else this.adoptKept(path, hidden, fid, ours);
33705
+ }
33576
33706
  await open2(tab, paths);
33577
33707
  },
33578
33708
  left: (tab) => {
@@ -33581,6 +33711,33 @@ var Launcher = class {
33581
33711
  }
33582
33712
  };
33583
33713
  }
33714
+ async adoptShared(path, hidden, fid) {
33715
+ const bound3 = [...this.held].map(
33716
+ (holds2) => [holds2, [...holds2.boundAt(path, fid), ...holds2.boundAt(hidden, fid)]]
33717
+ );
33718
+ const keeping = bound3.filter(([, ids]) => ids.length > 0);
33719
+ const nodes = [...this.openFiles].filter(
33720
+ (open2) => [path, hidden].some((at) => open2.holdsAt(at))
33721
+ );
33722
+ const refs = keeping.length + nodes.length;
33723
+ if (refs === 0) {
33724
+ this.held.doubt([path]);
33725
+ return;
33726
+ }
33727
+ const file = await this.unlinked.sharedFile(hidden, refs);
33728
+ if (!await this.rootFs.lstat(hidden).then(
33729
+ () => true,
33730
+ () => false
33731
+ )) {
33732
+ for (let i = 0; i < refs; i++) file.release();
33733
+ this.held.doubt([path]);
33734
+ return;
33735
+ }
33736
+ const kept = nodes.filter((open2) => open2.keepBound([hidden, path], fid, file)).length;
33737
+ for (let i = kept; i < nodes.length; i++) file.release();
33738
+ for (const [holds2, ids] of keeping) holds2.keepBound(ids, path, file);
33739
+ void this.rootFs.announceHolds?.([], [path, hidden], false);
33740
+ }
33584
33741
  adoptKept(path, hidden, fid, ours) {
33585
33742
  if (hidden === void 0 || !ours(hidden)) {
33586
33743
  this.held.doubt([path]);
@@ -33723,7 +33880,6 @@ var Launcher = class {
33723
33880
  moved(from, to, source) {
33724
33881
  this.renameLog.add(from, to);
33725
33882
  for (const holds2 of this.held) holds2.renamed(from, to);
33726
- if (this.mounts.locate(to) === void 0) this.rootFs.rehold?.(from, to);
33727
33883
  if (source) return;
33728
33884
  for (const nodes of this.openFiles) nodes.renamed(from, to);
33729
33885
  this.watchers.moved(from, to);