@ai-ecoverse/slicc-kernel 1.47.3 → 1.48.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 +13 -3
- package/dist/kernel-worker.js +10 -3
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +170 -32
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +10 -3
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +170 -32
- package/dist/process-worker.js.map +4 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ A descriptor a program inherits at one of those numbers (from bash's `3<file`, o
|
|
|
186
186
|
On top of preview1 a WASI program gets:
|
|
187
187
|
|
|
188
188
|
- **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.
|
|
189
|
-
- **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`.
|
|
189
|
+
- **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), 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`.
|
|
190
190
|
- **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names.
|
|
191
191
|
- **Pipes** show to WASI and WASIX programs as stream sockets (`fstat` gives `S_IFSOCK`), because WASI has no FIFO file type and an unknown type made programs refuse them (Python's asyncio subprocess pipes). They are still read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. In a WASIX shell, `[ -p ]` is therefore false for a pipe and `[ -S ]` true. Emscripten programs, bash included, still see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output: WASI and WASIX programs see it as a stream socket too.
|
|
192
192
|
- **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream pair, `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.
|
|
@@ -226,8 +226,8 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
226
226
|
- `wait(pid)` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
|
|
227
227
|
- `net`: `listen({ port, host, backlog })` and `connect({ host, port, signal })` (`host` defaults to `127.0.0.1`) on the kernel's loopback, the same sockets WASIX programs use, so a JS server is reached by other processes, by the page as `<port>.kernel.localhost` (`loopbackFetch`, `dial`) and by attached clients. A listener has its `port` (as bound, so port `0` picks a free one), `accept({ signal })`, `close()` and `for await` over its connections. A connection has `local` and `remote` addresses, `readable` and `writable` web streams (closing the writable shuts down the sending side, so the peer reads the end), and `close()`. The loopback is IPv4 (`127.0.0.1` by default, or `0.0.0.0`), so an IPv6 host such as `::1` fails with `EADDRNOTAVAIL` for both `listen` and `connect`, and binding and connecting fail as they would for a WASIX program (`EADDRINUSE`, `ECONNREFUSED`, `EADDRNOTAVAIL`). A blocked accept, read or write waits without holding anything in the kernel, so aborting it or closing the socket leaves nothing behind. A write to a connection the peer closed fails with `EPIPE` and no `SIGPIPE`;
|
|
228
228
|
- `websocket(url, { protocols, headers, signal })`, a WebSocket over the kernel's transport (its `websocket`, so `fetchTransport({ webSocket: true })` in a browser), shaped like `WebSocketStream`. It resolves once the socket is open, with `url`, the `protocol` the far end chose (always one the program offered: a transport that hands back a socket with another fails the open with `EPROTO`, while browsers and Node's `WebSocket`, which `fetchTransport()` uses, fail such a handshake themselves without saying why, so the open fails with `ECONNREFUSED`), a `readable` of messages (`string` for text, `Uint8Array` for binary), a `writable` for sending them, `closed` (the `{ code, reason }` of the close, from either side, whether or not the program reads) and `close({ code, reason })`. An `http(s):` URL is taken as `ws(s):`. Sending waits while more than 1 MiB is buffered. A signal aborts the open, and after it the socket. A transport without WebSockets rejects with a `TypeError` whose `cause.code` is `ENOTSUP`, and one that cannot connect with `ECONNREFUSED`;
|
|
229
|
-
- `fetch(input, init)`, the web's `fetch` over the kernel's transport, the one the proxy uses, so the embedder's transport, its WebSocket and proxy settings and the kernel's routes apply. The response body streams as the program reads it. The request body is read whole first, since a transport takes it whole (streaming it is [#238](https://github.com/ai-ecoverse/slicc-kernel/issues/238)), and one larger than the transport's `maxRequestBody` is answered `413`, as through the proxy. A signal aborts the request at any stage. Redirects follow the fetch rules: at most 20, a 301 or 302 after a `POST` and a 303 turn into a `GET` without a body, `Authorization` is dropped across origins, and `redirect: 'manual'` returns the redirect itself. On a transport that follows redirects on its own (`fetchTransport()` in a browser) only `'follow'` works: `'manual'` and `'error'` reject with a `TypeError` whose `cause.code` is `ENOTSUP`, since such a transport never shows the program a redirect. A status the transport chose comes back as a response whose body is the reason. An unreachable host, or no transport at all, rejects with a `TypeError` whose `cause.code` is `ECONNREFUSED` or `ENETUNREACH`, as Node's `fetch` does. A `data:` URL is answered by `fetch` itself, as the fetch standard decodes it
|
|
230
|
-
- `tty`, for programs on a terminal: `getAttr(fd)` and `setAttr(fd, termios)` read and set its termios (`{ c_iflag, c_oflag, c_cflag, c_lflag, c_cc }`, Linux values), `setRaw(fd, raw)` switches raw mode on as libuv (and Node's `setRawMode`) does (no echo, no line editing, no `^C` signal, each key read as it comes) and off again to the termios it had before, `size(fd)` answers `{ columns, rows }`, and `foreground(fd)` and `setForeground(fd, pgrp)` get and set the foreground process group, under the same job-control rules as other programs. A window resize arrives as `SIGWINCH` through `signals`. A terminal the program left raw is set back when the program ends, whether it returns, exits or throws (a program killed by a signal cannot do so). On a descriptor that is not a terminal, each call fails with `ENOTTY`;
|
|
229
|
+
- `fetch(input, init)`, the web's `fetch` over the kernel's transport, the one the proxy uses, so the embedder's transport, its WebSocket and proxy settings and the kernel's routes apply. The response body streams as the program reads it. The request body is read whole first, since a transport takes it whole (streaming it is [#238](https://github.com/ai-ecoverse/slicc-kernel/issues/238)), and one larger than the transport's `maxRequestBody` is answered `413`, as through the proxy. A signal aborts the request at any stage. Redirects follow the fetch rules: at most 20, a 301 or 302 after a `POST` and a 303 turn into a `GET` without a body, `Authorization` is dropped across origins, and `redirect: 'manual'` returns the redirect itself. On a transport that follows redirects on its own (`fetchTransport()` in a browser) only `'follow'` works: `'manual'` and `'error'` reject with a `TypeError` whose `cause.code` is `ENOTSUP`, since such a transport never shows the program a redirect. A status the transport chose comes back as a response whose body is the reason. An unreachable host, or no transport at all, rejects with a `TypeError` whose `cause.code` is `ECONNREFUSED` or `ENETUNREACH`, as Node's `fetch` does. A `data:` URL is answered by `fetch` itself, as the fetch standard decodes it, without a transport: for any method, a `HEAD` without a body, and its type parsed and serialized as a MIME type (lowercase, no spaces, invalid parameters dropped), as Node's `fetch` answers; any other scheme than `http:` and `https:` rejects with a `TypeError` whose `cause.code` is `EPROTONOSUPPORT`, at the start as for a redirect target. The global `fetch` stays withheld;
|
|
230
|
+
- `tty`, for programs on a terminal: `getAttr(fd)` and `setAttr(fd, termios)` read and set its termios (`{ c_iflag, c_oflag, c_cflag, c_lflag, c_cc }`, Linux values), `setRaw(fd, raw)` switches raw mode on as libuv (and Node's `setRawMode`) does (no echo, no line editing, no `^C` signal, each key read as it comes) and off again to the termios it had before (per terminal: after raw mode on fds 0 and 1 of one terminal, turning it off on either restores the termios from before the first), `size(fd)` answers `{ columns, rows }`, and `foreground(fd)` and `setForeground(fd, pgrp)` get and set the foreground process group, under the same job-control rules as other programs. A window resize arrives as `SIGWINCH` through `signals`. A terminal the program left raw is set back when the program ends, whether it returns, exits or throws (a program killed by a signal cannot do so). On a descriptor that is not a terminal, each call fails with `ENOTTY`;
|
|
231
231
|
- `exit(status)`, which ends the program once its pending writes are done.
|
|
232
232
|
|
|
233
233
|
A program that throws, or leaves an error or a rejected promise unhandled, ends with status 1 and the message on stderr; one whose module cannot be loaded, or exports no function, ends with 126. A write to a pipe whose reader is gone ends the program with 141, unless it ignores or catches `SIGPIPE` (an ignore inherited from its parent counts), in which case the write fails with `EPIPE`. Errors carry a POSIX `code`. Reads and writes of pipes, terminals and sockets go through the kernel's asynchronous operations, so a program blocked reading stdin can still write and handle signals; other calls of one process are made one at a time. A JS program cannot `fork`, and threads it starts itself are not the kernel's.
|
|
@@ -460,6 +460,16 @@ WASI and WASIX have no call for file modes, so the kernel adds the import module
|
|
|
460
460
|
| `fd_mode(fd, mode_out)` | the permission bits of an open file or directory |
|
|
461
461
|
| `path_mode(dirfd, path, path_len, flags, mode_out)` | those of a path, of the link itself with `flags & 1` |
|
|
462
462
|
|
|
463
|
+
WASIX's own terminal calls carry only echo and line buffering (`tty_set`) and answer for "the" terminal whatever the descriptor (`tty_get`), so a WASIX program's `cfmakeraw` would leave `^C` a signal and its output post-processed. The kernel adds the import module `slicc_tty`, with the full Linux `struct termios` (musl's 60-byte layout: four `u32` flag words, `c_line`, `c_cc[32]`, then input and output speed; speeds read as `B38400` and are ignored when set) and answers per descriptor, again as a preview1 errno (`ENOTTY` for a descriptor that is no terminal, `EBADF` for one that isn't open, `ENOSYS` from kernels without them):
|
|
464
|
+
|
|
465
|
+
| Import | Does |
|
|
466
|
+
| --- | --- |
|
|
467
|
+
| `tcgetattr(fd, termios_out)` | writes the terminal's termios |
|
|
468
|
+
| `tcsetattr(fd, actions, termios)` | sets every flag and `c_cc` as given (unknown bits are kept and read back), at once for `TCSANOW`, `TCSADRAIN` and `TCSAFLUSH` (0 to 2; anything else is `EINVAL`); `TCSAFLUSH` also drops the input typed but not yet read, the pending line too, as an Emscripten program's `TCSETSF` does |
|
|
469
|
+
| `winsize(fd, winsize_out)` | writes `struct winsize` (`u16` rows, columns, then zero pixels) |
|
|
470
|
+
|
|
471
|
+
A wasix-sysroot that builds `tcgetattr`, `tcsetattr`, `isatty` and `ioctl(TCGETS, TCSETS*, TIOCGWINSZ)` on them gives WASIX programs (python's `tty.setraw`, curses, ruby's `IO#raw`) a really raw terminal. Stock `tty_get` answers `ENOTTY` when none of the process's descriptors is a terminal, instead of an 80×24 terminal that isn't there, so stdio on pipes and files isn't taken for a terminal.
|
|
472
|
+
|
|
463
473
|
Lowering a soft resource limit (`ulimit -S -n 64`) still fails with `EPERM` in Emscripten programs: their libc's `setrlimit` is a stub compiled into the program, not a call to the kernel.
|
|
464
474
|
|
|
465
475
|
### Users
|
package/dist/kernel-worker.js
CHANGED
|
@@ -8059,9 +8059,13 @@ var KernelTty = class {
|
|
|
8059
8059
|
tcgets() {
|
|
8060
8060
|
return { ...this.termios, c_cc: [...this.termios.c_cc] };
|
|
8061
8061
|
}
|
|
8062
|
-
tcsets(termios) {
|
|
8062
|
+
tcsets(termios, flush = false) {
|
|
8063
8063
|
const wasCanonical = this.canonical;
|
|
8064
8064
|
this.termios = { ...termios, c_cc: [...termios.c_cc] };
|
|
8065
|
+
if (flush) {
|
|
8066
|
+
this.line = [];
|
|
8067
|
+
this.readable = [];
|
|
8068
|
+
}
|
|
8065
8069
|
if (wasCanonical && !this.canonical && this.line.length > 0) {
|
|
8066
8070
|
this.readable.push(Uint8Array.from(this.line));
|
|
8067
8071
|
this.line = [];
|
|
@@ -9494,6 +9498,7 @@ var WasmProcess = class {
|
|
|
9494
9498
|
}
|
|
9495
9499
|
signal(sig) {
|
|
9496
9500
|
if (this.execChild !== void 0) {
|
|
9501
|
+
if (sig === SIG.CONT) this.cont();
|
|
9497
9502
|
void Promise.resolve(this.options.kill?.(this.execChild, sig)).catch(() => void 0);
|
|
9498
9503
|
if (sig !== SIG.KILL) return "forward";
|
|
9499
9504
|
if (!this.options.writesBack?.(this.execChild)) return "terminate";
|
|
@@ -9849,6 +9854,7 @@ var WasmProcess = class {
|
|
|
9849
9854
|
fdInfo(fd) {
|
|
9850
9855
|
const file = this.fds.get(fd).file;
|
|
9851
9856
|
const flags = this.fds.statusFlags(fd);
|
|
9857
|
+
const terminal = (file.tty ?? file.pty?.slave)?.name;
|
|
9852
9858
|
return {
|
|
9853
9859
|
tty: file.tty !== void 0,
|
|
9854
9860
|
kind: file instanceof KernelSocket ? "socket" : kernelFdKind(file),
|
|
@@ -9857,7 +9863,8 @@ var WasmProcess = class {
|
|
|
9857
9863
|
...this.fds.closesOnExec(fd) ? { cloexec: true } : {},
|
|
9858
9864
|
...file.read && !file.write ? { access: "read" } : {},
|
|
9859
9865
|
...file.write && !file.read ? { access: "write" } : {},
|
|
9860
|
-
...file.tty?.name ? { name: file.tty.name } : file.pty ? { name: "/dev/ptmx" } : {}
|
|
9866
|
+
...file.tty?.name ? { name: file.tty.name } : file.pty ? { name: "/dev/ptmx" } : {},
|
|
9867
|
+
...terminal ? { terminal } : {}
|
|
9861
9868
|
};
|
|
9862
9869
|
}
|
|
9863
9870
|
ttySyscall(req) {
|
|
@@ -9867,7 +9874,7 @@ var WasmProcess = class {
|
|
|
9867
9874
|
case "tty-get":
|
|
9868
9875
|
return { ok: true, kind: "json", json: tty.tcgets() };
|
|
9869
9876
|
case "tty-set":
|
|
9870
|
-
tty.tcsets(req.termios);
|
|
9877
|
+
tty.tcsets(req.termios, req.flush === true);
|
|
9871
9878
|
return { ok: true, kind: "void" };
|
|
9872
9879
|
case "tty-winsz":
|
|
9873
9880
|
return { ok: true, kind: "json", json: tty.winsize() };
|