@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 +15 -0
- package/dist/kernel-worker.js +140 -11
- package/dist/kernel-worker.js.map +4 -4
- package/dist/meta.js.map +1 -1
- package/dist/node-process-worker.cjs +3 -1
- package/dist/node-process-worker.cjs.map +2 -2
- package/dist/node-process-worker.js +3 -1
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +140 -11
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +3 -1
- package/dist/process-worker.js.map +2 -2
- package/dist/types/kernel/package-swaps.d.ts +25 -0
- package/dist/types/kernel/proc-info.d.ts +6 -0
- package/dist/types/kernel/replacing.d.ts +1 -0
- package/dist/types/launcher.d.ts +2 -0
- package/package.json +1 -1
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.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -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
|
|
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:
|
|
3093
|
-
const second = copying && swap ? await this.intend({ from:
|
|
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(
|
|
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:
|
|
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
|
|
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,
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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 ?? "/";
|