@ai-ecoverse/slicc-kernel 1.126.0 → 1.126.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
@@ -648,6 +648,8 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
648
648
 
649
649
  `/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. Files directly in `/`, and directories created there after a process started, are OPFS as well, the same for Emscripten and WASI programs; only `/dev` and `/proc` live in each process's memory. A program's `mkdir` (WASI `path_create_directory`, Emscripten's `mkdir`) is exclusive, as on Linux: it fails with `EEXIST` when the name exists, and of several processes or threads making one name at once exactly one succeeds, so a directory works as a lock. Under a missing directory it fails with `ENOENT`, as on Linux (`mkdir -p` and `os.makedirs` make the parents in the program); a JS program's `ctx.fs.mkdir` makes parents itself. The kernel makes the system `PNPM_HOME` (`/usr/local/share/pnpm`, or the embedder's) when it starts, since pnpm makes only its `bin` directory there. A full origin storage (`QuotaExceededError`, which also comes early in a browser's private or off-the-record contexts, whatever `navigator.storage.estimate()` says) is `ENOSPC`, never retried. A read that meets `NotFoundError` while a writer outside the kernel swaps the file in with `createWritable` (and a `stat`, `lstat` or `utimes` that meets it) looks the name up again, up to three times, before it fails ([#1075](https://github.com/ai-ecoverse/slicc-kernel/issues/1075)), so a program whose package is overwritten in place keeps starting. An OPFS or IndexedDB failure that has no POSIX counterpart is `EIO`; its message names the DOM error and the operation (`lookup`, `read`, `write`, `commit` or `sidecar`), and the kernel worker logs the first of each kind to its console. File contents are buffered per open file and written back on close, `fsync` and exit, so a refused write (`ENOSPC`, `EIO`) surfaces at that close or `fsync`, and a program that ignores close errors doesn't see it (`echo x > f` can exit 0 with `f` empty, as with any buffered write-back). A write-back error a close has already reported to a WASI program isn't raised again when it exits, so the program keeps its own exit status. Metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
650
650
 
651
+ **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 reads OPFS directly and can see the tree mid-move. 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. While another tab shares the root, the split tree stays as it is (both names visible, each with part of the files) until a boot that's alone.
652
+
651
653
  A lookup follows at most 40 symlinks, as Linux does: the 41st is `ELOOP`, for WASI programs, JS programs and the kernel API alike, with or without `..` in a link's target. Emscripten programs resolve paths in their glue, which stops one link earlier, at 40.
652
654
 
653
655
  Directories are renamed with `FileSystemHandle.move()` where available, else by copy and delete.
@@ -1098,6 +1098,10 @@ var STALE = /* @__PURE__ */ new Set(["NotFoundError", "TypeMismatchError"]);
1098
1098
  var REPLACED_ATTEMPTS = 4;
1099
1099
  var RECONCILE_LISTINGS = 8;
1100
1100
  var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
1101
+ var RENAMES = "/.slicc-renames";
1102
+ function movable(handle) {
1103
+ return typeof handle.move === "function";
1104
+ }
1101
1105
  var ERRNO_BY_DOM_ERROR = {
1102
1106
  NotFoundError: "ENOENT",
1103
1107
  TypeMismatchError: "ENOTDIR",
@@ -2271,9 +2275,9 @@ var OpfsFs = class {
2271
2275
  }
2272
2276
  const now = Date.now();
2273
2277
  const moving = await this.movedOwner(source);
2274
- const carry = (entry) => ({
2278
+ const carry = (entry2) => ({
2275
2279
  ino: inodeOf(source.path),
2276
- ...occupied(entry),
2280
+ ...occupied(entry2),
2277
2281
  ...moving,
2278
2282
  path: destination,
2279
2283
  ctimeMs: now
@@ -2289,29 +2293,127 @@ var OpfsFs = class {
2289
2293
  const name2 = baseOf(destination);
2290
2294
  const swap = targetStat?.isDirectory === true;
2291
2295
  const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
2292
- this.forget(destination);
2293
- const timed2 = await this.explicitTimes(source.path);
2294
- const moved2 = await this.replacingTarget(
2295
- targetStat !== null,
2296
- () => this.place(source.handle, dir, staging)
2296
+ const copying = sourceStat.isDirectory && !movable(source.handle);
2297
+ const staged = resolvePath(parent.path, staging);
2298
+ const place = async () => {
2299
+ this.forget(destination);
2300
+ const timed2 = await this.explicitTimes(source.path);
2301
+ const first = copying ? await this.intend({ from: source.path, to: staged }) : void 0;
2302
+ const second = copying && swap ? await this.intend({ from: staged, to: destination, replace: true }) : void 0;
2303
+ const moved2 = await this.replacingTarget(
2304
+ targetStat !== null,
2305
+ () => this.place(source.handle, dir, staging)
2306
+ ).catch(async (err) => {
2307
+ if (first) await this.rollBack(staged, source.path, [first, second]);
2308
+ throw err;
2309
+ });
2310
+ if (swap) await this.rm(destination, { recursive: true });
2311
+ if (second) await this.intend({ from: staged, to: destination }, second);
2312
+ await this.meta.move(source.path, destination, carry, left);
2313
+ this.movePending(source.path, destination);
2314
+ if (!moved2) {
2315
+ await this.rm(source.path, { recursive: true });
2316
+ await this.meta.update(source.path, () => left);
2317
+ }
2318
+ if (first) await this.meta.update(first, () => void 0);
2319
+ let copied = !moved2;
2320
+ if (swap) {
2321
+ const hidden = await dir.getDirectoryHandle(staging);
2322
+ const placed = await this.place(hidden, dir, name2);
2323
+ if (!placed) await dir.removeEntry(staging, { recursive: true });
2324
+ copied ||= !placed;
2325
+ }
2326
+ if (second) await this.meta.update(second, () => void 0);
2327
+ if (copied) await this.restamp(destination, timed2);
2328
+ this.changed(source.path, destination);
2329
+ this.touch(Date.now(), parentOf3(source.path), parent.path);
2330
+ };
2331
+ if (!copying) return place();
2332
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
2333
+ const entry = { from: source.path, to: destination, done: promise };
2334
+ this.renamingDirs.add(entry);
2335
+ try {
2336
+ await place();
2337
+ } finally {
2338
+ this.renamingDirs.delete(entry);
2339
+ resolve2();
2340
+ }
2341
+ }
2342
+ renamingDirs = /* @__PURE__ */ new Set();
2343
+ intents = 0;
2344
+ async intend(rename, at = `${RENAMES}/${crypto.randomUUID()}`) {
2345
+ const record = { ...rename, at: Date.now() * 1e3 + this.intents++ % 1e3 };
2346
+ await this.meta.update(at, () => ({ path: at, rename: record }));
2347
+ return at;
2348
+ }
2349
+ async rollBack(from, to, intents) {
2350
+ try {
2351
+ if (await this.real(from)) {
2352
+ const moved2 = await this.locate(from, false);
2353
+ const back = await this.locate(parentOf3(to), true);
2354
+ await this.copy(
2355
+ moved2.handle,
2356
+ back.handle,
2357
+ baseOf(to)
2358
+ );
2359
+ const [dir, name2] = await this.parent(from);
2360
+ await this.inDir(dir, (parent) => parent.removeEntry(name2, { recursive: true }));
2361
+ }
2362
+ this.forget(from);
2363
+ this.forget(to);
2364
+ for (const intent of intents) if (intent) await this.meta.update(intent, () => void 0);
2365
+ } catch {
2366
+ }
2367
+ }
2368
+ async finishRenames() {
2369
+ const intents = (await this.meta.under(RENAMES)).filter((entry) => entry.rename !== void 0).sort(
2370
+ (a, b) => a.rename.at - b.rename.at
2297
2371
  );
2298
- if (swap) await this.rm(destination, { recursive: true });
2299
- await this.meta.move(source.path, destination, carry, left);
2300
- this.movePending(source.path, destination);
2301
- if (!moved2) {
2302
- await this.rm(source.path, { recursive: true });
2303
- await this.meta.update(source.path, () => left);
2304
- }
2305
- let copied = !moved2;
2306
- if (swap) {
2307
- const staged = await dir.getDirectoryHandle(staging);
2308
- const placed = await this.place(staged, dir, name2);
2309
- if (!placed) await dir.removeEntry(staging, { recursive: true });
2310
- copied ||= !placed;
2311
- }
2312
- if (copied) await this.restamp(destination, timed2);
2313
- this.changed(source.path, destination);
2314
- this.touch(Date.now(), parentOf3(source.path), parent.path);
2372
+ for (const entry of intents) {
2373
+ const { from, to, replace } = entry.rename;
2374
+ const left = await this.real(from);
2375
+ if (left && replace && await this.real(to)) await this.rm(to, { recursive: true });
2376
+ if (left) {
2377
+ const source = await this.locate(from, false);
2378
+ const target = await this.locate(parentOf3(to), true);
2379
+ await this.copy(
2380
+ source.handle,
2381
+ target.handle,
2382
+ baseOf(to)
2383
+ );
2384
+ const [dir, name2] = await this.parent(from);
2385
+ await this.inDir(dir, (parent) => parent.removeEntry(name2, { recursive: true }));
2386
+ this.forget(from);
2387
+ this.forget(to);
2388
+ }
2389
+ if ((await this.meta.under(from)).length > 0) {
2390
+ await this.meta.move(from, to, (moved2) => ({ ...occupied(moved2), path: to }));
2391
+ }
2392
+ await this.meta.update(entry.path, () => void 0);
2393
+ }
2394
+ return intents.length;
2395
+ }
2396
+ async settledRename(path) {
2397
+ if (this.renamingDirs.size === 0 || typeof path !== "string") return;
2398
+ const at = normalizePath(path);
2399
+ for (let real; ; ) {
2400
+ const busy = this.renameAt(at) ?? (real === void 0 ? void 0 : this.renameAt(real));
2401
+ if (busy) {
2402
+ await busy.done;
2403
+ continue;
2404
+ }
2405
+ if (real !== void 0 || at === "/") return;
2406
+ const parent = await this.realpath(parentOf3(at), true).catch(() => parentOf3(at));
2407
+ real = resolvePath(parent, baseOf(at));
2408
+ if (real === at) return;
2409
+ }
2410
+ }
2411
+ renameAt(at) {
2412
+ for (const m of this.renamingDirs) {
2413
+ if (within2(at, m.from) || within2(at, m.to) || at === parentOf3(m.from) || at === parentOf3(m.to))
2414
+ return m;
2415
+ }
2416
+ return void 0;
2315
2417
  }
2316
2418
  async replacingTarget(replaces, op) {
2317
2419
  if (!replaces) return op();
@@ -2353,15 +2455,29 @@ var OpfsFs = class {
2353
2455
  () => false
2354
2456
  );
2355
2457
  if (moved2) return true;
2458
+ const before = this.fileMoves;
2356
2459
  try {
2357
2460
  await this.copy(handle, dir, name2);
2358
2461
  } catch (err) {
2359
- if (handle.kind === "directory") await dir.removeEntry(name2, { recursive: true });
2462
+ if (handle.kind === "directory" && this.fileMoves === before) {
2463
+ await dir.removeEntry(name2, { recursive: true });
2464
+ }
2360
2465
  throw translate(err, name2);
2361
2466
  }
2362
2467
  return false;
2363
2468
  }
2469
+ fileMoves = 0;
2364
2470
  async copy(handle, dir, name2) {
2471
+ if (handle.kind === "file" && movable(handle)) {
2472
+ const moved2 = await handle.move?.(dir, name2).then(
2473
+ () => true,
2474
+ () => false
2475
+ );
2476
+ if (moved2) {
2477
+ this.fileMoves++;
2478
+ return;
2479
+ }
2480
+ }
2365
2481
  if (handle.kind === "file") {
2366
2482
  const writable2 = await (await dir.getFileHandle(name2, { create: true })).createWritable();
2367
2483
  await writable2.write(await handle.getFile());
@@ -2498,6 +2614,7 @@ var OpfsFs = class {
2498
2614
  }
2499
2615
  async reconcile() {
2500
2616
  if (this.remote && await this.remote.peered()) return 0;
2617
+ await this.finishRenames();
2501
2618
  let removed = 0;
2502
2619
  const entries = await this.meta.all();
2503
2620
  const carried = new Set(entries.filter((e) => !e.vacated).map((e) => e.ino));
@@ -2528,7 +2645,9 @@ var READS = [
2528
2645
  "readdir",
2529
2646
  "readdirStat",
2530
2647
  "readlink",
2531
- "realpath"
2648
+ "realpath",
2649
+ "version",
2650
+ "readVersioned"
2532
2651
  ];
2533
2652
  var WRITES = [
2534
2653
  "writeFile",
@@ -2554,6 +2673,7 @@ for (const [names2, op] of [
2554
2673
  const inner = proto[name2];
2555
2674
  proto[name2] = async function(...args) {
2556
2675
  try {
2676
+ if (op === "read") await this.settledRename(args[0]);
2557
2677
  return await inner.apply(this, args);
2558
2678
  } catch (err) {
2559
2679
  throw translate(err, String(args[0]), op, false);
@@ -6699,7 +6819,7 @@ function satisfies(version, range2) {
6699
6819
  }
6700
6820
 
6701
6821
  // src/version.ts
6702
- var KERNEL_VERSION = true ? "1.126.0" : "0.0.0-dev";
6822
+ var KERNEL_VERSION = true ? "1.126.1" : "0.0.0-dev";
6703
6823
  var OS_RELEASE = "6.0.0-slicc";
6704
6824
  function procVersion() {
6705
6825
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC