@ai-ecoverse/slicc-kernel 1.93.3 → 1.94.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 +4 -4
- package/dist/kernel-worker.js +85 -34
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +7 -4
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +104 -38
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +7 -4
- package/dist/process-worker.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,7 +207,7 @@ By default `nodeTransport()` refuses destinations on the host itself, because No
|
|
|
207
207
|
- **How it checks:** it matches on the addresses the name resolves to, so a public name that resolves to `127.0.0.1` is refused too. It checks every request and WebSocket open, `host.slicc.internal` and each redirect hop included.
|
|
208
208
|
- **What a refusal looks like:** a `403` whose body names the option, and `EACCES` for a WebSocket, before any connection is made.
|
|
209
209
|
- **The opt-in:** `nodeTransport({ hostLoopback: true })` allows the host's loopback except ports 9222–9229. `nodeTransport({ hostLoopback: [8080, 9222] })` allows exactly the listed ports, so DevTools ports are reachable only when named. Link-local stays refused either way.
|
|
210
|
-
- **Connect-time enforcement:** the kernel also checks the address each connection actually reaches, before anything is sent, so a name whose DNS answer changes after the check is still refused. A keep-alive connection opened outside this kernel's policy (by the host application, or by another kernel in the same process whose `hostLoopback` allows it) to the same origin could be reused without a new check. When a proxy is in use (`HTTP_PROXY`/`HTTPS_PROXY` with `--use-env-proxy`), the
|
|
210
|
+
- **Connect-time enforcement:** the kernel also checks the address each connection actually reaches, before anything is sent, so a name whose DNS answer changes after the check is still refused. A keep-alive connection opened outside this kernel's policy (by the host application, or by another kernel in the same process whose `hostLoopback` allows it) to the same origin could be reused without a new check. When a proxy is in use (`HTTP_PROXY`/`HTTPS_PROXY` with `--use-env-proxy` or `NODE_USE_ENV_PROXY=1`), a connection opened to the proxy's own host and port is allowed even on the host's loopback, and so is any request later tunnelled over that same live TCP connection (matched by its local and remote address and port, and forgotten when it closes), so only the pre-check applies to the target; a direct connection, a `NO_PROXY` host's included, is checked as usual, and a proxy that resolves names itself closes the rebinding window on its side.
|
|
211
211
|
|
|
212
212
|
## Commands
|
|
213
213
|
|
|
@@ -223,7 +223,7 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
|
|
|
223
223
|
{ "slicc": { "abi": "wasi", "commands": { "rg": { "wasm": "bin/rg.wasm" }, "rustc": { "script": "bin/rustc" } } } }
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
A command's name is any single path component without whitespace or control characters, so coreutils' `[` is in `/usr/bin` too; a name that is empty, `.` or `..`, or has a `/`, a space, a tab, a newline, NUL or another control character is skipped. Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes.
|
|
226
|
+
A command's name is any single path component without whitespace or control characters, so coreutils' `[` is in `/usr/bin` too; a name that is empty, `.` or `..`, or has a `/`, a space, a tab, a newline, NUL or another control character is skipped. Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The catalog reads its roots in order, and the first root that provides a name wins it: a user's own global packages (for that user's processes), `/node_modules`, root's global packages, then each appended root. Within a root, packages are read by name and the first wins (compared as strings by UTF-16 code unit, so a scoped `@scope/name` comes before names that start with a letter, but after those that start with a digit), whatever order the file system lists them in; a `PNPM_HOME`'s install groups count as one root, read group by group (by the group directory's name, effectively arbitrary for pnpm's hashed names), each by package name. A variant can take over from the package it extends with `"slicc": { "replaces": ["@ai-ecoverse/wasm-imagemagick"] }`: when it and a package it names are in the same root, it wins every command name they share, whatever the order; names only the other provides stay the other's, a third package that provides the name and sorts first still wins it, and packages that replace each other fall back to the name order. A replace never reaches into an earlier root, so root's global packages can't take a name from `/node_modules` or a user's own packages. The field must be an array of package names; anything else is ignored. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. Commands also come from pnpm's global directory. Every process gets a `PNPM_HOME` and has `$PNPM_HOME/bin` on `PATH`: root's is `/usr/local/share/pnpm` (unless the embedder sets its own), a user's `~/.local/share/pnpm` (see [Users](#users)). The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
|
|
227
227
|
|
|
228
228
|
**More roots:** `createKernel({ commands: { append: ['/opt/user/node_modules'] } })` (and `createNodeKernel`) scans more directories, each absolute and normalised (anything else throws a `TypeError`). A name is looked up in a user's own global packages first, then in `modules`, then in root's global packages, then in the appended roots in the order given, and the first match wins, so an appended root never shadows another command (or `sudo`). Their commands are in `/usr/bin` for every user, like root's global packages, and their `binfmt` and file system entries count too. A package in an appended root also gets a command for each `bin` entry of its `package.json` (a string `bin` is named after the package, without its scope), which runs as a `#!` script; a name its `slicc.commands` gives wins. A `bin` entry is skipped when its name isn't a command name (see above), or its file, with symlinks resolved, isn't a regular file inside the package. `/node_modules` and the global packages keep reading only `slicc.commands`. A missing root counts as empty until it appears, and an install or removal in a root is picked up as it happens. With `users: 'enforce'` a root counts only when it and every directory above it are root's and not writable by group or others; another root is skipped without a word. Under `'report'` such a root is still scanned, and reported once to `onDenial` and the report log as `{ pid: 0, uid: 0, op: 'commands', path, errno: 'EACCES' }`. Running a `bin` command needs `r` and `x` on its file under `users` (see [Permissions](#permissions)), checked when it runs, so a `0644` file is `EACCES` under `'enforce'`.
|
|
229
229
|
|
|
@@ -673,7 +673,7 @@ With `users: 'enforce'` the kernel checks every file call that reaches it agains
|
|
|
673
673
|
- **Approval:** `approve(req, { signal })` gets `{ id, kind: 'sudo', user, uid, target, argv, path, cwd, pid, tag?, tty?, createdAt, expiresAt, always: true }`: the caller's name and real uid, the target user, the argv as typed, the resolved `path`, the caller's directory, pid, tag and terminal, and the times in ms. It answers `'once'` (run it), `'always'` (run it, and append `NOPASSWD <user> (<target>) Cwd <dir> Cmnd <path> <args…>` to `/etc/sudoers.d/<user>`, root's `0440`, so it's never asked again for this command, these arguments, in this directory; if that can't be written, nothing runs) or `'deny'`; a throw, any other answer or no hook at all is `sudo: approval denied`. After 5 minutes the request expires (`signal` aborts with `'expired'`, and `sudo` fails with `sudo: the request expired`); when the requester ends or gets a signal while it waits, `^C` included, `signal` aborts with `'withdrawn'` and `sudo` ends with that signal's status. A user has at most one request waiting; another fails with `sudo: another request is pending`. On the page, `createKernel` relays the request from the kernel worker and back, and the abort too; `terminate()` or a failed worker aborts every request still waiting with `'withdrawn'`.
|
|
674
674
|
- **What `'always'` covers:** the same command with the same arguments in the same directory, so a relative argument (`tee config.toml`) can't be pointed elsewhere by running it from another directory. It doesn't make a command that writes, or an interpreter (`bash -c`, `tee`, `cp`), safe to allow: what such a command touches depends on the files it is given, which the caller may control (a symlink, a file it owns). Keep `'always'` to commands that are safe in any state of the files they're given. Lines that kernels before `Cwd` recorded have no `Cwd`, so they match in any directory; to drop them, rewrite `/etc/sudoers.d/<user>` as root.
|
|
675
675
|
- **Running:** the command runs as the caller's child, with all six ids and the groups of the target user, in the caller's directory, on its terminal, standard input, output and error, in a **process group of its own** in the caller's session, as sudo(8) runs it without a pty. When `sudo` has the terminal's foreground, it hands the terminal to the command's group, so `^C`, `^\` and `^Z` reach the command, and takes it back when the command stops or ends. `sudo` exits with the command's status, and passes `SIGTERM`, `SIGHUP`, `SIGINT`, `SIGQUIT`, `SIGUSR1`, `SIGUSR2` and `SIGALRM` on to the command's whole group, so its children get them too; it installs these relays before it asks, so one that comes while the command starts is passed on once it runs (the kernel lets `sudo` signal the group of the command it started, which its own user may not). When the command stops (`^Z`), `sudo` stops by the same signal, so the shell's `jobs`, `fg` and `bg` work on it; when it is continued, it hands the terminal back to the command if it is in the foreground (`fg`) and continues the command's group. A `sudo` killed with `-9` leaves the command running in its group, as sudo(8) does, and the shell takes the terminal back as for any job;
|
|
676
|
-
- **Symlinks:** a command `sudo` started, and every process it starts in turn, follows a symlink only when root or the target user owns it: Linux's `protected_symlinks`, applied in every directory rather than only in sticky world-writable ones. Any other link is `EACCES`, and the denial names the link. That holds in every runtime: in the kernel's own path walk (JS programs' `ctx.fs` and `ctx.open`, WASI paths, `exec` and `cd` by path), in an Emscripten program's lookups (where the denial's op is `follow`), and where a WASI program resolves `..` itself. So a link the caller put where a rule's file or directory is expected (`/tmp/k4l` → `/etc/fnox`, or `config.toml` → `/etc/fnox/config.toml` in the caller's own directory) leads nowhere. `readlink`, `ls -l`, `lstat`, and removing or renaming a link still work, since they don't follow it.
|
|
676
|
+
- **Symlinks:** a command `sudo` started, and every process it starts in turn, follows a symlink only when root or the target user owns it: Linux's `protected_symlinks`, applied in every directory rather than only in sticky world-writable ones. Any other link is `EACCES`, and the denial names the link. That holds in every runtime: in the kernel's own path walk (JS programs' `ctx.fs` and `ctx.open`, WASI paths, `exec` and `cd` by path), in an Emscripten program's lookups (where the denial's op is `follow`), and where a WASI program resolves `..` itself. So a link the caller put where a rule's file or directory is expected (`/tmp/k4l` → `/etc/fnox`, or `config.toml` → `/etc/fnox/config.toml` in the caller's own directory) leads nowhere. `readlink`, `ls -l`, `lstat`, and removing or renaming a link still work, since they don't follow it.
|
|
677
677
|
- **Cost:** with `users: 'off'`, and with owners on for programs that don't call `sudo`, nothing is added to any call. The symlink check costs those processes one owner compare per link in the kernel's walk (the owner is already loaded) and one kernel call per link an Emscripten program follows; other processes pay nothing for it.
|
|
678
678
|
|
|
679
679
|
### `/dev/sealed`
|
|
@@ -748,7 +748,7 @@ A character device (`0666`), there in every `users` mode, through which programs
|
|
|
748
748
|
|
|
749
749
|
### `/proc`
|
|
750
750
|
|
|
751
|
-
Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client. A process that exits while a program reads the `/proc` directory doesn't fail or cut short the listing: its pid is still listed, and opening its files fails with `ENOENT`, which `ps`, `pgrep` and `pidof` skip, as on Linux; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. `/proc/version` names the kernel, Linux-shaped, with the slicc-kernel version that built it: `Linux version 6.0.0-slicc (slicc-kernel <version>) #1 SMP PREEMPT_DYNAMIC`. `/proc/sys/kernel/osrelease` is `6.0.0-slicc` and `/proc/sys/kernel/ostype` is `Linux`. The version is the one npm publishes, set when the package is built (`0.0.0-dev` when run from the sources). A JS program reads it as `ctx.kernel.version`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`; a process with a tag, see `kernel.run`, also has `Tag:\t<tag>` after `Gid:`, shown to root and to the same uid only, with newlines, tabs and carriage returns written `\n`, `\t` and `\r`) and each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times are measured per process, its threads and the images it exec'd included: time a worker spends waiting for the kernel's answer is system time (at most 20 ms of each call; the rest of a longer one, such as a write into a full pipe, counts as blocked), time it spends blocked (a read, a `poll` or `select` that waits, a sleep, `wait`, a futex, while stopped) is neither (a
|
|
751
|
+
Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client. A process that exits while a program reads the `/proc` directory doesn't fail or cut short the listing: its pid is still listed, and opening its files fails with `ENOENT`, which `ps`, `pgrep` and `pidof` skip, as on Linux; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. `/proc/version` names the kernel, Linux-shaped, with the slicc-kernel version that built it: `Linux version 6.0.0-slicc (slicc-kernel <version>) #1 SMP PREEMPT_DYNAMIC`. `/proc/sys/kernel/osrelease` is `6.0.0-slicc` and `/proc/sys/kernel/ostype` is `Linux`. The version is the one npm publishes, set when the package is built (`0.0.0-dev` when run from the sources). A JS program reads it as `ctx.kernel.version`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`; a process with a tag, see `kernel.run`, also has `Tag:\t<tag>` after `Gid:`, shown to root and to the same uid only, with newlines, tabs and carriage returns written `\n`, `\t` and `\r`) and each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times are measured per process, its threads and the images it exec'd included: time a worker spends waiting for the kernel's answer is system time (at most 20 ms of each call; the rest of a longer one, such as a write into a full pipe, counts as blocked), time it spends blocked (a read, a `poll` or `select` that waits, a sleep, `wait`, a futex, while stopped) is neither, from the moment the call actually starts to wait (a call that could wait but answers at once, such as a `select` with a zero timeout or a ready descriptor, a `waitpid` with `WNOHANG`, or a futex wait whose value has already changed, is system time), and the rest is user time; a process's times stop when it ends, a call still in flight then adds nothing, so a loop that never calls the kernel shows as user time while it runs; they show in `/proc/<pid>/stat`, `ps` and `ps()`, and the times of the children a process has waited for (theirs and their own waited-for children's, as POSIX counts them) show as its `cutime` and `cstime` in `/proc/<pid>/stat`; a stopped process is `T` there, in `/proc/<pid>/status` (`T (stopped)`) and in `ps()`; a WASI or WASIX program's `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` (`clock_time_get`) count only the time its thread runs, its own waits and blocked kernel calls left out (a sleep is not CPU time), per thread for now, so a threaded program's process clock is its calling thread's; and `/proc/stat` counts every process of the kernel, the reaped ones included, and `/proc/loadavg` averages the processes not blocked, sampled every 5 seconds. A JS program's time isn't measured yet. `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) is a placeholder. A process that isn't root sees only its own user's processes (see [Users](#users)). WASI and WASIX programs see the same `/proc`, read-only (opening a file in it for writing, or linking one of its files elsewhere, fails with `EROFS`), from every thread, also through a symlink that points into it (where every change, `mkdir`, `rename`, `unlink`, `chmod`, a hard link from it or into it, and so on, fails too: with `EROFS` when the root file system has a physical `/proc`, which stays hidden and untouched, and otherwise with the `ENOENT` of the missing directory, since that check is only paid for when a physical `/proc` exists; with one, a symlink whose target passes `..` after a file or a missing name, such as `/f/../proc`, fails with `ENOTDIR` or `ENOENT`, as on Linux), and `/` lists it, so a WASI program gets a preopen for it; `/proc/self` is the program's own pid there too (the pid an exec'd program took over). JS programs read the same `/proc` through `ctx.fs` and `ctx.open` (see [JS programs](#js-programs)). Neither has `/proc/<pid>/fd` yet.
|
|
752
752
|
|
|
753
753
|
The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#users)). While the root has none of them, or no `/etc/mtab`, reading them gets per-process ones: root is uid 0 and the realm user, uid 1000, is named after `USER`, or `web_user` (Emscripten's default) without one, so it matches `$USER`, with `HOME` as its home, so `id`, `whoami`, `ls -l` and `ps` show names. Nothing is written to OPFS, and a real file there wins.
|
|
754
754
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -7787,10 +7787,15 @@ async function binCommandsOf(physical, pkg, manifest, taken) {
|
|
|
7787
7787
|
}
|
|
7788
7788
|
return out;
|
|
7789
7789
|
}
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
const
|
|
7793
|
-
|
|
7790
|
+
var PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
7791
|
+
function replacesOf(manifest) {
|
|
7792
|
+
const replaces = manifest.slicc?.replaces;
|
|
7793
|
+
if (!Array.isArray(replaces)) return [];
|
|
7794
|
+
return replaces.every((name2) => typeof name2 === "string" && PACKAGE_NAME.test(name2)) ? replaces : [];
|
|
7795
|
+
}
|
|
7796
|
+
async function tierPackages(fs, physical, roots, bins) {
|
|
7797
|
+
const out = [];
|
|
7798
|
+
for (const root of roots) {
|
|
7794
7799
|
for (const pkg of await packages(fs, root)) {
|
|
7795
7800
|
let manifest;
|
|
7796
7801
|
try {
|
|
@@ -7800,11 +7805,47 @@ async function scanCommands(fs, modules, binsFrom = Number.POSITIVE_INFINITY) {
|
|
|
7800
7805
|
}
|
|
7801
7806
|
const own = commandsOf(pkg, manifest);
|
|
7802
7807
|
const named = new Set(own.map((command) => command.name));
|
|
7803
|
-
const
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
+
const extra = bins ? await binCommandsOf(physical, pkg, manifest, named) : [];
|
|
7809
|
+
out.push({
|
|
7810
|
+
name: pkg.slice(root.length + 1),
|
|
7811
|
+
pkg,
|
|
7812
|
+
replaces: replacesOf(manifest),
|
|
7813
|
+
commands: [...own, ...extra]
|
|
7814
|
+
});
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
return out;
|
|
7818
|
+
}
|
|
7819
|
+
function winners(tier) {
|
|
7820
|
+
const providers = /* @__PURE__ */ new Map();
|
|
7821
|
+
for (const entry of tier) {
|
|
7822
|
+
for (const command of entry.commands) {
|
|
7823
|
+
const list = providers.get(command.name) ?? [];
|
|
7824
|
+
if (!list.includes(entry)) list.push(entry);
|
|
7825
|
+
providers.set(command.name, list);
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
7828
|
+
const out = /* @__PURE__ */ new Map();
|
|
7829
|
+
for (const [name2, list] of providers) {
|
|
7830
|
+
const kept = list.filter(
|
|
7831
|
+
(entry2) => !list.some((other) => other !== entry2 && other.replaces.includes(entry2.name))
|
|
7832
|
+
);
|
|
7833
|
+
const entry = kept[0] ?? list[0];
|
|
7834
|
+
const command = entry.commands.find((c) => c.name === name2);
|
|
7835
|
+
out.set(name2, { pkg: entry.pkg, command });
|
|
7836
|
+
}
|
|
7837
|
+
return out;
|
|
7838
|
+
}
|
|
7839
|
+
async function scanCommands(fs, modules, binsFrom = Number.POSITIVE_INFINITY) {
|
|
7840
|
+
const found = /* @__PURE__ */ new Map();
|
|
7841
|
+
const physical = physicalResolver(fs);
|
|
7842
|
+
const tiers = (typeof modules === "string" ? [modules] : modules).map(
|
|
7843
|
+
(tier) => typeof tier === "string" ? [tier] : tier
|
|
7844
|
+
);
|
|
7845
|
+
for (const [index, roots] of tiers.entries()) {
|
|
7846
|
+
const tier = await tierPackages(fs, physical, roots, index >= binsFrom);
|
|
7847
|
+
for (const [name2, { pkg, command }] of winners(tier)) {
|
|
7848
|
+
if (!found.has(name2)) found.set(name2, await withPackagePaths(fs, pkg, command));
|
|
7808
7849
|
}
|
|
7809
7850
|
}
|
|
7810
7851
|
if (!found.has("sh") && found.has("bash")) {
|
|
@@ -11178,14 +11219,6 @@ function attachSyncSabResponder(port, sab, token, opts) {
|
|
|
11178
11219
|
}
|
|
11179
11220
|
|
|
11180
11221
|
// src/kernel/cpu-clock.ts
|
|
11181
|
-
var IDLE_OPS = /* @__PURE__ */ new Set([
|
|
11182
|
-
"fd-select",
|
|
11183
|
-
"proc-wait",
|
|
11184
|
-
"proc-exec",
|
|
11185
|
-
"sig-pause",
|
|
11186
|
-
"sock-accept",
|
|
11187
|
-
"sock-connect"
|
|
11188
|
-
]);
|
|
11189
11222
|
var SYS_CAP_US = 2e4;
|
|
11190
11223
|
var LOCAL = SAB_CLOCK_OFFSET;
|
|
11191
11224
|
var BLOCKED = 0;
|
|
@@ -11217,9 +11250,16 @@ var CpuClock = class {
|
|
|
11217
11250
|
this.now = now;
|
|
11218
11251
|
this.start = now();
|
|
11219
11252
|
}
|
|
11220
|
-
enter(
|
|
11253
|
+
enter() {
|
|
11221
11254
|
this.since = this.now();
|
|
11222
|
-
this.kind = this.stopped
|
|
11255
|
+
this.kind = this.stopped ? "idle" : "sys";
|
|
11256
|
+
}
|
|
11257
|
+
block() {
|
|
11258
|
+
if (this.since === 0 || this.kind === "idle" || this.frozen !== void 0) return;
|
|
11259
|
+
const now = this.now();
|
|
11260
|
+
this.add(now - this.since);
|
|
11261
|
+
this.since = now;
|
|
11262
|
+
this.kind = "idle";
|
|
11223
11263
|
}
|
|
11224
11264
|
leave() {
|
|
11225
11265
|
if (this.since === 0) return;
|
|
@@ -11309,9 +11349,10 @@ var ProcessClocks = class {
|
|
|
11309
11349
|
}
|
|
11310
11350
|
timed(clock, dispatch2) {
|
|
11311
11351
|
const c = clock ?? this.main;
|
|
11352
|
+
const block = () => c.block();
|
|
11312
11353
|
return (req) => {
|
|
11313
|
-
c.enter(
|
|
11314
|
-
return dispatch2(req).finally(() => c.leave());
|
|
11354
|
+
c.enter();
|
|
11355
|
+
return dispatch2(req, block).finally(() => c.leave());
|
|
11315
11356
|
};
|
|
11316
11357
|
}
|
|
11317
11358
|
all() {
|
|
@@ -11570,7 +11611,7 @@ function mountTable(mounts = []) {
|
|
|
11570
11611
|
}
|
|
11571
11612
|
|
|
11572
11613
|
// src/version.ts
|
|
11573
|
-
var KERNEL_VERSION = true ? "1.
|
|
11614
|
+
var KERNEL_VERSION = true ? "1.94.0" : "0.0.0-dev";
|
|
11574
11615
|
var OS_RELEASE = "6.0.0-slicc";
|
|
11575
11616
|
function procVersion() {
|
|
11576
11617
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -14498,7 +14539,8 @@ var WasmProcess = class {
|
|
|
14498
14539
|
this.wake?.();
|
|
14499
14540
|
for (const listener of this.stateListeners) listener("continued", SIG.CONT);
|
|
14500
14541
|
}
|
|
14501
|
-
|
|
14542
|
+
blocks = /* @__PURE__ */ new WeakMap();
|
|
14543
|
+
async syscall(req, cancelled, block) {
|
|
14502
14544
|
this.options.onSyscall?.(req);
|
|
14503
14545
|
for (; ; ) {
|
|
14504
14546
|
await this.resumed;
|
|
@@ -14508,6 +14550,7 @@ var WasmProcess = class {
|
|
|
14508
14550
|
const forward = () => scope.abort();
|
|
14509
14551
|
if (cancelled?.aborted) scope.abort();
|
|
14510
14552
|
cancelled?.addEventListener("abort", forward, { once: true });
|
|
14553
|
+
if (block) this.blocks.set(scope.signal, block);
|
|
14511
14554
|
const result = await this.dispatch(req, scope.signal).finally(() => {
|
|
14512
14555
|
cancelled?.removeEventListener("abort", forward);
|
|
14513
14556
|
this.waiting.delete(scope.signal);
|
|
@@ -14590,6 +14633,7 @@ var WasmProcess = class {
|
|
|
14590
14633
|
}
|
|
14591
14634
|
if (this.options.hasPending?.() || cancelled.aborted) throw new KernelError("EINTR");
|
|
14592
14635
|
if (owing) this.waiting.add(cancelled);
|
|
14636
|
+
if (owing) this.blocks.get(cancelled)?.();
|
|
14593
14637
|
return this.cancelSignal(cancelled);
|
|
14594
14638
|
}
|
|
14595
14639
|
cancelSignal(cancelled) {
|
|
@@ -15231,6 +15275,7 @@ var WasmProcess = class {
|
|
|
15231
15275
|
case "proc-identity":
|
|
15232
15276
|
return { ok: true, kind: "json", json: await this.options.identity?.() ?? null };
|
|
15233
15277
|
case "proc-exec": {
|
|
15278
|
+
this.blocks.get(cancelled)?.();
|
|
15234
15279
|
this.adoptExec(req.pid);
|
|
15235
15280
|
this.options.jobs?.exec(this.pid, req.pid);
|
|
15236
15281
|
await this.fds.closeAll();
|
|
@@ -15561,6 +15606,9 @@ function killThread(thread, caught, sig) {
|
|
|
15561
15606
|
function unblocked(header3) {
|
|
15562
15607
|
return Atomics.load(header3, SAB_I_SIGNALS) & ~Atomics.load(header3, SAB_I_BLOCKED);
|
|
15563
15608
|
}
|
|
15609
|
+
function threadCall(process, req, cancelled, block) {
|
|
15610
|
+
return isWasmSyscall(req) ? process.syscall(req, cancelled, block) : dispatchSyncFs(req);
|
|
15611
|
+
}
|
|
15564
15612
|
function threadSignalsOf(sab) {
|
|
15565
15613
|
return { header: new Int32Array(sab, 0, SAB_HEADER_I32), interrupt: new AbortController() };
|
|
15566
15614
|
}
|
|
@@ -15764,7 +15812,9 @@ function spawnWasmProcess(opts) {
|
|
|
15764
15812
|
process.keptHandle = (handle2) => holds.keptFor(handle2)?.hidden;
|
|
15765
15813
|
const worker = opts.createWorker();
|
|
15766
15814
|
const dying = new Dying();
|
|
15767
|
-
const dispatch2 = (req) => dying.serving(
|
|
15815
|
+
const dispatch2 = (req, block) => dying.serving(
|
|
15816
|
+
() => isWasmSyscall(req) ? process.syscall(req, void 0, block) : dispatchSyncFs(req)
|
|
15817
|
+
);
|
|
15768
15818
|
const clocks = new ProcessClocks(sab, opts.program.abi !== "js", process);
|
|
15769
15819
|
const responder = attachSyncSabResponder(worker, sab, token, {
|
|
15770
15820
|
dispatch: clocks.timed(void 0, dispatch2)
|
|
@@ -15834,9 +15884,7 @@ function spawnWasmProcess(opts) {
|
|
|
15834
15884
|
const tsignals = threadSignalsOf(tsab);
|
|
15835
15885
|
if (thread.blocked) storeBlocked(tsignals.header, thread.blocked);
|
|
15836
15886
|
threadSignals.set(thread.tid, tsignals);
|
|
15837
|
-
const tdispatch = (req) => dying.serving(
|
|
15838
|
-
() => isWasmSyscall(req) ? process.syscall(req, threadCancel(tsignals)) : dispatchSyncFs(req)
|
|
15839
|
-
);
|
|
15887
|
+
const tdispatch = (req, block) => dying.serving(() => threadCall(process, req, threadCancel(tsignals), block));
|
|
15840
15888
|
const tresponder = attachSyncSabResponder(tw, tsab, token, {
|
|
15841
15889
|
dispatch: clocks.timed(clocks.thread(thread.tid, tsab), tdispatch)
|
|
15842
15890
|
});
|
|
@@ -19458,8 +19506,8 @@ var Launcher = class {
|
|
|
19458
19506
|
let found = this.catalogs.get(scope);
|
|
19459
19507
|
if (!found) {
|
|
19460
19508
|
found = this.trustedAppended().then(async (appended) => {
|
|
19461
|
-
const
|
|
19462
|
-
return scanCommands(this.scanned,
|
|
19509
|
+
const tiers = await this.tiers(scope, appended);
|
|
19510
|
+
return scanCommands(this.scanned, tiers, tiers.length - appended.length);
|
|
19463
19511
|
}).then((commands) => commands.set(SUDO_COMMAND.name, SUDO_COMMAND));
|
|
19464
19512
|
this.catalogs.set(scope, found);
|
|
19465
19513
|
}
|
|
@@ -19487,13 +19535,16 @@ var Launcher = class {
|
|
|
19487
19535
|
}
|
|
19488
19536
|
return found;
|
|
19489
19537
|
}
|
|
19490
|
-
async
|
|
19538
|
+
async tiers(scope = "", appended) {
|
|
19491
19539
|
const system = [
|
|
19492
|
-
this.modulesDir,
|
|
19493
|
-
|
|
19494
|
-
...appended ?? await this.trustedAppended()
|
|
19540
|
+
[this.modulesDir],
|
|
19541
|
+
await pnpmGlobalRoots(this.base, this.pnpmHome),
|
|
19542
|
+
...(appended ?? await this.trustedAppended()).map((root) => [root])
|
|
19495
19543
|
];
|
|
19496
|
-
return scope ? [
|
|
19544
|
+
return scope ? [await pnpmGlobalRoots(this.base, scope), ...system] : system;
|
|
19545
|
+
}
|
|
19546
|
+
async roots(scope = "") {
|
|
19547
|
+
return (await this.tiers(scope)).flat();
|
|
19497
19548
|
}
|
|
19498
19549
|
async rootOnlyPath(root) {
|
|
19499
19550
|
for (const at of chain(root)) {
|