@ai-ecoverse/slicc-kernel 1.124.1 → 1.124.3
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 +11 -3
- package/dist/kernel-worker.js +108 -36
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +42 -11
- package/dist/node-process-worker.js.map +3 -3
- package/dist/node.js +108 -36
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +42 -11
- package/dist/process-worker.js.map +3 -3
- package/dist/types/fs/remote.d.ts +8 -0
- package/dist/types/kernel/proc-info.d.ts +3 -0
- package/dist/types/kernel/wasm-memories.d.ts +8 -2
- package/dist/types/realm/live-vfs-fs.d.ts +1 -0
- package/dist/types/realm/sync-fs-dispatch.d.ts +1 -0
- package/dist/types/realm/sync-sab-bridge.d.ts +2 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -645,7 +645,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
|
|
|
645
645
|
- **The same file:** a lock is keyed by the file's device and inode, which both kernels read from the shared sidecar, so a rename by either tab keeps it. A file removed and created again at the same path has the same key, as it already has within one kernel.
|
|
646
646
|
- **Versions:** the channel and Web Lock names carry a protocol version (`v1`), and a kernel ignores messages that aren't v1. So kernels on different protocol versions (two tabs across an update) don't share locks, as before #675.
|
|
647
647
|
- **Not across tabs:** the imports module's whole-file `tryLock` (per kernel), and locks on other mounts.
|
|
648
|
-
- **Another tab's writes** ([#959](https://github.com/ai-ecoverse/slicc-kernel/issues/959)): a program's names and stats are cached in its process, and its kernel refreshes them when the kernel itself changes a file. While another kernel shares the root (a second tab), the kernel marks that view foreign, and a program then looks names up and reads stats again on every look. So a program that keeps checking a file sees what the other tab wrote, synced and closed. A kernel alone caches as before. While another kernel shares the root, a kernel also tells it of each file it writes ([#962](https://github.com/ai-ecoverse/slicc-kernel/issues/962)). The other kernel then drops the clean contents of that file, both its own and its programs', so a descriptor tab B already has open reads tab A's new bytes on its next read, as Linux's shared page cache does. The notice is a message, so a read that races it by less than a message's delivery can still see the old bytes, as with any reader that doesn't lock.
|
|
648
|
+
- **Another tab's writes** ([#959](https://github.com/ai-ecoverse/slicc-kernel/issues/959)): a program's names and stats are cached in its process, and its kernel refreshes them when the kernel itself changes a file. While another kernel shares the root (a second tab), the kernel marks that view foreign, and a program then looks names up and reads stats again on every look. So a program that keeps checking a file sees what the other tab wrote, synced and closed. A kernel alone caches as before. Stats are always read fresh while the root is shared. An Emscripten program keeps the names it has looked up, directories and files ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)), until its next call that isn't by path: a read, a write, a poll, a sleep, a wait, a spawn, a look at the clock or a draw of random bytes. Within one run of path-only calls (lstat, chmod, readlink and the like) with none of those in between, an answer the program takes from its own node, such as the node's type, can lag behind the other tab until that next call, as a path call racing a rename does on Linux. A program can only learn that the other tab acted through one of those calls, a new process or a stat, and all of them refresh what it sees. While another kernel shares the root, a kernel also tells it of each file it writes ([#962](https://github.com/ai-ecoverse/slicc-kernel/issues/962)). The other kernel then drops the clean contents of that file, both its own and its programs', so a descriptor tab B already has open reads tab A's new bytes on its next read, as Linux's shared page cache does. The notice is a message, so a read that races it by less than a message's delivery can still see the old bytes, as with any reader that doesn't lock.
|
|
649
649
|
- **`slicc.fcntl_lock(fd, cmd, flock_ptr) -> errno`** ([#722](https://github.com/ai-ecoverse/slicc-kernel/issues/722)): a WASIX program's `fcntl` record locks reach the same table as an Emscripten program's, so the two see each other's locks.
|
|
650
650
|
- **cmd:** `12` `F_GETLK`, `13` `F_SETLK` or `14` `F_SETLKW`. Any other cmd is `EINVAL`.
|
|
651
651
|
- **The struct:** musl's wasm32 `struct flock`, 32 bytes: `l_type` i16 at 0 (`0` read, `1` write, `2` unlock), `l_whence` i16 at 2 (`0` SET, `1` CUR, `2` END, resolved against the descriptor's offset or size), `l_start` i64 at 8, `l_len` i64 at 16 (`0` to EOF and beyond, negative before `l_start`), `l_pid` i32 at 24. The kernel reads and writes only those five fields and never the padding.
|
|
@@ -942,11 +942,14 @@ The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#us
|
|
|
942
942
|
### How many processes at once
|
|
943
943
|
|
|
944
944
|
In a browser every process worker of the page shares one renderer. V8 reserves about 8 GiB of address space for each 32-bit wasm memory, whatever its declared maximum, and its sandbox holds about 1 TiB. So a page runs about 125 wasm processes (or programs that own a wasm memory) at once, counting everything in the renderer (#970). Node has no such ceiling, and neither does Linux. The kernel keeps processes inside that ceiling:
|
|
945
|
-
- **The ceiling:** 112 by default in a browser and none in Node. It's the kernel's count of live wasm processes, with the kernel's other wasm memories in the page left outside it. When an instantiation runs out of memory anyway, the kernel learns the real ceiling at that moment (the live count minus one), and that learned value wins over the default.
|
|
945
|
+
- **The ceiling:** 112 by default in a browser and none in Node. It's the kernel's count of live wasm processes, with the kernel's other wasm memories in the page left outside it. When an instantiation runs out of memory anyway, the kernel learns the real ceiling at that moment (the live count minus one), and that learned value wins over the default. A learned ceiling is never below 16, since an OOM at a low count means something outside it (another tab in the same renderer) filled the renderer. It's raised by 8 for each minute without another OOM, back to the default.
|
|
946
946
|
- **Admission:** a spawn, fork, exec or `kernel.run` at the ceiling waits for another process to exit. It keeps waiting as long as processes exit or are admitted, and fails with `EAGAIN` only after 10 s with neither. A long `make -j` or `xargs -P` therefore queues, and a pipeline that needs more live processes than the ceiling fails cleanly. bash's own retry on a fork's `EAGAIN` (`fork: retry: Resource temporarily unavailable`) handles the rest.
|
|
947
947
|
- **Close to the ceiling** (within 16 of it), the kernel waits for each new process to finish instantiating before the spawn returns. If it ran out of memory, the kernel starts it again, quietly, once there's room, so the caller sees no failure.
|
|
948
948
|
- **The rare residual case:** a process that fails to instantiate with nobody waiting on it, which happens only when something outside the count fills the renderer. It ends with status 126 and `<program>: cannot allocate memory for a new process (too many processes at once)` on its stderr, never a JS `RangeError`.
|
|
949
|
-
-
|
|
949
|
+
- **Below a learned ceiling,** a process that still runs out of memory is started again once another process exits, and the spawn fails with `EAGAIN` after 10 s with none.
|
|
950
|
+
- **`/proc/sys/kernel/slicc_wasm_memories`** reads `<live> <ceiling> <source>`. The source is `default`, `learned`, or `none` (with the ceiling 0).
|
|
951
|
+
|
|
952
|
+
`/proc/sys/kernel/slicc_shared_root` reads `1` while another kernel shares the root (a tab, or a second kernel in the page) and `0` otherwise. A peer whose kernel ends (its tab closes or reloads) stops counting at once: each kernel queues a request for every peer's alive Web Lock, and the grant tells it the peer has gone ([#984](https://github.com/ai-ecoverse/slicc-kernel/issues/984)). `/proc/sys/kernel/slicc_fs_calls` reads the number of path calls (lookups, stats, opens, creates, removals and the like) the kernel has served its programs since it started.
|
|
950
953
|
|
|
951
954
|
### Metadata
|
|
952
955
|
|
|
@@ -1002,6 +1005,8 @@ npm run test:unit
|
|
|
1002
1005
|
|
|
1003
1006
|
`npm test` builds `dist/` and runs the integration tests in Chromium from `playwright-core`, over raw CDP through the [harness from slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web#integration-test-harness), against a cross-origin isolated test page that installs bash and coreutils into OPFS. Each test writes screenshots, console logs and CPU profiles to `artifacts/`; V8 coverage from the page and every worker, mapped back to `src/` through the source maps, goes to `coverage/`, and `artifacts/hotspots.md` lists where the time went.
|
|
1004
1007
|
|
|
1008
|
+
The recorder behind that coverage changes timing and memory in long runs. It attaches to every page and worker with the debugger, profiler and precise coverage on, and pauses every process worker at its exit breakpoint (`exits` in `test/integration/chrome.mjs`) until the coverage of that worker is collected, keeping every collected script until the run ends. A run with thousands of process exits or hundreds of page loads therefore grows the test process by gigabytes, and each exit waits longer as it grows, so a parent can sit in `proc-exec` for tens of seconds while its child has already finished; each further page load also gets slower (in one tab reloaded 300 times, a boot went from 80 to 500 ms with no kernel in any other tab). When a long run looks stuck or leaky, measure it again with a `launch` that passes `coverage: ['/nothing/']` and no `exits` before blaming the kernel (#991).
|
|
1009
|
+
|
|
1005
1010
|
Unit tests live in `test/unit/`, which stays out of git. They run real wasm bash and coreutils through the whole stack in Node `worker_threads` against an in-memory OPFS. The pre-commit hook runs them under coverage and requires every changed line in `src/` to be covered. `tools/heavy.sh` runs them two files at a time, in one of two unit slots of the host (`/tmp/slicc-unit-N.lock`, waiting while both are taken; a lock it can't take for any other reason, such as a lock file it can't open, ends the run with `lockf`'s status; `SLICC_UNIT_SLOTS` sets how many, `SLICC_UNIT_SLOT=0` takes none, as for CI; a HUP, INT or TERM to `heavy.sh` stops the run it holds or waits for), never in the browser runs' slots (those go through `slicc-heavy`; `SLICC_HEAVY_SLOT=1` runs the unit tests through `~/.bb/coord-bin/slicc-heavy` instead, when it is there), and stops the run if its processes use more than 3,000,000 KB together or 2,000,000 KB in one process (2,800,000 KB for the command itself, the coverage tool that merges the report at the end; `SLICC_HEAVY_ROOT_KB`, never below the per-process cap), or run longer than 900 s once it has its slot (`SLICC_HEAVY_TIMEOUT`; each test also has 180 s) (`SLICC_HEAVY_REPORT=1` prints the peak). `SLICC_HEAVY_TREE_KB` and `SLICC_HEAVY_PROC_KB` raise the caps, but only for measuring a run under a higher ceiling that still stops a runaway, never to get a commit through. The guard follows the command's process group and every process it has seen, including children that leave the group, but a process that forks, calls `setsid` and whose parent exits within one 0.25 s sample can escape: macOS has no unprivileged containment, and a test that daemonizes is a bug. The compiler tests (zig, rustc and cargo), each a few GB, are integration tests in `test/integration/compilers.test.mjs`, which CI runs.
|
|
1006
1011
|
|
|
1007
1012
|
The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web), which also provides the `slicc-lint-comments` (no comments anywhere), `slicc-no-unit-tests` (no unit tests in git) and `slicc-diff-cover` (100% coverage of changed lines) commands that `npm run lint` and the pre-commit hook use.
|
|
@@ -1012,6 +1017,8 @@ The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc
|
|
|
1012
1017
|
|
|
1013
1018
|
`test/integration/fixtures/preh1stat/` is a tiny Emscripten program (`preh1stat.c`) that prints its `getuid`/`geteuid`/`getgid`/`getegid` and the owner of each path it's given. It's built **without** the credential library (`slicc_cred_js`), with today's emcc from homescoop's pinned `emsdk@0.4.0`, so it stands for the packages installed before H1. Those see every file as `1000:1000` (`stat-owner.test.mjs`), and every published `@ai-ecoverse/wasm-*` package now links the library, so no package can carry that row. Build it with `test/integration/fixtures/preh1stat/build.sh`: it fetches `emsdk@0.4.0` unless `emcc` is on `PATH`, `PREH1STAT_BUILD` sets its work directory, and it refuses a build whose glue carries the credential library.
|
|
1014
1019
|
|
|
1020
|
+
`test/integration/fixtures/clocklink/` is a tiny Emscripten program (`clocklink.c`) for `crowded-lookups.test.mjs` (#984). It creates the file named by its second argument (the signal to the other kernel), reads the link named by its first, then reads only the wall clock until the deadline given as its third argument (milliseconds since the epoch), reads the link again and prints both answers (`first=EINVAL second=t`). Build it with `test/integration/fixtures/clocklink/build.sh`, which fetches `emsdk@0.4.0` (emcc 4.0.23) unless `emcc` is on `PATH`; `CLOCKLINK_BUILD` sets its work directory. The committed build is `bin/clocklink` sha256 `0096e2ff68fb380d1b71aaa5fae64d96d6fe60aa2a3933e7207394d30436bf72` and `bin/clocklink.wasm` sha256 `e251dfa3b9d19d855cd0704a9cf67e7a7d28270083292ecde18809932bfbba16`.
|
|
1021
|
+
|
|
1015
1022
|
`test/integration/fixtures/linktest/` is a WASIX program (`linktest.c`) that calls `linkat(AT_FDCWD, from, AT_FDCWD, to, AT_SYMLINK_FOLLOW)` and prints the new name's type, whether it shares the target's inode, and its link count, for `link-follow.test.mjs`. Coreutils' `ln -L` (Emscripten) takes another path, so the WASI program covers `path_link` with the follow flag. Build it with homescoop's pinned wasixcc and wasix-sysroot 2025.9.30-28: `INSTALL_WASIXCC=<homescoop>/scripts/install-wasixcc.sh WASIX_SYSROOT_28=<unpacked -28 package> test/integration/fixtures/linktest/build.sh`.
|
|
1016
1023
|
|
|
1017
1024
|
`tools/big-file.mjs [bytes]` is a manual check, not part of any suite. It writes a file of that size (64 MiB by default) through a tmpfs mount with the unit-test launcher, reads it back, and prints the peak RSS and its ratio to the file size. A 256 MiB file peaks at about 1.8 times its size. Before ranged I/O (#127), a 1.1 GiB file peaked at 18 GB of RSS. **Do not run it past 1 GiB on a workstation.** It refuses anything over 256 MiB unless `SLICC_BIG_FILE_I_HAVE_THE_RAM=1` is set. Measure a small file and extrapolate instead. The unit suite checks a 32 MiB file the same way, and allows a fixed 192 MiB for garbage V8 has yet to collect, which dominates at that size.
|
|
@@ -1055,6 +1062,7 @@ The tools in `tools/cert-review/`:
|
|
|
1055
1062
|
|
|
1056
1063
|
- **Which pull requests:** those that change shipped behaviour, by a conservative path filter (`behaviourFiles` in `lib.mjs`): `src/`, `build.mjs`, `package.json` and its lock, `tsconfig*.json`, `.releaserc*`, `tools/release-recover.mjs`, the workflows that build or publish (`release.yml`, `ci.yml`), and the reviewer's own code (`tools/cert-review/`, `cert-review*.yml`, `.github/runner.mjs`). **Main's code decides:** every job checks out the pull request's base and fetches only the trees under test, so the path filter, pack, suite, findings, the instructions and record all run from `main`. The merge group's lookup loads its tools from the protected branch itself (`merge_group.base_ref`), not from the group's `base_sha`, which can be the speculative commit of the group ahead and carry the reviewer changes of the pull requests queued before it; `base_sha` is only the lookup's input. A pull request that changes the reviewer is reviewed by `main`'s reviewer. The workflow file of a `pull_request` run still comes from the pull request, so a change to the reviewer is held by the **certification self-guard**, the shared composite action `slicc-shared-web/.github/actions/cert-self-guard` at a pinned sha. Its light `self-guard` job runs on every pull request event and every merge group. When the diff touches the reviewer (`.github/workflows/cert*.yml`, `tools/cert-review/**`, `.github/runner.mjs` and the guard's own paths), `cert-review` is red with `reviewer change: needs coordinator review (cert-review-approved on this head)` and no review runs. The label is the Kernel Coordinator's alone: they add `cert-review-approved` after reading the change, and the review then starts with `merge-ok`. The approval is bound to the head's arrival on GitHub, and a push removes the label. A merge group that holds an unapproved reviewer change is red too. If the guard doesn't finish, the status is an error that asks to re-run the failed jobs. **The residual:** the pull request's YAML still decides what runs, so the guard catches an accidental edit, not a deliberate rewrite of this workflow. The pull request that adds this workflow can't pass its own check, because its base has no tools yet. Any other pull request gets a green status that says `not behaviour: <files>`, so the skip is recorded.
|
|
1057
1064
|
- **A gate's silence never reads as a pass:** every tool a workflow gates on (`pr.mjs plan`, `same` and `lookup`, `same.mjs`, `pack.mjs`, `delta.mjs`, `suite.mjs`, `compat.mjs`, `findings.mjs`, `record.mjs`, `verify.mjs`) ends with one verdict line, `<tool>: PASS <what>` or `<tool>: FAIL <why>`, from the shared `verdict()` in `lib.mjs`. `tools/cert-review/gate.sh [--either|--evidence] <tool> <command>` runs a tool, passes its output through, and exits 70 with an error when the tool's last line starting `<tool>: ` isn't a PASS or FAIL verdict, so a tool replaced by `true`, an early exit 0 or an empty run fails its step. By default the step passes only on PASS with exit 0; a PASS with a non-zero exit, or a FAIL with exit 0, fails it. `--either` keeps the tool's own exit, for tools whose FAIL is evidence that record judges later (suite, compat). `--evidence` needs only the verdict, for the NEGATIVE run on main's tree, which is expected to fail. `suite: PASS` means exit 0, no failures and every expected file has results; `compat: PASS` means a run turned up with no regressions. `prompt.mjs` writes the review prompt to stdout, which goes into a step output, so it prints no verdict; a missing prompt leaves the review without `verdict.json`, and record marks that INCOMPLETE. The gate reads the tool's stdout, and no tool passes the pull request's output there: suite pipes the tests' output into its log file, and compat prints only its own lines, so a test that prints `suite: PASS` can't stand in for a silent suite. A tool that let such output through would need its verdict on a separate descriptor. Because the workflow of a pull request runs main's tools, the tools gain their verdicts first, and the workflows call them through `gate.sh` in the pull request after that.
|
|
1065
|
+
- **Evidence comes by id from the job that made it:** the jobs that run the pull request's code (`pack candidate`, which builds it, and both `suite` legs, which run its tests) share the run's artifact store with the jobs that don't, and code in them can reach the runner's artifact token. So no consumer trusts a name or a pattern. Each producer job hands on its artifact's `artifact-id` and `artifact-digest` as job outputs, consumers download by `artifact-ids` with `digest-mismatch: error`, and no upload uses `overwrite`, so a second upload of a name fails instead of replacing it. The base build has its own job, `pack base`, which runs only main's code, so the candidate's job can't speak for it. `tools/cert-review/artifacts.mjs` lists the run's artifacts and fails when one in our namespaces (`kernel-tgz-`, `cert-review-`) wasn't uploaded by its producer, with that producer's id, name and digest, or when a producer's artifact is gone. `same` runs it on the two tarballs before it compares them, so the "not behaviour (identical dist)" skip can't be bought with a planted base tarball. `record` runs it on every artifact before it writes the line, and a failure there makes the record INCOMPLETE. **What this protects:** artifacts of jobs that run no pull-request code (the base build, the delta, kernel-compat, the same-dist evidence, the review's verdict and evidence) can't be forged or replaced. **What it doesn't:** the candidate's own artifacts, its tarball and both suites' results (the base leg runs the pull request's tests on main's tree, for the NEGATIVE), stay the pull request's by design, because code that runs on a runner can always lie about its own results. They're consumed by their exact names, at most one each. That's the same deliberate-malice residual as the workflow-rewrite one above.
|
|
1058
1066
|
- **Decide by what ships:** when a pull request's behaviour files are only `package.json` and/or `package-lock.json` (a Renovate devDependency bump, say), both sides are packed as usual, and then the `same dist` job (`same.mjs`, in code) compares the two tarballs before anything else runs. It checks the same paths, entry types (a symlink isn't a file) and modes. `package.json` must be equal once `devDependencies`, `version` and `gitHead` are removed, so `scripts` (they ship, a consumer's install runs `postinstall`, and `prepack`/`prepublishOnly` can make the published build differ from the packed one), runtime dependencies, `exports`, `bin`, `files` and `engines` must all match. Every other file must be byte-identical once each side's **exact** version string (build.mjs bakes it into `dist`) is replaced by one placeholder. A version string in one more place, or any looser match, is a difference. Source maps are compared as JSON without their `mappings`, whose columns the longer or shorter version string shifts; the code itself is compared byte for byte. If all of that matches, `cert-review` is green with `not behaviour (identical dist): <candidate sha256> = <base sha256> after normalising <v1>/<v2>`, and the suites, kernel-compat, the review and the record are skipped. `same.json` (uploaded as an artifact) names the rule and both tarballs' full sha256. Any difference, or a `same dist` job that doesn't finish, means the full review. The kernel has no runtime `dependencies`, since esbuild bundles everything into `dist`, so a devDependency that changes the product shows up as a byte difference. A test-only devDependency is checked by `ci / test`, on the pull request and on the merge group.
|
|
1059
1067
|
- **When:** only for a same-repo pull request with `merge-ok`, when the label is added and on every push while it is set. Each pull request has its own concurrency group, and a newer push cancels the older review. A behaviour change without `merge-ok` gets a red status, `waits for merge-ok to be reviewed`. Other labels don't start or cancel anything. Fork code never reaches the pools or the key.
|
|
1060
1068
|
- **What:** the checkout is the event's default `refs/pull/N/merge`, the pull request merged onto `main` at review time, so a review is of what would land, without pushing to the branch. Both sides are packed in the same run, on a GitHub-hosted runner, with no secrets: the candidate (the merge ref, `x.y.z-pr.N.<run>`) and the base (its first parent, `main`). Then:
|
package/dist/kernel-worker.js
CHANGED
|
@@ -856,6 +856,8 @@ var ProtectedFs = class {
|
|
|
856
856
|
// src/fs/remote.ts
|
|
857
857
|
var ANNOUNCE_WAIT_MS = 500;
|
|
858
858
|
var PRUNE_EVERY_MS = 3e4;
|
|
859
|
+
var MOURN_TRIES = 5;
|
|
860
|
+
var MOURN_RETRY_MS = 100;
|
|
859
861
|
function lockManager() {
|
|
860
862
|
return globalThis.navigator?.locks;
|
|
861
863
|
}
|
|
@@ -864,6 +866,7 @@ var RemoteWatch = class {
|
|
|
864
866
|
name;
|
|
865
867
|
tab = crypto.randomUUID();
|
|
866
868
|
peers = /* @__PURE__ */ new Set();
|
|
869
|
+
mourned = /* @__PURE__ */ new Set();
|
|
867
870
|
others = /* @__PURE__ */ new Map();
|
|
868
871
|
union = /* @__PURE__ */ new Set();
|
|
869
872
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -932,8 +935,7 @@ var RemoteWatch = class {
|
|
|
932
935
|
receive(data) {
|
|
933
936
|
if (data.hello !== void 0) this.greeted(data.hello);
|
|
934
937
|
if (data.here !== void 0 && data.to === this.tab) {
|
|
935
|
-
this.
|
|
936
|
-
this.recount();
|
|
938
|
+
this.arrived(data.here);
|
|
937
939
|
this.met?.();
|
|
938
940
|
}
|
|
939
941
|
if (data.tab !== void 0 && data.paths !== void 0) this.announced(data);
|
|
@@ -942,9 +944,36 @@ var RemoteWatch = class {
|
|
|
942
944
|
}
|
|
943
945
|
if (data.changed !== void 0) this.emit({ paths: data.changed, trees: [] });
|
|
944
946
|
}
|
|
945
|
-
|
|
947
|
+
arrived(peer) {
|
|
946
948
|
this.peers.add(peer);
|
|
947
949
|
this.recount();
|
|
950
|
+
void this.mourn(peer);
|
|
951
|
+
}
|
|
952
|
+
async mourn(peer, tries = MOURN_TRIES) {
|
|
953
|
+
if (!this.locks || this.mourned.has(peer)) return;
|
|
954
|
+
this.mourned.add(peer);
|
|
955
|
+
const name2 = this.lockOf(peer);
|
|
956
|
+
const { held = [], pending = [] } = await this.locks.query();
|
|
957
|
+
if (![...held, ...pending].some((lock) => lock.name === name2)) {
|
|
958
|
+
this.mourned.delete(peer);
|
|
959
|
+
if (tries > 1 && this.peers.has(peer)) {
|
|
960
|
+
setTimeout(() => void this.mourn(peer, tries - 1), MOURN_RETRY_MS);
|
|
961
|
+
}
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
void this.locks.request(name2, async () => {
|
|
965
|
+
this.mourned.delete(peer);
|
|
966
|
+
this.others.delete(peer);
|
|
967
|
+
this.peers.delete(peer);
|
|
968
|
+
this.rebuild();
|
|
969
|
+
this.recount();
|
|
970
|
+
for (const wait2 of this.waiting.values()) {
|
|
971
|
+
if (wait2.left.delete(peer) && wait2.left.size === 0) wait2.done();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
greeted(peer) {
|
|
976
|
+
this.arrived(peer);
|
|
948
977
|
const answer = () => {
|
|
949
978
|
this.channel.postMessage({ here: this.tab, to: peer });
|
|
950
979
|
if (this.mine.length > 0) void this.announce(this.mine);
|
|
@@ -954,8 +983,7 @@ var RemoteWatch = class {
|
|
|
954
983
|
}
|
|
955
984
|
announced({ tab, paths, seq: seq2 }) {
|
|
956
985
|
const from = tab;
|
|
957
|
-
this.
|
|
958
|
-
this.recount();
|
|
986
|
+
this.arrived(from);
|
|
959
987
|
if (paths.length > 0) this.others.set(from, paths);
|
|
960
988
|
else this.others.delete(from);
|
|
961
989
|
this.rebuild();
|
|
@@ -1567,7 +1595,8 @@ var OpfsFs = class {
|
|
|
1567
1595
|
async linkAlong(path) {
|
|
1568
1596
|
for (const entry of await this.meta.get(prefixes(path))) {
|
|
1569
1597
|
if (entry?.link === void 0) continue;
|
|
1570
|
-
if (
|
|
1598
|
+
if (entry.path !== path && await this.real(entry.path)) continue;
|
|
1599
|
+
if (await this.real(parentOf3(entry.path))) return entry;
|
|
1571
1600
|
}
|
|
1572
1601
|
return void 0;
|
|
1573
1602
|
}
|
|
@@ -2992,7 +3021,7 @@ var CdpFacade = class {
|
|
|
2992
3021
|
for (const conn of this.connections) conn.close();
|
|
2993
3022
|
}
|
|
2994
3023
|
async acceptLoop() {
|
|
2995
|
-
const
|
|
3024
|
+
const served2 = /* @__PURE__ */ new Set();
|
|
2996
3025
|
for (; ; ) {
|
|
2997
3026
|
let conn;
|
|
2998
3027
|
try {
|
|
@@ -3000,10 +3029,10 @@ var CdpFacade = class {
|
|
|
3000
3029
|
} catch {
|
|
3001
3030
|
break;
|
|
3002
3031
|
}
|
|
3003
|
-
const done4 = this.serve(conn).finally(() =>
|
|
3004
|
-
|
|
3032
|
+
const done4 = this.serve(conn).finally(() => served2.delete(done4));
|
|
3033
|
+
served2.add(done4);
|
|
3005
3034
|
}
|
|
3006
|
-
await Promise.allSettled([...
|
|
3035
|
+
await Promise.allSettled([...served2]);
|
|
3007
3036
|
}
|
|
3008
3037
|
async serve(conn) {
|
|
3009
3038
|
this.connections.add(conn);
|
|
@@ -6565,7 +6594,7 @@ function satisfies(version, range2) {
|
|
|
6565
6594
|
}
|
|
6566
6595
|
|
|
6567
6596
|
// src/version.ts
|
|
6568
|
-
var KERNEL_VERSION = true ? "1.124.
|
|
6597
|
+
var KERNEL_VERSION = true ? "1.124.3" : "0.0.0-dev";
|
|
6569
6598
|
var OS_RELEASE = "6.0.0-slicc";
|
|
6570
6599
|
function procVersion() {
|
|
6571
6600
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -8264,7 +8293,7 @@ var RealmProxy = class {
|
|
|
8264
8293
|
for (const conn of this.connections) conn.close();
|
|
8265
8294
|
}
|
|
8266
8295
|
async acceptLoop() {
|
|
8267
|
-
const
|
|
8296
|
+
const served2 = /* @__PURE__ */ new Set();
|
|
8268
8297
|
try {
|
|
8269
8298
|
for (; ; ) {
|
|
8270
8299
|
await this.slots.acquire(1, this.stop.signal);
|
|
@@ -8277,13 +8306,13 @@ var RealmProxy = class {
|
|
|
8277
8306
|
}
|
|
8278
8307
|
const done4 = this.serve(conn).finally(() => {
|
|
8279
8308
|
this.slots.release(1);
|
|
8280
|
-
|
|
8309
|
+
served2.delete(done4);
|
|
8281
8310
|
});
|
|
8282
|
-
|
|
8311
|
+
served2.add(done4);
|
|
8283
8312
|
}
|
|
8284
8313
|
} catch {
|
|
8285
8314
|
}
|
|
8286
|
-
await Promise.allSettled([...
|
|
8315
|
+
await Promise.allSettled([...served2]);
|
|
8287
8316
|
}
|
|
8288
8317
|
async serve(conn) {
|
|
8289
8318
|
this.connections.add(conn);
|
|
@@ -12015,7 +12044,12 @@ var IDENTITY_OPS = /* @__PURE__ */ new Set([
|
|
|
12015
12044
|
"chown",
|
|
12016
12045
|
"lchown"
|
|
12017
12046
|
]);
|
|
12047
|
+
var served = 0;
|
|
12048
|
+
function servedFsCalls() {
|
|
12049
|
+
return served;
|
|
12050
|
+
}
|
|
12018
12051
|
async function dispatchSyncFs(req) {
|
|
12052
|
+
served++;
|
|
12019
12053
|
const entry = resolveSyncFsToken(req.token);
|
|
12020
12054
|
if (!entry) return { ok: false, errno: "EACCES", message: "sync-fs: unknown or revoked token" };
|
|
12021
12055
|
if (!req.identity || !IDENTITY_OPS.has(req.op)) return dispatchOne(entry, req);
|
|
@@ -12721,7 +12755,11 @@ var SYS_TREE = {
|
|
|
12721
12755
|
osrelease: `${OS_RELEASE}
|
|
12722
12756
|
`,
|
|
12723
12757
|
ostype: "Linux\n",
|
|
12724
|
-
slicc_wasm_memories: (listing) => `${listing.wasm?.live ?? 0} ${listing.wasm?.ceiling ?? 0}
|
|
12758
|
+
slicc_wasm_memories: (listing) => `${listing.wasm?.live ?? 0} ${listing.wasm?.ceiling ?? 0} ${listing.wasm?.source ?? "none"}
|
|
12759
|
+
`,
|
|
12760
|
+
slicc_shared_root: (listing) => `${listing.peers ?? 0}
|
|
12761
|
+
`,
|
|
12762
|
+
slicc_fs_calls: (listing) => `${listing.fsCalls ?? 0}
|
|
12725
12763
|
`
|
|
12726
12764
|
}
|
|
12727
12765
|
};
|
|
@@ -21926,37 +21964,65 @@ var SettlingChildren = class {
|
|
|
21926
21964
|
|
|
21927
21965
|
// src/kernel/wasm-memories.ts
|
|
21928
21966
|
var DEFAULT_CEILING = 112;
|
|
21967
|
+
var CEILING_FLOOR = 16;
|
|
21929
21968
|
var IDLE_MS = 1e4;
|
|
21930
21969
|
var MARGIN = 16;
|
|
21970
|
+
var DECAY_MS = 6e4;
|
|
21971
|
+
var DECAY_STEP = 8;
|
|
21931
21972
|
var WasmMemories = class {
|
|
21932
21973
|
live = 0;
|
|
21933
|
-
|
|
21974
|
+
learned;
|
|
21934
21975
|
waiters = /* @__PURE__ */ new Set();
|
|
21976
|
+
base;
|
|
21935
21977
|
idleMs;
|
|
21936
|
-
|
|
21937
|
-
|
|
21978
|
+
now;
|
|
21979
|
+
constructor(ceiling, idleMs = IDLE_MS, now = Date.now) {
|
|
21980
|
+
this.base = ceiling;
|
|
21938
21981
|
this.idleMs = idleMs;
|
|
21982
|
+
this.now = now;
|
|
21983
|
+
}
|
|
21984
|
+
ceiling() {
|
|
21985
|
+
const learned = this.learned;
|
|
21986
|
+
if (!learned) return this.base;
|
|
21987
|
+
const steps = Math.floor((this.now() - learned.at) / DECAY_MS);
|
|
21988
|
+
const raised = learned.ceiling + steps * DECAY_STEP;
|
|
21989
|
+
if (raised < (this.base ?? DEFAULT_CEILING)) return raised;
|
|
21990
|
+
this.learned = void 0;
|
|
21991
|
+
return this.base;
|
|
21939
21992
|
}
|
|
21940
21993
|
count() {
|
|
21941
|
-
|
|
21994
|
+
const ceiling = this.ceiling();
|
|
21995
|
+
const source = this.learned ? "learned" : ceiling === void 0 ? "none" : "default";
|
|
21996
|
+
return { live: this.live, ceiling: ceiling ?? 0, source };
|
|
21942
21997
|
}
|
|
21943
21998
|
ended() {
|
|
21944
21999
|
this.live = Math.max(0, this.live - 1);
|
|
21945
22000
|
this.progress();
|
|
21946
22001
|
}
|
|
21947
22002
|
learn() {
|
|
21948
|
-
|
|
22003
|
+
const top = this.base ?? DEFAULT_CEILING;
|
|
22004
|
+
this.learned = {
|
|
22005
|
+
ceiling: Math.min(top, Math.max(CEILING_FLOOR, this.live - 1)),
|
|
22006
|
+
at: this.now()
|
|
22007
|
+
};
|
|
21949
22008
|
}
|
|
21950
22009
|
near() {
|
|
21951
|
-
|
|
22010
|
+
const ceiling = this.ceiling();
|
|
22011
|
+
return ceiling !== void 0 && this.live + MARGIN >= ceiling;
|
|
21952
22012
|
}
|
|
21953
22013
|
async admit() {
|
|
21954
|
-
|
|
22014
|
+
for (let ceiling = this.ceiling(); ceiling !== void 0 && this.live >= ceiling; ) {
|
|
21955
22015
|
if (!await this.changed()) throw new SpawnError("EAGAIN");
|
|
22016
|
+
ceiling = this.ceiling();
|
|
21956
22017
|
}
|
|
21957
22018
|
this.live += 1;
|
|
21958
22019
|
this.progress();
|
|
21959
22020
|
}
|
|
22021
|
+
async refused() {
|
|
22022
|
+
const ceiling = this.ceiling();
|
|
22023
|
+
if (ceiling === void 0 || this.live >= ceiling) return;
|
|
22024
|
+
if (!await this.changed()) throw new SpawnError("EAGAIN");
|
|
22025
|
+
}
|
|
21960
22026
|
progress() {
|
|
21961
22027
|
const waiters = [...this.waiters];
|
|
21962
22028
|
this.waiters.clear();
|
|
@@ -23061,7 +23127,7 @@ async function openHostfs(spec, connect2, timing = {}) {
|
|
|
23061
23127
|
...active?.capabilities.ranges === true ? { ranges: true } : {},
|
|
23062
23128
|
...active ? { maxIo: Math.min(active.maxIo, HOSTFS_IO) } : {}
|
|
23063
23129
|
});
|
|
23064
|
-
const
|
|
23130
|
+
const served2 = serveFilesystem(
|
|
23065
23131
|
port2,
|
|
23066
23132
|
{ ...slot2.handlers, mount: ranged2 },
|
|
23067
23133
|
{
|
|
@@ -23078,12 +23144,12 @@ async function openHostfs(spec, connect2, timing = {}) {
|
|
|
23078
23144
|
up(client) {
|
|
23079
23145
|
active = timed(client, timing.callMs ?? HOSTFS_CALL_MS);
|
|
23080
23146
|
slot2.insert(client, handlers);
|
|
23081
|
-
|
|
23147
|
+
served2.invalidate(true);
|
|
23082
23148
|
},
|
|
23083
|
-
event: (event) =>
|
|
23149
|
+
event: (event) => served2.invalidate(invalidation(event)),
|
|
23084
23150
|
down() {
|
|
23085
23151
|
slot2.eject();
|
|
23086
|
-
|
|
23152
|
+
served2.invalidate(true);
|
|
23087
23153
|
},
|
|
23088
23154
|
...timing
|
|
23089
23155
|
},
|
|
@@ -25217,6 +25283,10 @@ var Launcher = class {
|
|
|
25217
25283
|
return handle;
|
|
25218
25284
|
}
|
|
25219
25285
|
await handle.exited;
|
|
25286
|
+
await this.memories.refused().catch(async (err) => {
|
|
25287
|
+
await fds.closeAll();
|
|
25288
|
+
throw err;
|
|
25289
|
+
});
|
|
25220
25290
|
}
|
|
25221
25291
|
}
|
|
25222
25292
|
start(req) {
|
|
@@ -25282,7 +25352,9 @@ var Launcher = class {
|
|
|
25282
25352
|
processes: this.list(this.credOf(pid)),
|
|
25283
25353
|
load: this.loadAverage(),
|
|
25284
25354
|
cpu: this.cpuTotal(),
|
|
25285
|
-
wasm: this.memories.count()
|
|
25355
|
+
wasm: this.memories.count(),
|
|
25356
|
+
peers: Number(this.locks.shared?.() === true),
|
|
25357
|
+
fsCalls: servedFsCalls()
|
|
25286
25358
|
}),
|
|
25287
25359
|
sudo: (call, signal) => this.sudo(pid, call, signal),
|
|
25288
25360
|
crontab: (call) => this.crontab(pid, call),
|
|
@@ -25776,7 +25848,7 @@ var Launcher = class {
|
|
|
25776
25848
|
const worker = this.createDriverWorker();
|
|
25777
25849
|
const driver = new MessageChannel();
|
|
25778
25850
|
const client = new MessageChannel();
|
|
25779
|
-
const
|
|
25851
|
+
const served2 = serveClient(client.port1, {
|
|
25780
25852
|
launcher: async () => this,
|
|
25781
25853
|
scope: "transport"
|
|
25782
25854
|
});
|
|
@@ -25788,7 +25860,7 @@ var Launcher = class {
|
|
|
25788
25860
|
port: driver.port1,
|
|
25789
25861
|
dispose: () => {
|
|
25790
25862
|
worker.terminate();
|
|
25791
|
-
|
|
25863
|
+
served2.detach();
|
|
25792
25864
|
driver.port1.close();
|
|
25793
25865
|
},
|
|
25794
25866
|
onCrash: (listener) => worker.addEventListener("error", (event) => {
|
|
@@ -25802,12 +25874,12 @@ var Launcher = class {
|
|
|
25802
25874
|
const ask = (handle) => this.onMountPending?.({ target, source, ...handle ? { handle } : {} });
|
|
25803
25875
|
const { port1, port2 } = new MessageChannel();
|
|
25804
25876
|
const medium = removableMedium((handle) => {
|
|
25805
|
-
|
|
25877
|
+
served2.invalidate(true);
|
|
25806
25878
|
ask(handle);
|
|
25807
25879
|
});
|
|
25808
|
-
const
|
|
25880
|
+
const served2 = serveFilesystem(port2, medium.handlers, FSA_CAPABILITIES);
|
|
25809
25881
|
const id2 = source.slice("fsa:".length);
|
|
25810
|
-
this.removable.set(target, { id: id2, medium, served });
|
|
25882
|
+
this.removable.set(target, { id: id2, medium, served: served2 });
|
|
25811
25883
|
const stored = this.inserted.get(id2) ?? await this.media.get(id2);
|
|
25812
25884
|
if (stored && await granted(stored)) medium.insert(stored);
|
|
25813
25885
|
else ask(stored);
|
|
@@ -26818,7 +26890,7 @@ function serveKernel(port, deps) {
|
|
|
26818
26890
|
}
|
|
26819
26891
|
const { port1, port2 } = new MessageChannel();
|
|
26820
26892
|
const held = locks ? await holdLock(locks) : void 0;
|
|
26821
|
-
const
|
|
26893
|
+
const served2 = serveClient(port1, {
|
|
26822
26894
|
launcher: ready2,
|
|
26823
26895
|
signal: signalNumber,
|
|
26824
26896
|
...user !== void 0 ? { user } : {},
|
|
@@ -26826,8 +26898,8 @@ function serveKernel(port, deps) {
|
|
|
26826
26898
|
...locks ? { locks } : {},
|
|
26827
26899
|
...held ? { lock: held } : {}
|
|
26828
26900
|
});
|
|
26829
|
-
clients.add(
|
|
26830
|
-
void
|
|
26901
|
+
clients.add(served2);
|
|
26902
|
+
void served2.closed.then(() => clients.delete(served2));
|
|
26831
26903
|
return port2;
|
|
26832
26904
|
}
|
|
26833
26905
|
async function ready2() {
|