@ai-ecoverse/slicc-kernel 1.100.4 → 1.102.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 +12 -2
- package/dist/kernel-worker.js +5 -13
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +63 -13
- package/dist/node-process-worker.js.map +3 -3
- package/dist/node.js +5 -13
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +63 -13
- package/dist/process-worker.js.map +3 -3
- package/dist/testing.js +63 -0
- package/dist/testing.js.map +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
|
|
|
76
76
|
| `pid` | the session leader's pid |
|
|
77
77
|
| `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
|
|
78
78
|
| `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; this applies only to children of a job-control shell (a process that ignores `SIGTTOU`, as interactive shells do) and to the programs those children exec, so a program that starts a child without job control (Python's `subprocess`) gets the key at once. Setting up means the child's calls before its first other one: `setpgid`, `tcsetpgrp`, signal masks, and starting the program it execs (which then sets itself up too). The hold ends 2 s after the child's first call at the latest. A key typed while the foreground group is busy, not reading the terminal (a shell between reading a command line and starting it), goes to that group and also to the job the group starts next, once that job has taken the terminal; the second copy is dropped when the group reads the terminal again. So `^C` or `^Z` right after a command line reaches the new job, as it does on Linux, where starting a job takes microseconds rather than a worker start. For the same reason, a child that is still setting up cannot give the terminal to its own group once its parent has already given the terminal to that group and taken it back: on Linux the child's `tcsetpgrp` would have landed first, so the parent's later choice stands. Otherwise `tcsetpgrp` behaves as before. A `^C`, `^\` or `^Z` that a program with a handler for it takes between two system calls, rather than while waiting, ends the program's next wait in the kernel that would block (reading nothing yet, `select` or `poll` with a timeout, a blocking write, `waitpid`) with `EINTR`, once, as if the key had come during that wait. A zero-timeout `select` or `poll` does not use it up. Without this, an interactive shell whose `^C` landed while it was taking in the key before it would wait for the next key before showing a new prompt: readline acts on `^C` only when a read ends with `EINTR`. On Linux that window is the microseconds between two reads; here it is a few kernel calls per key. A signal sent with `kill` does not do this. |
|
|
79
|
-
| `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group. As on Linux, setting the size the terminal already has, here or with a program's `TIOCSWINSZ`, sends no `SIGWINCH`. A program's `TIOCSWINSZ` that writes back the size it last read with `TIOCGWINSZ` is left out, so it can't undo a resize that came in between. Readline does that write-back every time it starts reading a line; on Linux the gap between the two calls is microseconds, here it's two kernel calls, wide enough for a resize to land in it and the shell to keep the old size (#740). |
|
|
79
|
+
| `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group. As on Linux, setting the size the terminal already has, here or with a program's `TIOCSWINSZ`, sends no `SIGWINCH`. A program's `TIOCSWINSZ` that writes back the size it last read with `TIOCGWINSZ` is left out, so it can't undo a resize that came in between. Readline does that write-back every time it starts reading a line; on Linux the gap between the two calls is microseconds, here it's two kernel calls, wide enough for a resize to land in it and the shell to keep the old size (#740). Limit: Linux has the same race and applies the stale write-back, only in a far shorter gap; leaving it out is a deliberate difference, so a program that really means to set the size it just read, after a resize it hasn't seen, keeps the newer size. |
|
|
80
80
|
| `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
|
|
81
81
|
| `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
|
|
82
82
|
| `ended` | resolves with `{ status, signal }` at the same time: `status` as `exited`, and `signal` named (`'SIGKILL'`) only when a signal ended it |
|
|
@@ -261,7 +261,7 @@ An empty path is `ENOENT` in every path call (`path_open`, `path_filestat_get`,
|
|
|
261
261
|
On top of preview1 a WASI program gets:
|
|
262
262
|
|
|
263
263
|
- **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.
|
|
264
|
-
- **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 again, since their data is already in the copied memory. 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`.
|
|
264
|
+
- **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 again, since their data is already in the copied memory. 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.
|
|
265
265
|
- **epoll and eventfd** (WASIX `epoll_create`, `epoll_ctl`, `epoll_wait` and `fd_event`, which tokio's reactor uses through the WASIX mio fork):
|
|
266
266
|
- **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.
|
|
267
267
|
- **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`).
|
|
@@ -480,6 +480,8 @@ In the browser, the page serves the uplink to the kernel worker over the same po
|
|
|
480
480
|
- `reset(code)` (default `ECONNRESET`) breaks the connection both ways: writes the peer has not read fail, the program's next read fails with `ECONNRESET`, and its writes with `EPIPE`.
|
|
481
481
|
- `closed` resolves when the kernel closes the connection.
|
|
482
482
|
|
|
483
|
+
`fakeCronClock(start?, utc?)` is a clock for the kernel's cron to run on in tests, so a test moves time instead of waiting for it. It starts at `start` (default 2026-01-01T00:00Z) and reads schedules in UTC unless `utc` is `false`. `now()` is the fake time. `sleep(until, signal)` resolves when `advance(ms)` or `set(ms)` moves time to `until` or past it, or when `signal` aborts. `sleepers` counts the sleeps still waiting. `flush()` resolves once every sleeper that a time move woke has gone back to sleep for a later time, which is the point where a scheduler has done what was due. It never resolves while nothing sleeps.
|
|
484
|
+
|
|
483
485
|
### Browser automation (CDP)
|
|
484
486
|
|
|
485
487
|
Programs drive a browser with the Chrome DevTools Protocol over a WebSocket, whichever host actually has the browser: slicc-extension's `chrome.debugger`, slicc-node's or slicc-swift's CDP proxy, or a test harness. The kernel relays; it does not launch a browser.
|
|
@@ -1008,6 +1010,14 @@ Two builds of one C program (`forktls.c` with `tlsvars.c`), a position-independe
|
|
|
1008
1010
|
|
|
1009
1011
|
**Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with homescoop's pinned `wasixcc` 0.4.7 and binaryen's `wasm-opt` 133 on `PATH` (the same toolchain rebuilds `bin/piefork.wasm` byte for byte). `bin/forktls-pie.wasm` is 816,066 bytes, sha256 `c5ed6b06036f4d3c42c660fc13ca1a995d647f11021829f2169567574254fd9e`; `bin/forktls-static.wasm` is 45,564 bytes, sha256 `5df019d5f2a4432132c8b7c6f5992d18ecf9d8e4c4be21351acd59c46c3ca48d`; `lib/libtlsside.so` is 736 bytes, sha256 `ecd32e561c051576a99e87caee5e1aca5238c4055e2e551065fa10fe7476d0b5`.
|
|
1010
1012
|
|
|
1013
|
+
### `threadjmp`
|
|
1014
|
+
|
|
1015
|
+
A threaded WASIX C program for `test/integration/thread-asyncify.test.mjs` (#710). A thread longjmps 50 times through a 40-frame recursion and forks. Its child forks again and reports its own child's exit status. Meanwhile the main thread keeps recursing 200 frames deep and checks its frames afterwards (`main damaged`).
|
|
1016
|
+
- `bin/threadjmp.wasm`: built without exceptions (`WASIXCC_WASM_EXCEPTIONS=no`), so `setjmp` goes through Asyncify's `stack_checkpoint`/`stack_restore`. wasixcc runs Asyncify itself for that build.
|
|
1017
|
+
- `bin/threadjmp-pie.wasm`: position-independent (`WASIXCC_PIC=yes`, legacy exceptions), with Asyncify for `proc_fork` only. It's built with `-DFORK_ONLY`, because Asyncify can't flatten a function that uses exceptions for `setjmp`.
|
|
1018
|
+
|
|
1019
|
+
**Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with homescoop's pinned `wasixcc` 0.4.7 and binaryen's `wasm-opt` on `PATH`. `bin/threadjmp.wasm` is 76,125 bytes, sha256 `351fe15874125b1115f555ea406af9a6b27700bb8a51eb599baf7529ef631b20`; `bin/threadjmp-pie.wasm` is 815,633 bytes, sha256 `9179896d805b005b8d9569286b6e233ee96f718ae3630708c859be3eee5c1c08`.
|
|
1020
|
+
|
|
1011
1021
|
### `stopwait`
|
|
1012
1022
|
|
|
1013
1023
|
A WASIX C program for `test/integration/stop-restart.test.mjs` (#738). `stopwait CALL` forks a child that waits 300 ms in `CALL` (`epoll`, `poll`, `ppoll`, `select`, `pselect`, `nanosleep` or `clock_nanosleep`) on a pipe that never gets data. The parent stops the child 50 ms after it's ready and continues it 200 ms later. The child prints the result, the error and whether the wait ended before the deadline, at it (300–450 ms) or late.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -12114,7 +12114,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
|
|
|
12114
12114
|
var EARLY_WHOLE_FILE = 64 * 1024;
|
|
12115
12115
|
|
|
12116
12116
|
// src/version.ts
|
|
12117
|
-
var KERNEL_VERSION = true ? "1.
|
|
12117
|
+
var KERNEL_VERSION = true ? "1.102.0" : "0.0.0-dev";
|
|
12118
12118
|
var OS_RELEASE = "6.0.0-slicc";
|
|
12119
12119
|
function procVersion() {
|
|
12120
12120
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -14595,13 +14595,6 @@ var SEM_NAMES_MAX = 4096;
|
|
|
14595
14595
|
var SEM_HANDLES_MAX = 256;
|
|
14596
14596
|
var NAME_MAX = 251;
|
|
14597
14597
|
var TIMER_MAX_MS = 2147483647;
|
|
14598
|
-
var deadlines = /* @__PURE__ */ new WeakMap();
|
|
14599
|
-
function timeLeft(req, timeoutMs) {
|
|
14600
|
-
const now = performance.now();
|
|
14601
|
-
const deadline = deadlines.get(req) ?? now + timeoutMs;
|
|
14602
|
-
deadlines.set(req, deadline);
|
|
14603
|
-
return Math.max(0, deadline - now);
|
|
14604
|
-
}
|
|
14605
14598
|
var SEM_OPS = [
|
|
14606
14599
|
"sem-open",
|
|
14607
14600
|
"sem-close",
|
|
@@ -14788,9 +14781,8 @@ async function semSyscall(req, proc) {
|
|
|
14788
14781
|
const sem = handles.get(req.handle);
|
|
14789
14782
|
if (sem.take()) return ok2();
|
|
14790
14783
|
if (req.how === "try") throw new KernelError("EAGAIN");
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
const got = await sem.wait(proc.blocking(), timeoutMs);
|
|
14784
|
+
if (req.how === "timed" && req.timeoutMs === 0) throw new KernelError("ETIMEDOUT");
|
|
14785
|
+
const got = await sem.wait(proc.blocking(), req.how === "block" ? -1 : req.timeoutMs);
|
|
14794
14786
|
if (!got) throw new KernelError("ETIMEDOUT");
|
|
14795
14787
|
return ok2();
|
|
14796
14788
|
}
|
|
@@ -15813,7 +15805,7 @@ function identityKey(st, path) {
|
|
|
15813
15805
|
}
|
|
15814
15806
|
var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
|
|
15815
15807
|
var HOSTS_ONLY = new Resolver({ routes: new Routes() });
|
|
15816
|
-
function
|
|
15808
|
+
function timeLeft(req, started) {
|
|
15817
15809
|
if (!("timeoutMs" in req) || typeof req.timeoutMs !== "number" || req.timeoutMs < 0) return req;
|
|
15818
15810
|
return { ...req, timeoutMs: Math.max(0, req.timeoutMs - (performance.now() - started)) };
|
|
15819
15811
|
}
|
|
@@ -15983,7 +15975,7 @@ var WasmProcess = class {
|
|
|
15983
15975
|
for (; ; ) {
|
|
15984
15976
|
await this.resumed;
|
|
15985
15977
|
if (this.dying) return DYING;
|
|
15986
|
-
const current = restarted ?
|
|
15978
|
+
const current = restarted ? timeLeft(req, started) : req;
|
|
15987
15979
|
const stops = this.stops;
|
|
15988
15980
|
const scope = new AbortController();
|
|
15989
15981
|
const forward = () => scope.abort();
|