@ai-ecoverse/slicc-kernel 1.124.13 → 1.125.0
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 +17 -3
- package/dist/kernel-worker.js +246 -43
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +132 -68
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +246 -43
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +132 -68
- package/dist/process-worker.js.map +4 -4
- package/dist/types/fs/watch.d.ts +19 -0
- package/dist/types/kernel/fd-table.d.ts +1 -0
- package/dist/types/kernel/host.d.ts +1 -0
- package/dist/types/kernel/process.d.ts +4 -1
- package/dist/types/kernel/select.d.ts +1 -0
- package/dist/types/kernel/watch-ops.d.ts +30 -0
- package/dist/types/launcher.d.ts +1 -0
- package/dist/types/process/js/js-context.d.ts +2 -0
- package/dist/types/process/js/js-sync.d.ts +5 -1
- package/dist/types/process/js/js-watch.d.ts +17 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -265,7 +265,7 @@ An empty path is `ENOENT` in every path call (`path_open`, `path_filestat_get`,
|
|
|
265
265
|
On top of preview1 a WASI program gets:
|
|
266
266
|
|
|
267
267
|
- **Threads**: `wasi.thread-spawn` (wasm32-wasip1-threads) and WASIX `thread_spawn_v2` start a worker per thread on the process's shared memory, sized from the module's own import (a 2 GiB maximum when the engine cannot reserve more). The threads share one descriptor table, and a file written, created, renamed or removed in one thread shows its new size and metadata to `stat` in every other thread at once, under any of its names (a symlink, a hard link, a new name after a rename in the same process), also when another thread renames a file over that name meanwhile, since the kernel reports which file an open or a change reached; this holds among the kernel's own processes, not against other tabs, `hostfs` writers or programs that aren't WASI; another process's changes show at close and after a `spawn` or `wait`, as with close-to-open caching, so a file another process renames while this one holds it open may show its old size under the new name until then; `exit` or a trap in any thread ends the process. At most 64 threads per process; `SLICC_WASM_THREADS` sets another cap, up to 256.
|
|
268
|
-
- **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), with the unwound frames kept in the unused part of the program's stack, below its stack pointer (for a position-independent program, the stack the kernel's linker gives it), so a fork from deep in the stack leaves the program's data alone, and a forked child's thread-locals (`errno` included) where the parent left them: the kernel reruns the module's `__wasm_init_tls` at the parent's TLS base (and puts back the block it overwrites), which also sets the internal globals that hold thread-local addresses (a position-independent program's, a side module's, and those of a variable in the `global-dynamic` TLS model), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`. Each thread has its own copy of the function table, so the kernel hands out its slots (a side module's range, a `dlsym`'d function, a closure): threads loading or looking up at the same time never get the same slot. As in glibc, one `dlopen` at a time runs in a process, constructors included: a `dlopen` from a library's constructor goes ahead in its own thread, and another thread's `dlopen` waits, then finds the library loaded and gets the same handle. A thread waiting there still ends or stops with its process, but a signal handler it has runs once that `dlopen` returns. The lock is let go when a `dlopen` fails (an exception, a trap or `pthread_exit` out of a constructor included) and when its thread ends; a forked child starts with it free. A forked child also gets the side modules its parent had loaded, at the same addresses and table slots, with the slot and closure counters and the parent's thread-local blocks: they are linked again in the child before it resumes, without running their constructors or their data relocations again, since their data is already in the copied memory. The same goes for the main module of a position-independent program: its data relocations ran once, in the parent, so a pointer the parent changed before the fork (`int *p = &a; p = &b;`) is still changed in the child (#777). A side-module function on the stack when `fork` is called (a callback into the main module that forks) is not instrumented by Asyncify, so it runs again when the stack is rewound, in the parent and in the child, and libffi frames there can trap; side modules would need building with Asyncify for that, which the toolchain does not do yet. In such a program, `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); a program that isn't position-independent gets an error from `dlopen(NULL)`, as from any `dlopen`; `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`. Each WASIX thread has its own Asyncify driver too, so a thread can `longjmp` through Asyncify and `fork` (#710). The unwound frames go at the bottom of the thread's own stack (the `stack` and `stack_size` its libc passes to `wasi_thread_start`), never on the main thread's. A thread whose start args don't look like wasix-libc's (a `stack_size` under 4096 or over the memory, a stack top that isn't 16-aligned or not right above the args) gets no driver, so `fork` there is `ENOSYS` as before, rather than an unwind buffer at a bad address. A child forked from a thread runs as that thread: it's rewound through `wasi_thread_start`, can fork again, and ends when the thread's function returns. A program built with exceptions (every position-independent one) does `setjmp` with exceptions rather than Asyncify, so there only `fork` uses the driver. A kernel timer can be longer than `setTimeout`'s 2^31-1 ms (about 24.8 days): an `alarm`, an interval timer, or a `select`, `poll_oneoff` or `epoll_wait` timeout waits its whole time, re-armed in steps of at most 2^31-1 ms, instead of firing at once or spinning (#767). A WASI or WASIX program that a signal ends (`SIGKILL`, or a signal whose default terminates it) stops at its next kernel call. The kernel first lets the calls it is already serving finish and the program write its files back: it ends the process once the program has made no file call for 2 s, and 5 s after the signal at the latest. A call the program makes after the kill is answered `EINTR` at once and doesn't extend the wait, so a program that swallows the kill and keeps retrying (python writing a fatal error to stderr) still ends (#755).
|
|
268
|
+
- **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), with the unwound frames kept in the unused part of the program's stack, below its stack pointer (for a position-independent program, the stack the kernel's linker gives it), so a fork from deep in the stack leaves the program's data alone, and a forked child's thread-locals (`errno` included) where the parent left them: the kernel reruns the module's `__wasm_init_tls` at the parent's TLS base (and puts back the block it overwrites), which also sets the internal globals that hold thread-local addresses (a position-independent program's, a side module's, and those of a variable in the `global-dynamic` TLS model), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`. Each thread has its own copy of the function table, so the kernel hands out its slots (a side module's range, a `dlsym`'d function, a closure): threads loading or looking up at the same time never get the same slot. As in glibc, one `dlopen` at a time runs in a process, constructors included: a `dlopen` from a library's constructor goes ahead in its own thread, and another thread's `dlopen` waits, then finds the library loaded and gets the same handle. A thread waiting there still ends or stops with its process, but a signal handler it has runs once that `dlopen` returns. The lock is let go when a `dlopen` fails (an exception, a trap or `pthread_exit` out of a constructor included) and when its thread ends; a forked child starts with it free. A forked child also gets the side modules its parent had loaded, at the same addresses and table slots, with the slot and closure counters and the parent's thread-local blocks: they are linked again in the child before it resumes, without running their constructors or their data relocations again, since their data is already in the copied memory. The same goes for the main module of a position-independent program: its data relocations ran once, in the parent, so a pointer the parent changed before the fork (`int *p = &a; p = &b;`) is still changed in the child (#777). A side-module function on the stack when `fork` is called (a callback into the main module that forks) is not instrumented by Asyncify, so it runs again when the stack is rewound, in the parent and in the child, and libffi frames there can trap; side modules would need building with Asyncify for that, which the toolchain does not do yet. In such a program, `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); a program that isn't position-independent gets an error from `dlopen(NULL)`, as from any `dlopen`; `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`. Each WASIX thread has its own Asyncify driver too, so a thread can `longjmp` through Asyncify and `fork` (#710). The unwound frames go at the bottom of the thread's own stack (the `stack` and `stack_size` its libc passes to `wasi_thread_start`), never on the main thread's. A thread whose start args don't look like wasix-libc's (a `stack_size` under 4096 or over the memory, a stack top that isn't 16-aligned or not right above the args) gets no driver, so `fork` there is `ENOSYS` as before, rather than an unwind buffer at a bad address. A child forked from a thread runs as that thread: it's rewound through `wasi_thread_start`, can fork again, and ends when the thread's function returns. A program built with exceptions (every position-independent one) does `setjmp` with exceptions rather than Asyncify, so there only `fork` uses the driver. A kernel timer can be longer than `setTimeout`'s 2^31-1 ms (about 24.8 days): an `alarm`, an interval timer, or a `select`, `poll_oneoff` or `epoll_wait` timeout waits its whole time, re-armed in steps of at most 2^31-1 ms, instead of firing at once or spinning (#767). A `poll_oneoff` read event's `nbytes` is the number of bytes a read would get at once, as `FIONREAD` counts them, for a pipe, a FIFO, a socket, a terminal and a pty master; on a hangup it's 0 when nothing is left, so a libc can report `POLLHUP` without `POLLIN`, as Linux does (#1053). Other descriptors, regular files included, answer 1. A WASI or WASIX program that a signal ends (`SIGKILL`, or a signal whose default terminates it) stops at its next kernel call. The kernel first lets the calls it is already serving finish and the program write its files back: it ends the process once the program has made no file call for 2 s, and 5 s after the signal at the latest. A call the program makes after the kill is answered `EINTR` at once and doesn't extend the wait, so a program that swallows the kill and keeps retrying (python writing a fatal error to stderr) still ends (#755).
|
|
269
269
|
- **epoll and eventfd** (WASIX `epoll_create`, `epoll_ctl`, `epoll_wait` and `fd_event`, which tokio's reactor uses through the WASIX mio fork):
|
|
270
270
|
- **Where they live:** an epoll instance and an eventfd are kernel descriptors, so they survive `dup`, `fork` and `exec` (unless close-on-exec) and are shared by a process's threads.
|
|
271
271
|
- **What epoll watches:** sockets (listening, connecting, connected, loopback and uplink), pipes, terminals, `kernel.run`'s stdin, eventfds and other epoll instances (a cycle of instances is `ELOOP`).
|
|
@@ -358,6 +358,20 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
358
358
|
- `fdStatus(fd)` (`{ type, tty, seekable }`, `type` being `file`, `pipe`, `tty`, `socket`, `device` or `directory`) and `isatty(fd)`, to tell `< file` from a pipe;
|
|
359
359
|
- `open(path, { read, write, append, create, exclusive, truncate, nonblock, signal })`, which returns a file with `read(position, length)`, `write(position, bytes)`, `size()`, `truncate(size)`, `sync()` and `close()`. Reads and writes go through the kernel's paged files at those positions, so a large file is never read whole; **A FIFO** ([#935](https://github.com/ai-ecoverse/slicc-kernel/issues/935)) opens through the kernel's FIFO open, as fifo(7) says. A read-open waits for a writer and a write-open for a reader, unless the open is `read` and `write` both. `nonblock: true` gives a read-open at once and a write-open with no reader `ENXIO`, and `signal` aborts a waiting open with its reason. Its file reads and writes the pipe: `read(0, length)` and `write(0, bytes)`, any other position is `ESPIPE`, `size()` is `0`, and `truncate` is `EINVAL`. A socket node is `ENXIO`.
|
|
360
360
|
- `fs`: `stat`, `lstat`, `exists`, `readdir`, `mkdir` (with parents, or, with `{ recursive: false }`, as Node's `fs.mkdir` without `recursive`: `EEXIST` for an existing entry and `ENOENT` for a missing parent, atomically among the processes of one kernel, so it can serve as a lock there, but not between two kernels on one origin, such as two tabs ([#519](https://github.com/ai-ecoverse/slicc-kernel/issues/519)); `sync.fs.mkdir` takes the same option), `rm` (recursive), `unlink`, `rename`, `symlink`, `readlink`, `statfs`, `chmod(path, mode)`, and `readFile`/`writeFile` for small files. `chmod` takes the permission bits (`0` to `0o7777`, `EINVAL` otherwise) and follows a symlink; as `chmod(2)`, only the file's owner or root may (`EPERM`), so a program can make a script it wrote runnable by path. `sync.fs.chmod` is the same, synchronously. `statfs(path)` answers `{ type, bsize, blocks, bfree, bavail, files, ffree, quota, usage }`: the fields of Node's `StatFs`, plus `quota` and `usage` in bytes (what jsh 0.17 reads). The values come from the same source as an Emscripten program's `statfs`/`df`: the driver of the mount the path is on, or, on the root file system, the origin's storage estimate (`navigator.storage.estimate()`). `bsize` is 4096, `bfree` and `bavail` are what's left of the quota, and `type`, `files` and `ffree` are 0, since the kernel doesn't know them. Where there's no estimate (the Node entry's root), every count is 0. A missing path is `ENOENT`. `sync.fs` has it too. `rm` removes a whole tree in one kernel call (on the OPFS root, one recursive removal and one metadata delete: 50,000 files take about 3 s in Chrome, where `rm -r` takes about 25 s) with the result of a file-by-file removal: symlinks inside the tree are removed, never followed; a tree holding a protected path is refused with `EROFS` and nothing is removed; a mount inside the tree is emptied and its mount point answers `EBUSY`, while the rest is removed; under `users`, what the user may remove is removed and the call then fails with the first `EACCES`/`EPERM`; files a process holds open stay readable to it;
|
|
361
|
+
- **`fs.watch(path, options?, onEvent)`** resolves with `{ close() }` ([#1031](https://github.com/ai-ecoverse/slicc-kernel/issues/1031)). `sync.fs.watch` returns the watcher directly and is in place when it returns, so it sees every change made after the call. This is the shape for Node's synchronous `fs.watch`.
|
|
362
|
+
- **What it reports:** a file reports its own changes. A directory reports its direct entries, or with `{ recursive: true }`, everything below it.
|
|
363
|
+
- **Events:** each one is `{ type, filename }`, in the order the operations happened.
|
|
364
|
+
- `'rename'`: an entry appeared or went away (create, `mkdir`, `unlink`, `rm`, both names of a `rename`, `link`, `symlink`).
|
|
365
|
+
- `'change'`: an existing entry's data or metadata changed (a write, `truncate`, `chmod`, `chown`, `utimes`). A write that creates a file gives `'rename'` and then `'change'`, as on Linux.
|
|
366
|
+
- `filename` is the entry's name relative to the watched directory (`sub/a.txt` when recursive), or the file's basename for a file watch.
|
|
367
|
+
- **Overflow:** a watch holds 1,000 events between reads. Beyond that, the rest are dropped, and `{ overflow: true }` follows the ones kept.
|
|
368
|
+
- **The watched path itself:** when it is removed or renamed away (or an ancestor is), the watch gets one `'rename'` with its own name, then `{ ended: true }`, and ends. `close()` is safe at any time and idempotent, and nothing is delivered after it returns.
|
|
369
|
+
- **Errors at watch time:** a path the program may not read (under `users`) is `EACCES`, and one that doesn't exist is `ENOENT`. Permission is checked first, so a path under a directory the program can't search is `EACCES` whether or not it exists. A process holds at most 8,192 watches; past that, `ENOSPC`.
|
|
370
|
+
- **Permissions per event:** under `users`, a program only hears about paths it may reach. An event about `P` below the watched directory is delivered only if the program's user can search every directory down to `P`'s parent and read that parent, as Node's recursive watch would need in order to add a watch there. The check runs when the program takes the events, so another user's `0700` directory stays silent until it's opened up. A consequence is that events inside a directory that is gone by the time the program takes them are dropped: after `rm -r sub` the program sees `rename sub` but not `sub/a`. A watcher should rescan on a directory's `rename`, as Node users do anyway.
|
|
371
|
+
- **Who counts as a writer:** every process in the kernel, the embedder (`kernel.writeFile` and the like) and the mounts that report changes. A `hostfs` change from the host comes as `'change'`, since the host doesn't say which kind it was.
|
|
372
|
+
- **Not reported:** another kernel's changes on the same origin (another tab), as inotify on a shared file system doesn't see other hosts.
|
|
373
|
+
- **Lifetime:** a watch ends with its process and doesn't keep the program alive.
|
|
374
|
+
- **The older watches** (`kernel.watch`, a client's `fs.watch` with `{ paths }`) are unchanged.
|
|
361
375
|
- `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`). `/dev/shm` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Removing a `/dev` entry fails (see [`/dev`](#dev-entries)), and so does a new name directly in `/dev` or `/proc`;
|
|
362
376
|
- `/proc`, in `fs`, `open` and their `sync` forms, the same as a WASI program's (see [`/proc`](#proc)): `stat`, `lstat`, `exists`, `readdir`, `readlink` and `readFile` answer it, and `/proc/self` is the program's own pid. It's read-only, so every change in it is `EROFS`: `writeFile`, `mkdir`, `rm`, `unlink`, `chmod`, a `rename`, `link` or `symlink` into or out of it, and an `open` for writing, creating or truncating. A file opened in it is a snapshot read at the open, with no descriptor (`fd` is `-1`): its `read` and `size` answer from the snapshot until it's closed, and `write` and `truncate` are `EBADF`. Other paths pay only a prefix check;
|
|
363
377
|
- `signals`: `on(signal, handler)` catches a signal (by number or as `'SIGINT'`), `ignore(signal)` ignores it and `reset(signal)` restores its default action. A call blocked in a read or write is restarted after a handler runs; signals without a handler keep their default actions, so an uncaught `SIGINT` ends the program with 130 and `SIGKILL` always does;
|
|
@@ -1073,7 +1087,7 @@ Every change is certified in its pull request, before it merges (`cert-review-pr
|
|
|
1073
1087
|
|
|
1074
1088
|
The tools in `tools/cert-review/`:
|
|
1075
1089
|
|
|
1076
|
-
- **delta** (`delta.mjs`): lists the pull requests merged between the two tags (their merge commits, plus `pr` for a planted branch). Each gets its conventional-commit kind
|
|
1090
|
+
- **delta** (`delta.mjs`): lists the pull requests merged between the two tags (their merge commits, plus `pr` for a planted branch). Each is a behaviour change **by the paths it changes**, with the same rule the plan uses (`behaviourFiles`: `src/`, `build.mjs`, `package.json` and its lock, the build and publish workflows, the reviewer…), whatever its title says (#1036 and #1041 were titled "A2 …"), and it gets its conventional-commit kind and the integration test files and titles it adds or changes. Every behaviour change with tests gets its NEGATIVE: its tests run on the previous tree. A `fix` must fail there (`no-negative` otherwise); any other kind is `negative` or `passes`. A behaviour change whose NEGATIVE has no result (`prev-not-run`), or whose changed tests the delta can't name (`no-titles`), makes the record INCOMPLETE. The delta names the test around every changed line: a title on the `test(` line or the line after it (as biome wraps a long one), a template literal, a title in the context lines above the change, and, when the hunk header shows only `test(`, the title read from the file at the merge commit. A pull request that changes only `package.json` and `package-lock.json`, and in `package.json` only the versions in `dependencies`, `devDependencies` and `optionalDependencies` (delta.mjs compares the file before and after its merge), is decided `manifest` with no test gap: it passed the required `cert-review` in the merge queue, as identical dist or with a full review, so the release line doesn't repeat it. A scripts, exports or new-dependency change stays a behaviour change, with `no-test` when it has no tests. The titles are read from the pull request's patch: the added `test(…)` lines and the hunk headers git puts above changed lines. Escapes are decoded as JavaScript does. A title git cut short matches as a prefix, and each `${…}` in a template-literal title matches anything there, so `` `a forked child of a ${kind} WASIX program…` `` matches the rows it ran as. A title made only of placeholders is skipped.
|
|
1077
1091
|
- **suite** (`suite.mjs`, `reporter.mjs`) runs on the bench pool, without secrets. It takes the tag's tree with its own dependencies and Chromium, and replaces `dist/` with the **published npm tarball** (for a planted branch, its own `npm pack`). The test command runs in a session of its own (`detached`), and `suite.mjs` notes a `SIGINT` (`ignored` in its summary) instead of dying of it. That way a stray signal to the step's process group can't cut the run short, while a `SIGTERM` (a real cancel) still ends it. The log is written as the run goes, and a `SIGTERM` still writes the summary with the results so far (`exit: 'signal SIGTERM'`). A run that left only its `.jsonl` (the runner was lost) is read from that, as `exit: 'no summary'`. The NEGATIVE step runs even when the release suite's step failed. Before it reports, `suite.mjs` re-runs once just the test files that left no result (no rows, a cancelled row, or a file-level failure, such as a lost process), with a second `node --test` over those files, and merges their results (`retried`, `firstExit`, `unfinished` in the summary). That holds for the release suite and for each NEGATIVE run. A fix whose test passes on its baseline stays `no-negative`.
|
|
1078
1092
|
- The candidate job runs the pull request's whole `test/integration/` suite on its packed build.
|
|
1079
1093
|
- The base job runs `main`'s own suite on `main`'s packed build, then the NEGATIVE (#861): `main`'s tree, with all of the pull request's `test/**` changes applied (test files, helpers and fixtures), on `main`'s build. So a fix in `tools/` fails there just as a fix in `src/` does. A test file that fails to load on `main` (a module or export that only the pull request adds) leaves no result for its titles, which is `prev-not-run`, never a NEGATIVE.
|
|
@@ -1091,7 +1105,7 @@ The tools in `tools/cert-review/`:
|
|
|
1091
1105
|
- **record** (`record.mjs`), in code, with read-only permissions:
|
|
1092
1106
|
- It re-downloads the tarball and checks `dist.integrity` (sha512) and the sha256, the SLSA v1 provenance (the tarball is its subject; `release.yml`, `refs/heads/main`, a GitHub-hosted builder; commit = `gitHead` = the tag), `npm audit signatures`, and that the commit is on `main`. A problem there, a `compat-crash`, or a planted build is a FAIL whatever the review says.
|
|
1093
1107
|
- It adds the **known issues as of the release**: every issue labelled `known-issue` (open or closed) and every one in `known.json`, unless it was fixed before the release. An issue in another repository, whether listed or named by the reviewer, counts only while it is open. That holds when `known.json`'s `fixedIn` names an earlier or equal version, or the merged pull request that closed it is an ancestor of the tag, or it was closed (with no closing pull request) before the release commit. An issue closed as not planned or as a duplicate is dropped.
|
|
1094
|
-
- The line is `certified @ai-ecoverse/slicc-kernel@<v> sha256=<…> (ci: <run>; known: …)`. Any test gaps from the findings (`no-test`, `no-negative`, `
|
|
1108
|
+
- The line is `certified @ai-ecoverse/slicc-kernel@<v> sha256=<…> (ci: <run>; known: …)`. Any test gaps from the findings (`no-test`, `no-negative`, `no-titles`, `not-run`) are appended in code as `test gaps: #<pr> <kind>`, so a fix without a NEGATIVE is never certified silently.
|
|
1095
1109
|
- **verify** (`verify.mjs`) is the `release / verify on npm` job above.
|
|
1096
1110
|
|
|
1097
1111
|
### Certification in the pull request (`cert-review`)
|
package/dist/kernel-worker.js
CHANGED
|
@@ -4028,7 +4028,12 @@ function openPipe(capacity) {
|
|
|
4028
4028
|
}
|
|
4029
4029
|
},
|
|
4030
4030
|
close: () => pipe.closeRead(),
|
|
4031
|
-
poll: () => ({
|
|
4031
|
+
poll: () => ({
|
|
4032
|
+
readable: pipe.readReady,
|
|
4033
|
+
writable: false,
|
|
4034
|
+
hangup: pipe.writersGone,
|
|
4035
|
+
nread: pipe.buffered
|
|
4036
|
+
}),
|
|
4032
4037
|
changed: (signal) => pipe.changed(signal),
|
|
4033
4038
|
identity: pipe
|
|
4034
4039
|
}),
|
|
@@ -4816,7 +4821,8 @@ var KernelSocket = class _KernelSocket {
|
|
|
4816
4821
|
readable: this.readShut || rx.readReady,
|
|
4817
4822
|
writable: this.writeShut || tx.writeReady,
|
|
4818
4823
|
hangup: rx.writersGone && tx.readersGone,
|
|
4819
|
-
rdhup: rx.writersGone
|
|
4824
|
+
rdhup: rx.writersGone,
|
|
4825
|
+
nread: this.readShut ? 0 : rx.buffered
|
|
4820
4826
|
};
|
|
4821
4827
|
}
|
|
4822
4828
|
changed(signal) {
|
|
@@ -4876,8 +4882,8 @@ var KernelSocket = class _KernelSocket {
|
|
|
4876
4882
|
if (this.state !== "open") throw new KernelError("EINVAL");
|
|
4877
4883
|
if (addr.family !== this.domain) throw new KernelError("EAFNOSUPPORT");
|
|
4878
4884
|
this.failed = void 0;
|
|
4879
|
-
const
|
|
4880
|
-
if (
|
|
4885
|
+
const route2 = viaUplink && addr.family === "inet" ? this.net.uplinkFor(addr.host) : void 0;
|
|
4886
|
+
if (route2) return this.dial(addr, route2);
|
|
4881
4887
|
const target = this.net.target(addr, name2);
|
|
4882
4888
|
if (!this.local) {
|
|
4883
4889
|
if (addr.family === "inet") {
|
|
@@ -4933,9 +4939,9 @@ var KernelSocket = class _KernelSocket {
|
|
|
4933
4939
|
}
|
|
4934
4940
|
}
|
|
4935
4941
|
switch;
|
|
4936
|
-
connectSecure(addr,
|
|
4942
|
+
connectSecure(addr, route2, secure2) {
|
|
4937
4943
|
if (this.state !== "open") throw new KernelError("EISCONN");
|
|
4938
|
-
return this.dial(addr,
|
|
4944
|
+
return this.dial(addr, route2, secure2);
|
|
4939
4945
|
}
|
|
4940
4946
|
upgradeSecure(make) {
|
|
4941
4947
|
if (this.state !== "connected") throw new KernelError("ENOTCONN");
|
|
@@ -4943,7 +4949,7 @@ var KernelSocket = class _KernelSocket {
|
|
|
4943
4949
|
if (this.switch.upgraded || this.writeShut || this.readShut) throw new KernelError("EINVAL");
|
|
4944
4950
|
return this.switch.upgrade(make);
|
|
4945
4951
|
}
|
|
4946
|
-
dial(addr,
|
|
4952
|
+
dial(addr, route2, secure2) {
|
|
4947
4953
|
this.state = "connecting";
|
|
4948
4954
|
const abort = new AbortController();
|
|
4949
4955
|
this.dialing = abort;
|
|
@@ -4951,13 +4957,13 @@ var KernelSocket = class _KernelSocket {
|
|
|
4951
4957
|
const timer = setTimeout(() => {
|
|
4952
4958
|
timedOut = true;
|
|
4953
4959
|
abort.abort(new KernelError("ETIMEDOUT"));
|
|
4954
|
-
},
|
|
4960
|
+
}, route2.timeoutMs ?? DIAL_TIMEOUT_MS);
|
|
4955
4961
|
const abandoned = new Promise((_, reject) => {
|
|
4956
4962
|
abort.signal.addEventListener("abort", () => reject(abort.signal.reason), { once: true });
|
|
4957
4963
|
});
|
|
4958
4964
|
const { host, port } = addr;
|
|
4959
4965
|
const dialled = new Promise(
|
|
4960
|
-
(resolve2) => resolve2(
|
|
4966
|
+
(resolve2) => resolve2(route2.uplink.dial({ network: "tcp", host, port, signal: abort.signal }))
|
|
4961
4967
|
);
|
|
4962
4968
|
dialled.then(
|
|
4963
4969
|
(conn) => abort.signal.aborted && conn.close(),
|
|
@@ -5106,14 +5112,14 @@ var LoopbackNet = class {
|
|
|
5106
5112
|
if (!this.origins) throw new KernelError("ENETUNREACH");
|
|
5107
5113
|
void this.origins(conn, origin).catch(() => conn.close());
|
|
5108
5114
|
}
|
|
5109
|
-
useUplink(
|
|
5110
|
-
this.route =
|
|
5115
|
+
useUplink(route2) {
|
|
5116
|
+
this.route = route2;
|
|
5111
5117
|
}
|
|
5112
5118
|
tlsClient;
|
|
5113
5119
|
uplinkFor(host) {
|
|
5114
|
-
const
|
|
5115
|
-
if (!
|
|
5116
|
-
return this.destination(host) === "uplink" ?
|
|
5120
|
+
const route2 = this.route;
|
|
5121
|
+
if (!route2) return void 0;
|
|
5122
|
+
return this.destination(host) === "uplink" ? route2 : void 0;
|
|
5117
5123
|
}
|
|
5118
5124
|
destination(host) {
|
|
5119
5125
|
return (this.route?.routes ?? LOCAL_ONLY).classify(host);
|
|
@@ -6626,7 +6632,7 @@ function satisfies(version, range2) {
|
|
|
6626
6632
|
}
|
|
6627
6633
|
|
|
6628
6634
|
// src/version.ts
|
|
6629
|
-
var KERNEL_VERSION = true ? "1.
|
|
6635
|
+
var KERNEL_VERSION = true ? "1.125.0" : "0.0.0-dev";
|
|
6630
6636
|
var OS_RELEASE = "6.0.0-slicc";
|
|
6631
6637
|
function procVersion() {
|
|
6632
6638
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -9419,6 +9425,8 @@ async function physicalPathAsync(fs, path) {
|
|
|
9419
9425
|
}
|
|
9420
9426
|
|
|
9421
9427
|
// src/fs/watch.ts
|
|
9428
|
+
var WATCH_NOTES_LIMIT = 1e3;
|
|
9429
|
+
var baseOf2 = (path) => path.slice(path.lastIndexOf("/") + 1);
|
|
9422
9430
|
var WATCH_BATCH_LIMIT = 1e3;
|
|
9423
9431
|
var under3 = (path, dir) => dir === "/" || path.startsWith(`${dir}/`);
|
|
9424
9432
|
var parentOf6 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
|
|
@@ -9428,8 +9436,39 @@ function touches(watcher, changed) {
|
|
|
9428
9436
|
);
|
|
9429
9437
|
}
|
|
9430
9438
|
var REALS_KEPT = 4096;
|
|
9439
|
+
function noted(watch, note) {
|
|
9440
|
+
if (watch.ended) return;
|
|
9441
|
+
if ("ended" in note) {
|
|
9442
|
+
watch.notes.push(note);
|
|
9443
|
+
watch.ended = true;
|
|
9444
|
+
watch.wake?.();
|
|
9445
|
+
return;
|
|
9446
|
+
}
|
|
9447
|
+
if (watch.overflowed) return;
|
|
9448
|
+
if (watch.notes.length >= WATCH_NOTES_LIMIT) {
|
|
9449
|
+
watch.overflowed = true;
|
|
9450
|
+
watch.notes.push({ overflow: true });
|
|
9451
|
+
} else watch.notes.push(note);
|
|
9452
|
+
watch.wake?.();
|
|
9453
|
+
}
|
|
9454
|
+
function route(watch, type, path) {
|
|
9455
|
+
for (const root of watch.roots) if (routed(watch, root, type, path)) return;
|
|
9456
|
+
}
|
|
9457
|
+
function routed(watch, root, type, path) {
|
|
9458
|
+
if (path === root || type === "rename" && root !== "/" && under3(root, path)) {
|
|
9459
|
+
noted(watch, { type, filename: baseOf2(root) });
|
|
9460
|
+
if (type === "rename") noted(watch, { ended: true });
|
|
9461
|
+
return true;
|
|
9462
|
+
}
|
|
9463
|
+
if (parentOf6(path) === root) noted(watch, { type, filename: baseOf2(path) });
|
|
9464
|
+
else if (watch.recursive && under3(path, root)) {
|
|
9465
|
+
noted(watch, { type, filename: path.slice(root === "/" ? 1 : root.length + 1) });
|
|
9466
|
+
} else return false;
|
|
9467
|
+
return true;
|
|
9468
|
+
}
|
|
9431
9469
|
var FsWatchers = class {
|
|
9432
9470
|
watchers = /* @__PURE__ */ new Set();
|
|
9471
|
+
typed = /* @__PURE__ */ new Set();
|
|
9433
9472
|
taps = [];
|
|
9434
9473
|
reals = /* @__PURE__ */ new Map();
|
|
9435
9474
|
reshapes = [];
|
|
@@ -9456,7 +9495,65 @@ var FsWatchers = class {
|
|
|
9456
9495
|
this.watchers.delete(watcher);
|
|
9457
9496
|
};
|
|
9458
9497
|
}
|
|
9498
|
+
typedWatch(root, recursive, alias) {
|
|
9499
|
+
const main = normalizePath(root);
|
|
9500
|
+
const other = alias === void 0 ? main : normalizePath(alias);
|
|
9501
|
+
const watch = {
|
|
9502
|
+
roots: other === main ? [main] : [main, other],
|
|
9503
|
+
recursive,
|
|
9504
|
+
notes: [],
|
|
9505
|
+
overflowed: false,
|
|
9506
|
+
ended: false,
|
|
9507
|
+
wake: void 0
|
|
9508
|
+
};
|
|
9509
|
+
this.typed.add(watch);
|
|
9510
|
+
const close = () => {
|
|
9511
|
+
this.typed.delete(watch);
|
|
9512
|
+
watch.ended = true;
|
|
9513
|
+
watch.wake?.();
|
|
9514
|
+
};
|
|
9515
|
+
return {
|
|
9516
|
+
close,
|
|
9517
|
+
next: async (signal) => {
|
|
9518
|
+
while (watch.notes.length === 0 && !watch.ended && !signal.aborted) {
|
|
9519
|
+
await new Promise((resolve2) => {
|
|
9520
|
+
const done4 = () => {
|
|
9521
|
+
watch.wake = void 0;
|
|
9522
|
+
signal.removeEventListener("abort", done4);
|
|
9523
|
+
resolve2();
|
|
9524
|
+
};
|
|
9525
|
+
watch.wake = done4;
|
|
9526
|
+
signal.addEventListener("abort", done4, { once: true });
|
|
9527
|
+
});
|
|
9528
|
+
}
|
|
9529
|
+
const notes = watch.notes.splice(0);
|
|
9530
|
+
watch.overflowed = false;
|
|
9531
|
+
if (watch.ended) this.typed.delete(watch);
|
|
9532
|
+
return notes.length === 0 && watch.ended ? [{ ended: true }] : notes;
|
|
9533
|
+
}
|
|
9534
|
+
};
|
|
9535
|
+
}
|
|
9536
|
+
covers(path) {
|
|
9537
|
+
for (const watch of this.typed) {
|
|
9538
|
+
for (const root of watch.roots) {
|
|
9539
|
+
if (path === root || parentOf6(path) === root || watch.recursive && under3(path, root))
|
|
9540
|
+
return true;
|
|
9541
|
+
}
|
|
9542
|
+
}
|
|
9543
|
+
return false;
|
|
9544
|
+
}
|
|
9545
|
+
emit(type, paths) {
|
|
9546
|
+
if (this.typed.size === 0) return;
|
|
9547
|
+
for (const path of paths) {
|
|
9548
|
+
const at = normalizePath(path);
|
|
9549
|
+
for (const watch of [...this.typed]) route(watch, type, at);
|
|
9550
|
+
}
|
|
9551
|
+
}
|
|
9459
9552
|
changed(...paths) {
|
|
9553
|
+
this.report(paths);
|
|
9554
|
+
this.emit("change", paths);
|
|
9555
|
+
}
|
|
9556
|
+
report(paths) {
|
|
9460
9557
|
for (const tap of this.taps) tap(paths);
|
|
9461
9558
|
for (const watcher of this.watchers) {
|
|
9462
9559
|
for (const path of paths) {
|
|
@@ -9476,11 +9573,15 @@ var FsWatchers = class {
|
|
|
9476
9573
|
watcher.notify(paths.length > WATCH_BATCH_LIMIT ? { overflow: true } : { paths });
|
|
9477
9574
|
}
|
|
9478
9575
|
wrap(fs) {
|
|
9479
|
-
const after = (run2, changed) => async (...args) => {
|
|
9576
|
+
const after = (run2, changed, type) => async (...args) => {
|
|
9577
|
+
let done4 = false;
|
|
9480
9578
|
try {
|
|
9481
9579
|
await run2(...args);
|
|
9580
|
+
done4 = true;
|
|
9482
9581
|
} finally {
|
|
9483
|
-
|
|
9582
|
+
const paths = changed(...args);
|
|
9583
|
+
this.report(paths);
|
|
9584
|
+
if (done4) this.emit(type, paths);
|
|
9484
9585
|
}
|
|
9485
9586
|
};
|
|
9486
9587
|
const reals = this.reals;
|
|
@@ -9499,15 +9600,21 @@ var FsWatchers = class {
|
|
|
9499
9600
|
reals.set(at, real);
|
|
9500
9601
|
return real;
|
|
9501
9602
|
};
|
|
9502
|
-
const content = (run2) => async (...args) => {
|
|
9603
|
+
const content = (run2, types) => async (...args) => {
|
|
9604
|
+
const at = normalizePath(args[0]);
|
|
9605
|
+
const fresh2 = this.covers(at) && !await fs.exists(at).catch(() => true);
|
|
9606
|
+
let done4 = false;
|
|
9503
9607
|
try {
|
|
9504
9608
|
await run2(...args);
|
|
9609
|
+
done4 = true;
|
|
9505
9610
|
} finally {
|
|
9506
|
-
const at = normalizePath(args[0]);
|
|
9507
9611
|
const real = await realOf(at);
|
|
9508
|
-
|
|
9612
|
+
const paths = real === at ? [at] : [at, real];
|
|
9613
|
+
this.report(paths);
|
|
9614
|
+
if (done4) for (const type of types(fresh2)) this.emit(type, paths);
|
|
9509
9615
|
}
|
|
9510
9616
|
};
|
|
9617
|
+
const written2 = (fresh2) => fresh2 ? ["rename", "change"] : ["change"];
|
|
9511
9618
|
const reshaping = (run2) => async (...args) => {
|
|
9512
9619
|
this.reshaped();
|
|
9513
9620
|
try {
|
|
@@ -9517,8 +9624,8 @@ var FsWatchers = class {
|
|
|
9517
9624
|
}
|
|
9518
9625
|
};
|
|
9519
9626
|
const overrides = {
|
|
9520
|
-
writeFile: content(fs.writeFile.bind(fs)),
|
|
9521
|
-
...fs.createExclusive ? { createExclusive: content(fs.createExclusive.bind(fs)) } : {},
|
|
9627
|
+
writeFile: content(fs.writeFile.bind(fs), written2),
|
|
9628
|
+
...fs.createExclusive ? { createExclusive: content(fs.createExclusive.bind(fs), () => ["rename"]) } : {},
|
|
9522
9629
|
mkdir: async (path, options) => {
|
|
9523
9630
|
const created3 = [];
|
|
9524
9631
|
if (options?.recursive) {
|
|
@@ -9528,33 +9635,39 @@ var FsWatchers = class {
|
|
|
9528
9635
|
if (created3.length > 0 || !await fs.exists(ancestor)) created3.push(ancestor);
|
|
9529
9636
|
}
|
|
9530
9637
|
}
|
|
9638
|
+
const fresh2 = !options?.recursive || !(this.covers(path) && await fs.exists(path));
|
|
9639
|
+
let done4 = false;
|
|
9531
9640
|
try {
|
|
9532
9641
|
await fs.mkdir(path, options);
|
|
9642
|
+
done4 = true;
|
|
9533
9643
|
} finally {
|
|
9534
|
-
this.
|
|
9644
|
+
this.report([...created3, path]);
|
|
9645
|
+
if (done4) this.emit("rename", fresh2 ? [...created3, path] : created3);
|
|
9535
9646
|
}
|
|
9536
9647
|
},
|
|
9537
|
-
rm: reshaping(after(fs.rm.bind(fs), (path) => [path])),
|
|
9538
|
-
rename: reshaping(after(fs.rename.bind(fs), (from, to) => [from, to])),
|
|
9648
|
+
rm: reshaping(after(fs.rm.bind(fs), (path) => [path], "rename")),
|
|
9649
|
+
rename: reshaping(after(fs.rename.bind(fs), (from, to) => [from, to], "rename")),
|
|
9539
9650
|
link: reshaping(
|
|
9540
9651
|
after(
|
|
9541
9652
|
(from, to, follow2) => fs.link(from, to, follow2),
|
|
9542
|
-
(_from, to) => [to]
|
|
9653
|
+
(_from, to) => [to],
|
|
9654
|
+
"rename"
|
|
9543
9655
|
)
|
|
9544
9656
|
),
|
|
9545
|
-
symlink: reshaping(after(fs.symlink.bind(fs), (_target, path) => [path])),
|
|
9546
|
-
chmod: after(fs.chmod.bind(fs), (path) => [path]),
|
|
9547
|
-
...fs.chown ? { chown: after(fs.chown.bind(fs), (path) => [path]) } : {},
|
|
9548
|
-
utimes: after(fs.utimes.bind(fs), (path) => [path]),
|
|
9657
|
+
symlink: reshaping(after(fs.symlink.bind(fs), (_target, path) => [path], "rename")),
|
|
9658
|
+
chmod: after(fs.chmod.bind(fs), (path) => [path], "change"),
|
|
9659
|
+
...fs.chown ? { chown: after(fs.chown.bind(fs), (path) => [path], "change") } : {},
|
|
9660
|
+
utimes: after(fs.utimes.bind(fs), (path) => [path], "change"),
|
|
9549
9661
|
lutimes: after(
|
|
9550
9662
|
(path, atime, mtime) => lutimesOf(fs, path, atime, mtime),
|
|
9551
|
-
(path) => [path]
|
|
9663
|
+
(path) => [path],
|
|
9664
|
+
"change"
|
|
9552
9665
|
)
|
|
9553
9666
|
};
|
|
9554
9667
|
const ranged2 = rangedOps(fs);
|
|
9555
9668
|
if (ranged2) {
|
|
9556
|
-
overrides.pwrite = content(ranged2.pwrite);
|
|
9557
|
-
overrides.truncate = content(ranged2.truncate);
|
|
9669
|
+
overrides.pwrite = content(ranged2.pwrite, () => ["change"]);
|
|
9670
|
+
overrides.truncate = content(ranged2.truncate, () => ["change"]);
|
|
9558
9671
|
}
|
|
9559
9672
|
return new Proxy(fs, {
|
|
9560
9673
|
get(target, key, receiver) {
|
|
@@ -10162,7 +10275,8 @@ var FifoTable = class {
|
|
|
10162
10275
|
poll: () => ({
|
|
10163
10276
|
readable: reads && pipe.readReady,
|
|
10164
10277
|
writable: writes && pipe.writeReady,
|
|
10165
|
-
hangup: reads ? pipe.writersGone : pipe.readersGone
|
|
10278
|
+
hangup: reads ? pipe.writersGone : pipe.readersGone,
|
|
10279
|
+
...reads ? { nread: pipe.buffered } : {}
|
|
10166
10280
|
}),
|
|
10167
10281
|
changed: (signal) => pipe.changed(signal)
|
|
10168
10282
|
});
|
|
@@ -10561,8 +10675,8 @@ var MountTable = class {
|
|
|
10561
10675
|
...error ? { error } : {}
|
|
10562
10676
|
})
|
|
10563
10677
|
);
|
|
10564
|
-
const
|
|
10565
|
-
return [...mounted, ...
|
|
10678
|
+
const noted2 = [...this.notes.values()].filter((note) => !this.mounts.has(note.target));
|
|
10679
|
+
return [...mounted, ...noted2.map((note) => ({ ...note, options: { ...note.options } }))];
|
|
10566
10680
|
}
|
|
10567
10681
|
listed() {
|
|
10568
10682
|
return this.list().filter((entry) => !this.system.has(entry.target));
|
|
@@ -16031,7 +16145,8 @@ var KernelTty = class {
|
|
|
16031
16145
|
poll: () => ({
|
|
16032
16146
|
readable: this.readable.length > 0 || this.hungUp,
|
|
16033
16147
|
writable: true,
|
|
16034
|
-
hangup: this.hungUp
|
|
16148
|
+
hangup: this.hungUp,
|
|
16149
|
+
nread: this.readable.reduce((n, chunk2) => n + (chunk2?.length ?? 0), 0)
|
|
16035
16150
|
}),
|
|
16036
16151
|
changed: (signal) => this.changed(signal),
|
|
16037
16152
|
close: () => {
|
|
@@ -16227,7 +16342,8 @@ var PtyPair = class {
|
|
|
16227
16342
|
poll: () => ({
|
|
16228
16343
|
readable: this.queue.length > 0 || this.slaveGone,
|
|
16229
16344
|
writable: true,
|
|
16230
|
-
hangup: this.slaveGone
|
|
16345
|
+
hangup: this.slaveGone,
|
|
16346
|
+
nread: this.queue.reduce((n, chunk2) => n + chunk2.length, 0)
|
|
16231
16347
|
}),
|
|
16232
16348
|
changed: (signal) => this.changed(signal),
|
|
16233
16349
|
close: () => this.closeMaster(),
|
|
@@ -16509,13 +16625,17 @@ async function runChild(req, deps) {
|
|
|
16509
16625
|
// src/kernel/select.ts
|
|
16510
16626
|
function ready(fds, read, write) {
|
|
16511
16627
|
const hangup = /* @__PURE__ */ new Set();
|
|
16628
|
+
const nread = {};
|
|
16512
16629
|
const pick = (list, dir) => list.filter((fd) => {
|
|
16513
16630
|
const state = pollFile(fds.get(fd).file);
|
|
16514
16631
|
if (state.hangup) hangup.add(fd);
|
|
16515
|
-
|
|
16632
|
+
const picked = state[dir] || state.hangup;
|
|
16633
|
+
if (picked && dir === "readable" && state.nread !== void 0) nread[fd] = state.nread;
|
|
16634
|
+
return picked;
|
|
16516
16635
|
});
|
|
16517
16636
|
const out = { read: pick(read, "readable"), write: pick(write, "writable") };
|
|
16518
16637
|
if (hangup.size > 0) out.hangup = [...hangup];
|
|
16638
|
+
if (Object.keys(nread).length > 0) out.nread = nread;
|
|
16519
16639
|
return out;
|
|
16520
16640
|
}
|
|
16521
16641
|
function interruption(signal) {
|
|
@@ -17510,14 +17630,14 @@ async function connectSecure(proc, socket, host, port, req) {
|
|
|
17510
17630
|
const servername = serverName(req.servername || host);
|
|
17511
17631
|
if (isLoopbackHost(servername) || isLoopbackHost(host)) throw new KernelError("ENOTSUP");
|
|
17512
17632
|
const address = await uplinkAddress(proc, host);
|
|
17513
|
-
const
|
|
17514
|
-
if (!
|
|
17633
|
+
const route2 = proc.net.uplinkFor(address);
|
|
17634
|
+
if (!route2)
|
|
17515
17635
|
throw new KernelError("ENOTSUP", "TLS in the kernel needs a destination the uplink routes");
|
|
17516
17636
|
const support = supportOf(proc.net);
|
|
17517
17637
|
let info;
|
|
17518
17638
|
const { make, abandon } = secure(await prepare(support, req, servername));
|
|
17519
17639
|
try {
|
|
17520
|
-
await socket.connectSecure({ family: "inet", host: address, port },
|
|
17640
|
+
await socket.connectSecure({ family: "inet", host: address, port }, route2, async (conn) => {
|
|
17521
17641
|
info = await conn.upgrade(make);
|
|
17522
17642
|
});
|
|
17523
17643
|
} finally {
|
|
@@ -18728,6 +18848,65 @@ function vfsFile(fs, opts, nodes) {
|
|
|
18728
18848
|
});
|
|
18729
18849
|
}
|
|
18730
18850
|
|
|
18851
|
+
// src/kernel/watch-ops.ts
|
|
18852
|
+
var WATCH_OPS = ["watch-open", "watch-next", "watch-close"];
|
|
18853
|
+
var WATCHING = new Set(WATCH_OPS);
|
|
18854
|
+
function isWatchSyscall(req) {
|
|
18855
|
+
return WATCHING.has(req.op);
|
|
18856
|
+
}
|
|
18857
|
+
var WATCHES_MAX = 8192;
|
|
18858
|
+
var parentOf8 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
|
|
18859
|
+
function visibleTo2(watch, root, visible) {
|
|
18860
|
+
return {
|
|
18861
|
+
close: () => watch.close(),
|
|
18862
|
+
next: async (signal) => {
|
|
18863
|
+
const notes = await watch.next(signal);
|
|
18864
|
+
const seen = /* @__PURE__ */ new Map();
|
|
18865
|
+
const shown = await Promise.all(
|
|
18866
|
+
notes.map(async (note) => {
|
|
18867
|
+
if (!("filename" in note) || !note.filename.includes("/")) return true;
|
|
18868
|
+
const dir = parentOf8(`${root === "/" ? "" : root}/${note.filename}`);
|
|
18869
|
+
if (!seen.has(dir)) seen.set(dir, visible(dir));
|
|
18870
|
+
return seen.get(dir);
|
|
18871
|
+
})
|
|
18872
|
+
);
|
|
18873
|
+
return notes.filter((_note, i) => shown[i]);
|
|
18874
|
+
}
|
|
18875
|
+
};
|
|
18876
|
+
}
|
|
18877
|
+
var ProcessWatches = class {
|
|
18878
|
+
open = /* @__PURE__ */ new Map();
|
|
18879
|
+
nextId = 1;
|
|
18880
|
+
source;
|
|
18881
|
+
constructor(source) {
|
|
18882
|
+
this.source = source;
|
|
18883
|
+
}
|
|
18884
|
+
async syscall(req, cancelled) {
|
|
18885
|
+
if (req.op === "watch-open") {
|
|
18886
|
+
if (!this.source) throw new KernelError("ENOSYS");
|
|
18887
|
+
if (this.open.size >= WATCHES_MAX) throw new KernelError("ENOSPC");
|
|
18888
|
+
const watch2 = await this.source.open(req.path, req.recursive === true);
|
|
18889
|
+
const id2 = this.nextId++;
|
|
18890
|
+
this.open.set(id2, watch2);
|
|
18891
|
+
return { ok: true, kind: "json", json: id2 };
|
|
18892
|
+
}
|
|
18893
|
+
const watch = this.open.get(req.id);
|
|
18894
|
+
if (req.op === "watch-close") {
|
|
18895
|
+
watch?.close();
|
|
18896
|
+
this.open.delete(req.id);
|
|
18897
|
+
return { ok: true, kind: "void" };
|
|
18898
|
+
}
|
|
18899
|
+
if (!watch) return { ok: true, kind: "json", json: [{ ended: true }] };
|
|
18900
|
+
const notes = await watch.next(cancelled);
|
|
18901
|
+
if (notes.some((note) => "ended" in note)) this.open.delete(req.id);
|
|
18902
|
+
return { ok: true, kind: "json", json: notes };
|
|
18903
|
+
}
|
|
18904
|
+
closeAll() {
|
|
18905
|
+
for (const watch of this.open.values()) watch.close();
|
|
18906
|
+
this.open.clear();
|
|
18907
|
+
}
|
|
18908
|
+
};
|
|
18909
|
+
|
|
18731
18910
|
// src/kernel/process.ts
|
|
18732
18911
|
function aborted(signal) {
|
|
18733
18912
|
return new Promise((_, reject) => {
|
|
@@ -18854,7 +19033,8 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
|
|
|
18854
19033
|
...SEM_OPS,
|
|
18855
19034
|
...HTTP_OPS,
|
|
18856
19035
|
...PTY_OPS,
|
|
18857
|
-
...HOST_OPS
|
|
19036
|
+
...HOST_OPS,
|
|
19037
|
+
...WATCH_OPS
|
|
18858
19038
|
]);
|
|
18859
19039
|
function isWasmSyscall(req) {
|
|
18860
19040
|
const op = req.op;
|
|
@@ -18906,6 +19086,7 @@ function timeLeft(req, started) {
|
|
|
18906
19086
|
}
|
|
18907
19087
|
var WasmProcess = class {
|
|
18908
19088
|
exited = false;
|
|
19089
|
+
watches;
|
|
18909
19090
|
children;
|
|
18910
19091
|
deathSignals = /* @__PURE__ */ new Map();
|
|
18911
19092
|
caught = 0;
|
|
@@ -18936,6 +19117,7 @@ var WasmProcess = class {
|
|
|
18936
19117
|
this.pid = pid;
|
|
18937
19118
|
this.fds = fds;
|
|
18938
19119
|
this.options = options;
|
|
19120
|
+
this.watches = new ProcessWatches(options.watches);
|
|
18939
19121
|
this.inherited = options.ignored ?? 0;
|
|
18940
19122
|
this.umask = options.umask ?? DEFAULT_UMASK2;
|
|
18941
19123
|
this.cred = copyCred(options.cred ?? ROOT);
|
|
@@ -19193,6 +19375,7 @@ var WasmProcess = class {
|
|
|
19193
19375
|
});
|
|
19194
19376
|
}
|
|
19195
19377
|
if (isHttpSyscall(req)) return await this.httpSyscall(req);
|
|
19378
|
+
if (isWatchSyscall(req)) return await this.watches.syscall(req, cancelled);
|
|
19196
19379
|
if (isHostSyscall(req)) {
|
|
19197
19380
|
return await hostSyscall(req, {
|
|
19198
19381
|
pid: this.pid,
|
|
@@ -20333,6 +20516,7 @@ var WasmProcess = class {
|
|
|
20333
20516
|
this.options.locks?.release(this.pid);
|
|
20334
20517
|
for (const owner of this.lockOwners) this.options.locks?.release(owner);
|
|
20335
20518
|
this.asyncOps.close();
|
|
20519
|
+
this.watches.closeAll();
|
|
20336
20520
|
for (const [target, sig] of this.deathSignals) this.sendDeath(target, sig);
|
|
20337
20521
|
this.deathSignals.clear();
|
|
20338
20522
|
for (const pid of this.children.pids()) this.options.onReap?.(pid);
|
|
@@ -20594,6 +20778,7 @@ function processOptions(opts, header3, hooks, created3) {
|
|
|
20594
20778
|
locks: opts.locks,
|
|
20595
20779
|
fifos: opts.fifos,
|
|
20596
20780
|
...opts.sems ? { sems: opts.sems } : {},
|
|
20781
|
+
watches: opts.watches,
|
|
20597
20782
|
onReap: opts.onReap,
|
|
20598
20783
|
resolver: opts.resolver,
|
|
20599
20784
|
...notifiers(header3),
|
|
@@ -25811,6 +25996,7 @@ var Launcher = class {
|
|
|
25811
25996
|
locks: this.locks,
|
|
25812
25997
|
fifos: this.fifos,
|
|
25813
25998
|
sems: this.sems,
|
|
25999
|
+
watches: this.watchSource(pid),
|
|
25814
26000
|
resolver: this.resolver,
|
|
25815
26001
|
onReap: (child) => this.reaped(child, pid),
|
|
25816
26002
|
...req.fork ? { fork: req.fork } : {},
|
|
@@ -26354,6 +26540,23 @@ var Launcher = class {
|
|
|
26354
26540
|
mounts: this.mounts.list().map((entry) => entry.target).filter((target) => this.mounts.mounted(target) !== void 0)
|
|
26355
26541
|
});
|
|
26356
26542
|
}
|
|
26543
|
+
watchSource(pid) {
|
|
26544
|
+
return {
|
|
26545
|
+
open: async (path, recursive) => {
|
|
26546
|
+
const at = normalizePath(path);
|
|
26547
|
+
const g = this.execGuard(this.credOf(pid), pid);
|
|
26548
|
+
if (g) await readable(g, "watch", at);
|
|
26549
|
+
const st = await this.fs.stat(at);
|
|
26550
|
+
const real = await this.fs.realpath?.(at, true).catch(() => at) ?? at;
|
|
26551
|
+
const watch = this.watchers.typedWatch(real, recursive && st.isDirectory, at);
|
|
26552
|
+
if (!g || g.cred.euid === 0) return watch;
|
|
26553
|
+
return visibleTo2(watch, real, async (dir) => {
|
|
26554
|
+
const walked = await access(g.fs, g.cred, dir, R_OK | X_OK, { directory: true });
|
|
26555
|
+
return walked.denied === void 0 && walked.missing !== true;
|
|
26556
|
+
});
|
|
26557
|
+
}
|
|
26558
|
+
};
|
|
26559
|
+
}
|
|
26357
26560
|
watchFiles(paths, notify) {
|
|
26358
26561
|
this.fileWatches ??= new FileWatches(this.watchers, this.rootFs);
|
|
26359
26562
|
return this.fileWatches.watch(paths, notify);
|