@ai-ecoverse/slicc-kernel 1.29.2 → 1.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -4
- package/dist/kernel-worker.js +80 -9
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +37 -21
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +80 -9
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +37 -21
- package/dist/process-worker.js.map +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
|
|
|
64
64
|
| --- | --- |
|
|
65
65
|
| `pid` | the session leader's pid |
|
|
66
66
|
| `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
|
|
67
|
-
| `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group |
|
|
67
|
+
| `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group. A key typed while a child of that group is still setting up is held until the child has finished, then sent to the group in the foreground at that point. Setting up means the child's calls before its first other one: `setpgid`, `tcsetpgrp` and signal masks. The hold ends 250 ms after the child's first call at the latest. So `^Z` right after a command line stops the new job, as it does on Linux, where that setup takes microseconds rather than a worker start. |
|
|
68
68
|
| `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
|
|
69
69
|
| `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
|
|
70
70
|
| `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
|
|
@@ -140,15 +140,38 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
|
|
|
140
140
|
{ "slicc": { "abi": "wasi", "commands": { "rg": { "wasm": "bin/rg.wasm" }, "rustc": { "script": "bin/rustc" } } } }
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. 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 first package to define a name wins. `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 `PNPM_HOME=/home/.local/share/pnpm`, unless the embedder sets its own, and has `$PNPM_HOME/bin` on `PATH`. 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.
|
|
143
|
+
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 first package to define a name wins. `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 `PNPM_HOME=/home/.local/share/pnpm`, unless the embedder sets its own, and has `$PNPM_HOME/bin` on `PATH`. 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.
|
|
144
144
|
|
|
145
145
|
### WASI programs
|
|
146
146
|
|
|
147
|
-
A WASI program runs in a process worker of its own like an Emscripten one, against the same kernel: its descriptors, pipes, terminals and job control are the kernel's, and its files are OPFS. Preopens
|
|
147
|
+
A WASI program runs in a process worker of its own like an Emscripten one, against the same kernel: its descriptors, pipes, terminals and job control are the kernel's, and its files are OPFS. **Preopens**, in order:
|
|
148
|
+
1. `.`, the cwd, as fd 3, which Zig's std takes for its cwd;
|
|
149
|
+
2. `/dev` and every top-level directory, plus the virtual `/usr` and `/bin`, sorted by name.
|
|
150
|
+
|
|
151
|
+
Every WASI program sees `/` through the cwd.
|
|
152
|
+
|
|
153
|
+
**`preopenRoot`.** A command whose manifest entry sets `"preopenRoot": true` gets two more preopens after those:
|
|
154
|
+
3. `/`, the root;
|
|
155
|
+
4. `./`, the cwd again.
|
|
156
|
+
|
|
157
|
+
That's for programs that walk to the root, such as Go's (`GOOS=wasip1`) and esbuild's resolver. The order serves each runtime:
|
|
158
|
+
- **Go** takes its working directory from `PWD`, which every process gets, and resolves absolute paths against the longest matching preopen name. So `os.ReadDir("/")`, `filepath.Abs("../..")` and walking up to the root work. Without `preopenRoot`, nothing is named `/` and they fail with `EBADF`.
|
|
159
|
+
- **wasi-libc** (C, and Rust's `wasm32-wasip1`) reads `.`, `/` and `./` as the same empty prefix, and the last one wins. So the trailing `./` keeps relative paths on the cwd.
|
|
160
|
+
- **Zig** keeps fd 3, and a second `.` would stop it from starting.
|
|
161
|
+
|
|
162
|
+
**Working directory.** wasi-libc's own `getcwd()` stays `/` (Rust's `current_dir()`, `canonicalize(".")`) in either layout, because the C library starts with a working directory of `/` and no runtime can change it. A wasi-libc program that needs its real directory takes it from `PWD` (`set_current_dir($PWD)` at start); relative paths are the cwd either way. WASIX programs ask the kernel for their cwd and get the real one.
|
|
163
|
+
|
|
164
|
+
**Descriptor numbers.** Preopens take consecutive descriptors from 3, since WASI libraries look for them until the first gap:
|
|
165
|
+
- without `preopenRoot`: fds 3 through 3 + *n*, *n* being the number of top-level directories, `/dev`, `/usr` and `/bin` together;
|
|
166
|
+
- with it: two more, through 3 + *n* + 2.
|
|
167
|
+
|
|
168
|
+
A descriptor a program inherits at one of those numbers (from bash's `3<file`, or a `posix_spawn` file action) moves to the first free number above them. One above the range keeps its number. A command that takes descriptors by number from its caller should leave `preopenRoot` off, which keeps the shorter range.
|
|
169
|
+
|
|
170
|
+
On top of preview1 a WASI program gets:
|
|
148
171
|
|
|
149
172
|
- **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; `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.
|
|
150
173
|
- **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid`, 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`.
|
|
151
|
-
- **Sockets**: the WASIX socket calls (`sock_open`, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers
|
|
174
|
+
- **Sockets**: the WASIX socket calls (`sock_open`, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names.
|
|
152
175
|
- **Diagnostics**: a trap ends the program with 134 and its message on stderr; with `SLICC_WASM_BACKTRACE=1` the wasm frames follow, named from the module's name section, or from a sidecar for a module shipped without one (`<module>.names` beside it, or the same path in a `<package>-names` package). `SLICC_WASI_STATS=1` makes a program print its calls, counted and timed, as it ends.
|
|
153
176
|
|
|
154
177
|
### Program imports
|
package/dist/kernel-worker.js
CHANGED
|
@@ -4550,6 +4550,7 @@ function commandOf(pkg, name2, raw, packageAbi, shared) {
|
|
|
4550
4550
|
wasm,
|
|
4551
4551
|
argv0,
|
|
4552
4552
|
...raw.argv0Path === true ? { argv0Path: true } : {},
|
|
4553
|
+
...abi === "wasi" && raw.preopenRoot === true ? { preopenRoot: true } : {},
|
|
4553
4554
|
...args ? { args } : {},
|
|
4554
4555
|
...withEnv,
|
|
4555
4556
|
...imports ? { imports } : {}
|
|
@@ -7875,7 +7876,7 @@ var WasmProcess = class {
|
|
|
7875
7876
|
for (const listener of this.stateListeners) listener("continued", SIG.CONT);
|
|
7876
7877
|
}
|
|
7877
7878
|
async syscall(req) {
|
|
7878
|
-
this.options.onSyscall?.();
|
|
7879
|
+
this.options.onSyscall?.(req);
|
|
7879
7880
|
for (; ; ) {
|
|
7880
7881
|
await this.resumed;
|
|
7881
7882
|
const stops = this.stops;
|
|
@@ -7999,9 +8000,10 @@ var WasmProcess = class {
|
|
|
7999
8000
|
json: this.fds.numbers().map((fd) => ({ fd, ...this.fdInfo(fd) }))
|
|
8000
8001
|
};
|
|
8001
8002
|
case "fd-meta": {
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8003
|
+
if (!this.fds.get(req.fd).file.held) throw new KernelError("EBADF");
|
|
8004
|
+
const cloexec = this.fds.closesOnExec(req.fd);
|
|
8005
|
+
this.fds.installAt(req.fd, heldFile(req.meta));
|
|
8006
|
+
if (cloexec) this.fds.setCloseOnExec(req.fd);
|
|
8005
8007
|
return { ok: true, kind: "void" };
|
|
8006
8008
|
}
|
|
8007
8009
|
case "fd-setfl":
|
|
@@ -8668,6 +8670,57 @@ var JobTable = class {
|
|
|
8668
8670
|
}
|
|
8669
8671
|
};
|
|
8670
8672
|
|
|
8673
|
+
// src/kernel/settling.ts
|
|
8674
|
+
var SETUP_OPS = /* @__PURE__ */ new Set([
|
|
8675
|
+
"proc-list",
|
|
8676
|
+
"proc-identity",
|
|
8677
|
+
"fd-info",
|
|
8678
|
+
"sig-mask",
|
|
8679
|
+
"proc-setpgid",
|
|
8680
|
+
"tty-pgrp-set"
|
|
8681
|
+
]);
|
|
8682
|
+
var SETTLE_MS = 250;
|
|
8683
|
+
var KEY_SIGNALS = /* @__PURE__ */ new Set([2, 3, 20]);
|
|
8684
|
+
var SettlingChildren = class {
|
|
8685
|
+
children = /* @__PURE__ */ new Map();
|
|
8686
|
+
held = [];
|
|
8687
|
+
pgidOf;
|
|
8688
|
+
settleMs;
|
|
8689
|
+
constructor(pgidOf, settleMs = SETTLE_MS) {
|
|
8690
|
+
this.pgidOf = pgidOf;
|
|
8691
|
+
this.settleMs = settleMs;
|
|
8692
|
+
}
|
|
8693
|
+
started(pid, ppid) {
|
|
8694
|
+
this.children.set(pid, { ppid });
|
|
8695
|
+
}
|
|
8696
|
+
syscall(pid, op) {
|
|
8697
|
+
const child = this.children.get(pid);
|
|
8698
|
+
if (!child) return;
|
|
8699
|
+
if (!SETUP_OPS.has(op)) {
|
|
8700
|
+
this.settled(pid);
|
|
8701
|
+
return;
|
|
8702
|
+
}
|
|
8703
|
+
child.timer ??= setTimeout(() => this.settled(pid), this.settleMs);
|
|
8704
|
+
}
|
|
8705
|
+
settled(pid) {
|
|
8706
|
+
const child = this.children.get(pid);
|
|
8707
|
+
if (!child) return;
|
|
8708
|
+
clearTimeout(child.timer);
|
|
8709
|
+
this.children.delete(pid);
|
|
8710
|
+
const ready2 = this.held.filter((h) => !this.blocking(h.pgid));
|
|
8711
|
+
this.held = this.held.filter((h) => this.blocking(h.pgid));
|
|
8712
|
+
for (const h of ready2) h.send();
|
|
8713
|
+
}
|
|
8714
|
+
deliver(sig, pgid, send) {
|
|
8715
|
+
if (KEY_SIGNALS.has(sig) && this.blocking(pgid)) this.held.push({ pgid, send });
|
|
8716
|
+
else send();
|
|
8717
|
+
}
|
|
8718
|
+
blocking(pgid) {
|
|
8719
|
+
for (const { ppid } of this.children.values()) if (this.pgidOf(ppid) === pgid) return true;
|
|
8720
|
+
return false;
|
|
8721
|
+
}
|
|
8722
|
+
};
|
|
8723
|
+
|
|
8671
8724
|
// src/mount/driver.ts
|
|
8672
8725
|
function fsError2(code, message2 = code) {
|
|
8673
8726
|
return Object.assign(new Error(message2), { code });
|
|
@@ -9893,7 +9946,8 @@ function targetOf(command) {
|
|
|
9893
9946
|
...command.args ? { prefix: command.args } : {},
|
|
9894
9947
|
...command.env ? { env: command.env } : {},
|
|
9895
9948
|
...command.unset ? { unset: command.unset } : {},
|
|
9896
|
-
...command.imports ? { imports: command.imports } : {}
|
|
9949
|
+
...command.imports ? { imports: command.imports } : {},
|
|
9950
|
+
...command.preopenRoot ? { preopenRoot: true } : {}
|
|
9897
9951
|
};
|
|
9898
9952
|
}
|
|
9899
9953
|
function childHandle(handle) {
|
|
@@ -9927,7 +9981,14 @@ var Launcher = class {
|
|
|
9927
9981
|
described = /* @__PURE__ */ new Map();
|
|
9928
9982
|
jobs = new JobTable();
|
|
9929
9983
|
locks = new LockTable();
|
|
9930
|
-
|
|
9984
|
+
settling = new SettlingChildren((pid) => this.jobs.pgidOf(pid));
|
|
9985
|
+
ptys = new PtyTable(
|
|
9986
|
+
(tty, sig) => this.settling.deliver(
|
|
9987
|
+
sig,
|
|
9988
|
+
this.jobs.tcgetpgrp(tty, 0),
|
|
9989
|
+
() => this.jobs.signalOwnedForeground(tty, sig)
|
|
9990
|
+
)
|
|
9991
|
+
);
|
|
9931
9992
|
net = new LoopbackNet();
|
|
9932
9993
|
cdp;
|
|
9933
9994
|
ca;
|
|
@@ -10129,7 +10190,8 @@ var Launcher = class {
|
|
|
10129
10190
|
...memory ? { memory } : {},
|
|
10130
10191
|
foreign,
|
|
10131
10192
|
...names ? { names } : {},
|
|
10132
|
-
...imports !== void 0 ? { imports } : {}
|
|
10193
|
+
...imports !== void 0 ? { imports } : {},
|
|
10194
|
+
...target.preopenRoot ? { preopenRoot: true } : {}
|
|
10133
10195
|
};
|
|
10134
10196
|
}
|
|
10135
10197
|
async launch(planned, req) {
|
|
@@ -10166,7 +10228,10 @@ var Launcher = class {
|
|
|
10166
10228
|
await (asked ??= decided.then(count));
|
|
10167
10229
|
return this.identityOf(pid);
|
|
10168
10230
|
},
|
|
10169
|
-
onSyscall: () =>
|
|
10231
|
+
onSyscall: (call) => {
|
|
10232
|
+
this.decide(pid);
|
|
10233
|
+
this.settling.syscall(pid, call.op);
|
|
10234
|
+
},
|
|
10170
10235
|
...req.ignored ? { ignored: req.ignored } : {},
|
|
10171
10236
|
program: req.program,
|
|
10172
10237
|
argv0: req.argv0,
|
|
@@ -10199,6 +10264,7 @@ var Launcher = class {
|
|
|
10199
10264
|
...this.shownIds(req)
|
|
10200
10265
|
});
|
|
10201
10266
|
this.processes.set(pid, handle);
|
|
10267
|
+
if (req.ppid !== void 0) this.settling.started(pid, req.ppid);
|
|
10202
10268
|
this.jobs.add(pid, req.ppid, (sig) => handle.signal(sig), terminal);
|
|
10203
10269
|
if (req.exec && req.ppid !== void 0) this.jobs.exec(req.ppid, pid, true);
|
|
10204
10270
|
if (req.pgid !== void 0) this.jobs.join(pid, req.pgid);
|
|
@@ -10209,6 +10275,7 @@ var Launcher = class {
|
|
|
10209
10275
|
});
|
|
10210
10276
|
void handle.exited.then(() => {
|
|
10211
10277
|
this.processes.delete(pid);
|
|
10278
|
+
this.settling.settled(pid);
|
|
10212
10279
|
if (req.ppid === void 0 || this.orphans.delete(pid)) this.forget(pid);
|
|
10213
10280
|
else this.zombies.add(pid);
|
|
10214
10281
|
});
|
|
@@ -10521,7 +10588,11 @@ var Launcher = class {
|
|
|
10521
10588
|
let leader = 0;
|
|
10522
10589
|
const tty = new KernelTty(
|
|
10523
10590
|
{ write: (bytes) => options.onData(bytes.slice()) },
|
|
10524
|
-
(sig) => this.
|
|
10591
|
+
(sig) => this.settling.deliver(
|
|
10592
|
+
sig,
|
|
10593
|
+
this.jobs.tcgetpgrp(tty, leader),
|
|
10594
|
+
() => this.jobs.signalForeground(tty, leader, sig)
|
|
10595
|
+
)
|
|
10525
10596
|
);
|
|
10526
10597
|
tty.name = `/dev/tty${++this.terminals}`;
|
|
10527
10598
|
tty.setSize(options.cols ?? 80, options.rows ?? 24);
|