@ai-ecoverse/slicc-kernel 1.168.0 → 1.168.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
@@ -1260,6 +1260,21 @@ OPFS stays authoritative: an entry is consulted only for a path that exists in O
1260
1260
  - **A holder that ends** (its tab closes or its worker dies) releases its locks, as Web Locks do, so execs go on with the files as it left them, and the next installer run repairs them.
1261
1261
  - **Under the Node entry,** Node's `navigator.locks` (26.11) is per thread, although its documentation says it spans worker threads: a lock taken in one thread neither holds nor shows in another. So call `replacing` on the thread that runs the kernel. A Node kernel names its locks with the default `slicc-kernel`.
1262
1262
 
1263
+ **Swapping a package inside the kernel** ([#1705](https://github.com/ai-ecoverse/slicc-kernel/issues/1705)): pnpm, run inside the kernel (`kernel.run(['pnpm', 'install', …])`), replaces a package in a hoisted `node_modules` by staging the new copy in a sibling directory, renaming the old one aside, renaming the new one in and then deleting the old one. For the time between the two renames the package isn't there, and an exec that reads its glue before the rename in and its wasm after would get a mix. So the kernel takes the same `replacing` lock for the swap:
1264
+
1265
+ - **The rename aside:** a rename that moves a package directory (the shape an exec locks, above) somewhere else first takes that directory's exclusive `slicc-kernel-replacing:v1:<metadata>:<directory>` lock, so execs that are loading from it finish first. A tab that holds the lock shared parks the swap right there, with the package still whole: an exec started after that queues behind the swap (Web Locks let a waiting exclusive request go before later shared ones, even with `ifAvailable`) and gets the new files. When the moved directory has a `package.json`, the kernel keeps the lock after the rename: the swap is open.
1266
+ - **The rename in:** a rename onto the open name runs under that lock and ends the swap; the kernel then releases it. A rename onto it that fails leaves the swap open. A staged copy landing on its name from a sibling named like pnpm's or npm's stages (`<name>_pacquet-stage_…`, `<name>_tmp_…` or `.<name>-…`) takes no lock of its own and opens no swap, so pnpm's first try, a new package's stage and a rename in after the bound cost nothing.
1267
+ - **The bound:** a swap with no rename onto its name within 2 s ends then, and execs that waited for it go on (and find the package missing, as they would have at once without the lock).
1268
+ - **Execs in the kernel:** `run`, `openTerminal`, every spawn, and a new scan of the command catalog, wait while one of the kernel's own swaps is open, and start once it ends. So a program run by name or by path between the two renames, a pnpm lifecycle script included, gets the new files, never `ENOENT` or the copy that was renamed aside. Other kernels sharing the root plan from the catalog they have and wait on the package's lock.
1269
+ - **Cost:** nothing per exec, and per rename two string tests; a rename aside of a package makes one lock request and one `stat` of its `package.json`, the rename in none.
1270
+ - **`/proc/sys/kernel/slicc_package_swaps`** reads four numbers: the swaps the kernel held, the longest in ms, the times an exec or a catalog scan waited for one, and the ms they waited in all.
1271
+ - **Not covered:**
1272
+ - an exec **by path** in another kernel sharing the root, between the two renames: it doesn't see the swap and can get `ENOENT`;
1273
+ - a `run` or `openTerminal` by name in another kernel between the two renames: it scans the catalog afresh and can run the copy renamed aside (the old files, whole);
1274
+ - a rename of a directory above packages (`mv node_modules x`): it locks nothing;
1275
+ - an installer that removes a package (`rm -r`) and writes it again, instead of swapping it in: its execs can see it missing or half written. One that moves it aside and writes the new one in place holds execs for the 2 s, and after that they can see it half written;
1276
+ - a plain `mv pkg elsewhere` (a package moved away for good): it holds execs in the kernel for up to 2 s, after which that package's are missing.
1277
+
1263
1278
  The database is `slicc-kernel` (or the `metadata` option), schema version 1, with one object store, `entries`, keyed by absolute path (`keyPath: 'path'`) and an index `dir` on the parent directory of symlinks. A record looks like `{ path, mode?, atimeMs?, mtimeMs?, mtimeFor?, ctimeMs?, ino?, link?, dir?, uid?, gid?, heir?, writes?, epoch? }`, where `mtimeFor` is the OPFS `lastModified` an explicit `mtimeMs` belongs to, `uid`, `gid` and `heir` (`[uid, gid]`) are the owners of [File owners](#file-owners), `writes` counts the writes kernels with `integrity` made to a protected file (see [Protected paths](#protected-paths)), whose `lastModified` OPFS leaves alone for a write through a sync access handle, and `epoch`, on the entry of `/`, counts their renames and symlinks at or above a protected path. A future schema bumps the version and migrates in `onupgradeneeded`; an older kernel then closes its connection. The sidecar is keyed by path from the `root` handle, so use a different `metadata` name for each distinct root.
1264
1279
 
1265
1280
  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.
@@ -3082,15 +3082,15 @@ var OpfsFs = class {
3082
3082
  const hooks = {};
3083
3083
  const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
3084
3084
  const copying = sourceStat.isDirectory && !movable(source.handle);
3085
- const staged = resolvePath(parent.path, staging);
3085
+ const staged2 = resolvePath(parent.path, staging);
3086
3086
  const place = async () => {
3087
3087
  this.forget(destination);
3088
3088
  const copies = /* @__PURE__ */ new Map();
3089
3089
  const root = { ino: inodeOf(source.path), ...moving };
3090
3090
  const owned4 = lock ? { lock } : {};
3091
3091
  hooks.notify?.();
3092
- const first = copying ? await this.intend({ from: source.path, to: staged, root, ...owned4 }) : void 0;
3093
- const second = copying && swap ? await this.intend({ from: staged, to: destination, replace: true, root, ...owned4 }) : void 0;
3092
+ const first = copying ? await this.intend({ from: source.path, to: staged2, root, ...owned4 }) : void 0;
3093
+ const second = copying && swap ? await this.intend({ from: staged2, to: destination, replace: true, root, ...owned4 }) : void 0;
3094
3094
  await hooks.started?.().catch(async (err) => {
3095
3095
  for (const intent of [first, second]) {
3096
3096
  if (intent) await this.meta.update(intent, () => void 0);
@@ -3101,11 +3101,11 @@ var OpfsFs = class {
3101
3101
  targetStat !== null,
3102
3102
  () => this.place(source.handle, dir, staging, copies)
3103
3103
  ).catch(async (err) => {
3104
- if (first) await this.rollBack(staged, source.path, [first, second]);
3104
+ if (first) await this.rollBack(staged2, source.path, [first, second]);
3105
3105
  throw err;
3106
3106
  });
3107
3107
  if (swap) await this.rm(destination, { recursive: true });
3108
- if (second) await this.intend({ from: staged, to: destination, root, ...owned4 }, second);
3108
+ if (second) await this.intend({ from: staged2, to: destination, root, ...owned4 }, second);
3109
3109
  await this.meta.move(source.path, destination, carry, left);
3110
3110
  this.movePending(source.path, destination);
3111
3111
  if (!moved2) {
@@ -7833,15 +7833,15 @@ var UserDb = class {
7833
7833
  (r) => r !== row && this.fs.resolvePath("/", r.home) === home
7834
7834
  );
7835
7835
  const removeHome = !options.keepHome && !shared2 && HOME_DIR.test(home) && await this.fs.exists(home);
7836
- const staged = `/home/.${name2}.removed-${Date.now()}`;
7836
+ const staged2 = `/home/.${name2}.removed-${Date.now()}`;
7837
7837
  await this.commit(
7838
7838
  formatPasswd(passwd.filter((r) => r !== row)),
7839
7839
  formatGroup(keptGroups),
7840
- removeHome ? () => this.fs.rename(home, staged) : void 0
7840
+ removeHome ? () => this.fs.rename(home, staged2) : void 0
7841
7841
  );
7842
7842
  this.umasks.delete(name2);
7843
7843
  if (removeHome)
7844
- await this.fs.rm(staged, { recursive: true, force: true }).catch(() => void 0);
7844
+ await this.fs.rm(staged2, { recursive: true, force: true }).catch(() => void 0);
7845
7845
  return true;
7846
7846
  });
7847
7847
  }
@@ -8497,7 +8497,7 @@ function satisfies(version, range2) {
8497
8497
  }
8498
8498
 
8499
8499
  // src/version.ts
8500
- var KERNEL_VERSION = true ? "1.168.0" : "0.0.0-dev";
8500
+ var KERNEL_VERSION = true ? "1.168.1" : "0.0.0-dev";
8501
8501
  var OS_RELEASE = "6.0.0-slicc";
8502
8502
  function procVersion() {
8503
8503
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -16299,6 +16299,8 @@ var SYS_TREE = {
16299
16299
  slicc_opfs_ops: (listing) => `${listing.opfsOps ?? 0}
16300
16300
  `,
16301
16301
  slicc_cwd_checks: (listing) => `${listing.cwdChecks ?? 0}
16302
+ `,
16303
+ slicc_package_swaps: ({ swaps }) => `${swaps?.swaps ?? 0} ${(swaps?.longestMs ?? 0).toFixed(1)} ${swaps?.waits ?? 0} ${(swaps?.waitedMs ?? 0).toFixed(1)}
16302
16304
  `,
16303
16305
  slicc_rename_ack_busy: (listing) => `${listing.renameAcks?.busy ?? 0}
16304
16306
  `,
@@ -29015,6 +29017,105 @@ function replayingHops(inner, heldMs = HELD_MS) {
29015
29017
  };
29016
29018
  }
29017
29019
 
29020
+ // src/kernel/package-swaps.ts
29021
+ var SWAP_GAP_MS = 2e3;
29022
+ var STAGES = ["_tmp_", "_pacquet-stage_"];
29023
+ function staged(source, target) {
29024
+ if (parentOf4(source) !== parentOf4(target)) return false;
29025
+ const name2 = target.slice(target.lastIndexOf("/") + 1);
29026
+ const from = source.slice(source.lastIndexOf("/") + 1);
29027
+ return from.startsWith(`.${name2}-`) || STAGES.some((stage) => from.startsWith(`${name2}${stage}`));
29028
+ }
29029
+ var PackageSwaps = class {
29030
+ gaps = /* @__PURE__ */ new Map();
29031
+ isRoot;
29032
+ hold;
29033
+ closed;
29034
+ gapMs;
29035
+ tally = { swaps: 0, longestMs: 0, waits: 0, waitedMs: 0 };
29036
+ constructor(isRoot, hold, closed, gapMs = SWAP_GAP_MS) {
29037
+ this.isRoot = isRoot;
29038
+ this.hold = hold;
29039
+ this.closed = closed;
29040
+ this.gapMs = gapMs;
29041
+ }
29042
+ counts() {
29043
+ return { ...this.tally };
29044
+ }
29045
+ settled() {
29046
+ if (this.gaps.size === 0) return Promise.resolve();
29047
+ const started = performance.now();
29048
+ this.tally.waits++;
29049
+ return Promise.all([...this.gaps.values()].map((gap) => gap.settled)).then(() => {
29050
+ this.tally.waitedMs += performance.now() - started;
29051
+ });
29052
+ }
29053
+ wrap(fs) {
29054
+ return { ...fs, rename: (from, to) => this.rename(fs, from, to) };
29055
+ }
29056
+ async rename(fs, from, to) {
29057
+ const source = fs.resolvePath("/", from);
29058
+ const target = fs.resolvePath("/", to);
29059
+ const gap = this.gaps.get(target);
29060
+ if (gap) return this.land(gap, target, () => fs.rename(from, to));
29061
+ if (source === target || this.gaps.has(source) || staged(source, target) || !this.isRoot(source)) {
29062
+ return fs.rename(from, to);
29063
+ }
29064
+ const release = await this.hold(source);
29065
+ let kept = false;
29066
+ try {
29067
+ await fs.rename(from, to);
29068
+ kept = await fs.stat(`${target}/package.json`).then(
29069
+ (st) => st.isFile,
29070
+ () => false
29071
+ );
29072
+ if (kept) this.open(source, release);
29073
+ } finally {
29074
+ if (!kept) release();
29075
+ }
29076
+ }
29077
+ open(root, release) {
29078
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
29079
+ const gap = {
29080
+ release,
29081
+ done: () => resolve2(),
29082
+ settled: promise,
29083
+ timer: void 0,
29084
+ landing: 0,
29085
+ opened: performance.now()
29086
+ };
29087
+ this.gaps.set(root, gap);
29088
+ this.tally.swaps++;
29089
+ this.arm(root, gap);
29090
+ }
29091
+ arm(root, gap) {
29092
+ const timer = setTimeout(() => this.close(root, gap), this.gapMs);
29093
+ timer.unref?.();
29094
+ gap.timer = timer;
29095
+ }
29096
+ close(root, gap) {
29097
+ if (this.gaps.get(root) !== gap) return;
29098
+ clearTimeout(gap.timer);
29099
+ this.gaps.delete(root);
29100
+ this.tally.longestMs = Math.max(this.tally.longestMs, performance.now() - gap.opened);
29101
+ this.closed();
29102
+ gap.done();
29103
+ gap.release();
29104
+ }
29105
+ async land(gap, root, run2) {
29106
+ gap.landing++;
29107
+ clearTimeout(gap.timer);
29108
+ try {
29109
+ await run2();
29110
+ } catch (err) {
29111
+ gap.landing--;
29112
+ if (gap.landing === 0 && this.gaps.get(root) === gap) this.arm(root, gap);
29113
+ throw err;
29114
+ }
29115
+ this.close(root, gap);
29116
+ }
29117
+ };
29118
+
29018
29119
  // src/kernel/rename-log.ts
29019
29120
  var RENAME_LOG_SIZE = 4096;
29020
29121
  function within9(path, root) {
@@ -29083,6 +29184,18 @@ var PackageLocks = class {
29083
29184
  const names2 = roots.map((root) => replacingLock(this.metadata, root));
29084
29185
  return shared(this.locks, names2, run2, false);
29085
29186
  }
29187
+ holding(root) {
29188
+ const { locks } = this;
29189
+ if (!locks) return Promise.resolve(() => {
29190
+ });
29191
+ const { promise: held, resolve: release } = Promise.withResolvers();
29192
+ return new Promise((granted2, failed3) => {
29193
+ locks.request(replacingLock(this.metadata, root), { mode: "exclusive" }, () => {
29194
+ granted2(() => release());
29195
+ return held;
29196
+ }).catch(failed3);
29197
+ });
29198
+ }
29086
29199
  };
29087
29200
 
29088
29201
  // src/kernel/settling.ts
@@ -31592,6 +31705,11 @@ var Launcher = class {
31592
31705
  jobs = new JobTable();
31593
31706
  locks;
31594
31707
  packageLocks;
31708
+ swaps = new PackageSwaps(
31709
+ (path) => this.packageRootIs(path),
31710
+ (root) => this.packageLocks.holding(root),
31711
+ () => this.resetCatalogs()
31712
+ );
31595
31713
  fifos = new FifoTable();
31596
31714
  memories;
31597
31715
  sems = new SemTable();
@@ -31882,7 +32000,7 @@ var Launcher = class {
31882
32000
  commands(scope = "") {
31883
32001
  let found = this.catalogs.get(scope);
31884
32002
  if (!found) {
31885
- found = this.trustedAppended().then(async (appended2) => {
32003
+ found = this.swaps.settled().then(() => this.trustedAppended()).then(async (appended2) => {
31886
32004
  const tiers = await this.tiers(scope, appended2);
31887
32005
  const refused3 = [];
31888
32006
  const commands = await scanCommands(
@@ -32727,6 +32845,13 @@ var Launcher = class {
32727
32845
  if (readsEtcHostname(program)) setOrDrop(env, "HOSTNAME", own);
32728
32846
  return { planned, env, program, exeId };
32729
32847
  }
32848
+ packageRootIs(path) {
32849
+ const at = normalizePath(path);
32850
+ const within10 = `${at}/`;
32851
+ const roots = [this.modulesDir, ...this.appended];
32852
+ const root = PACKAGE_ROOT.exec(within10)?.[1] ?? packageRootOf(within10, roots);
32853
+ return root === at;
32854
+ }
32730
32855
  packageRoots(planned) {
32731
32856
  const packages2 = [this.modulesDir, ...this.appended];
32732
32857
  const roots = /* @__PURE__ */ new Set();
@@ -32912,6 +33037,7 @@ var Launcher = class {
32912
33037
  opfsOps: this.rootFs.opfsOps?.() ?? 0,
32913
33038
  cwdChecks: this.cwds.checks,
32914
33039
  renameAcks: this.rootFs.renameWaits?.(),
33040
+ swaps: this.swaps.counts(),
32915
33041
  staleFds: staleFdCount(),
32916
33042
  fileIds: this.rootFs.fileIdWrites?.() ?? 0,
32917
33043
  holds: this.rootFs.holdCounts?.()
@@ -33168,6 +33294,7 @@ var Launcher = class {
33168
33294
  spawner(ppid, report) {
33169
33295
  return async (req, fds, grant) => {
33170
33296
  const booked = this.takeTicket(req);
33297
+ await this.swaps.settled();
33171
33298
  if (grant && !await this.grantedCwd(grant.caller, req.cwd)) {
33172
33299
  await fds.closeAll();
33173
33300
  throw new SpawnError("EACCES");
@@ -34080,10 +34207,12 @@ var Launcher = class {
34080
34207
  nodes.changed = (path) => this.changes.publish(path);
34081
34208
  nodes.movedAway = (from, to) => this.moved(from, to, nodes);
34082
34209
  nodes.dirtied = (delta) => this.changes.dirtied(delta);
34083
- return unlinked.wrap(keepingOpen(fs, nodes, (path) => this.changes.publish(path)));
34210
+ const kept = keepingOpen(fs, nodes, (path) => this.changes.publish(path));
34211
+ return this.swaps.wrap(unlinked.wrap(kept));
34084
34212
  }
34085
34213
  async starting(argv, dir, account, tags, base) {
34086
34214
  const tag = tags?.[0];
34215
+ await this.swaps.settled();
34087
34216
  this.resetCatalogs();
34088
34217
  const scope = account.uid === 0 ? "" : pnpmHomeOf(account, this.pnpmHome);
34089
34218
  const start2 = dir ?? "/";