@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 +2 -0
- package/dist/kernel-worker.js +147 -27
- package/dist/kernel-worker.js.map +3 -3
- package/dist/meta.js.map +2 -2
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +147 -27
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +1 -1
- package/dist/types/fs/meta.d.ts +6 -0
- package/dist/types/fs/opfs.d.ts +8 -0
- package/package.json +2 -2
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.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -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 = (
|
|
2278
|
+
const carry = (entry2) => ({
|
|
2275
2279
|
ino: inodeOf(source.path),
|
|
2276
|
-
...occupied(
|
|
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
|
-
|
|
2293
|
-
const
|
|
2294
|
-
const
|
|
2295
|
-
|
|
2296
|
-
|
|
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
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
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"
|
|
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.
|
|
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
|