@ai-ecoverse/slicc-kernel 1.47.1 → 1.47.3
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 +3 -2
- package/dist/kernel-worker.js +31 -3
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +63 -2
- package/dist/node-process-worker.js.map +3 -3
- package/dist/node.js +31 -3
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +63 -2
- package/dist/process-worker.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
|
|
|
66
66
|
| --- | --- |
|
|
67
67
|
| `pid` | the session leader's pid |
|
|
68
68
|
| `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
|
|
69
|
-
| `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`, 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. |
|
|
69
|
+
| `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`, 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. |
|
|
70
70
|
| `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
|
|
71
71
|
| `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
|
|
72
72
|
| `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
|
|
@@ -188,6 +188,7 @@ On top of preview1 a WASI program gets:
|
|
|
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
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`.
|
|
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
|
+
- **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.
|
|
191
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.
|
|
192
193
|
- **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.
|
|
193
194
|
|
|
@@ -225,7 +226,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
225
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);
|
|
226
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`;
|
|
227
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`;
|
|
228
|
-
- `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. The global `fetch` stays withheld;
|
|
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 (a `GET` only, as in Node), without a transport; 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;
|
|
229
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`;
|
|
230
231
|
- `exit(status)`, which ends the program once its pending writes are done.
|
|
231
232
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -8131,9 +8131,9 @@ var KernelTty = class {
|
|
|
8131
8131
|
this.line = [];
|
|
8132
8132
|
} else if (byte === 10) {
|
|
8133
8133
|
this.line.push(byte);
|
|
8134
|
-
if (echo) this.output(encoder2.encode("\n"));
|
|
8135
8134
|
this.readable.push(Uint8Array.from(this.line));
|
|
8136
8135
|
this.line = [];
|
|
8136
|
+
if (echo) this.output(encoder2.encode("\n"));
|
|
8137
8137
|
} else {
|
|
8138
8138
|
this.line.push(byte);
|
|
8139
8139
|
if (echo) this.echo(byte);
|
|
@@ -9874,7 +9874,7 @@ var WasmProcess = class {
|
|
|
9874
9874
|
case "tty-pgrp-get":
|
|
9875
9875
|
return { ok: true, kind: "json", json: jobs ? jobs.tcgetpgrp(tty, this.sid()) : this.pid };
|
|
9876
9876
|
case "tty-pgrp-set":
|
|
9877
|
-
if (jobs) jobs.tcsetpgrp(this.pid, tty, req.pgrp);
|
|
9877
|
+
if (jobs) jobs.tcsetpgrp(this.pid, tty, req.pgrp, this.options.settling?.() ?? false);
|
|
9878
9878
|
else if (req.pgrp !== this.pid) throw new KernelError("EPERM");
|
|
9879
9879
|
return { ok: true, kind: "void" };
|
|
9880
9880
|
}
|
|
@@ -10259,6 +10259,7 @@ function processOptions(opts, header2, hooks) {
|
|
|
10259
10259
|
...inherited(opts),
|
|
10260
10260
|
...opts.identity ? { identity: opts.identity } : {},
|
|
10261
10261
|
...opts.onSyscall ? { onSyscall: opts.onSyscall } : {},
|
|
10262
|
+
...opts.settling ? { settling: opts.settling } : {},
|
|
10262
10263
|
spawner: opts.spawner,
|
|
10263
10264
|
forker: opts.forker,
|
|
10264
10265
|
fs: opts.fs,
|
|
@@ -10431,6 +10432,7 @@ function spawnWasmProcess(opts) {
|
|
|
10431
10432
|
// src/kernel/jobs.ts
|
|
10432
10433
|
var JobTable = class {
|
|
10433
10434
|
members = /* @__PURE__ */ new Map();
|
|
10435
|
+
handoffs = /* @__PURE__ */ new Map();
|
|
10434
10436
|
foreground = /* @__PURE__ */ new Map();
|
|
10435
10437
|
terminals = /* @__PURE__ */ new Map();
|
|
10436
10438
|
add(pid, parentPid, signal, terminal) {
|
|
@@ -10450,7 +10452,12 @@ var JobTable = class {
|
|
|
10450
10452
|
return [...this.members.values()];
|
|
10451
10453
|
}
|
|
10452
10454
|
remove(pid) {
|
|
10455
|
+
const member = this.members.get(pid);
|
|
10453
10456
|
this.members.delete(pid);
|
|
10457
|
+
if (member) this.prune(member.pgid);
|
|
10458
|
+
}
|
|
10459
|
+
prune(pgid) {
|
|
10460
|
+
if (![...this.members.values()].some((m) => m.pgid === pgid)) this.handoffs.delete(pgid);
|
|
10454
10461
|
}
|
|
10455
10462
|
exec(pid, child, adopt = false) {
|
|
10456
10463
|
const member = this.members.get(child);
|
|
@@ -10514,7 +10521,12 @@ var JobTable = class {
|
|
|
10514
10521
|
if (id2 === target.sid) throw new KernelError("EPERM");
|
|
10515
10522
|
const exists = [...this.members.values()].some((m) => m.pgid === group && m.sid === target.sid);
|
|
10516
10523
|
if (group !== id2 && !exists) throw new KernelError("EPERM");
|
|
10524
|
+
const left = target.pgid;
|
|
10517
10525
|
target.pgid = group;
|
|
10526
|
+
this.prune(left);
|
|
10527
|
+
}
|
|
10528
|
+
get handoffCount() {
|
|
10529
|
+
return this.handoffs.size;
|
|
10518
10530
|
}
|
|
10519
10531
|
getpgid(caller, pid) {
|
|
10520
10532
|
return this.byId(pid || this.idOf(caller)).pgid;
|
|
@@ -10567,12 +10579,24 @@ var JobTable = class {
|
|
|
10567
10579
|
tcgetpgrp(tty, fallback) {
|
|
10568
10580
|
return this.foreground.get(tty) ?? fallback;
|
|
10569
10581
|
}
|
|
10570
|
-
tcsetpgrp(caller, tty, pgid) {
|
|
10582
|
+
tcsetpgrp(caller, tty, pgid, settling = false) {
|
|
10571
10583
|
const self = this.member(caller);
|
|
10572
10584
|
const inSession = [...this.members.values()].some((m) => m.pgid === pgid && m.sid === self.sid);
|
|
10573
10585
|
if (!inSession) throw new KernelError("EPERM");
|
|
10586
|
+
const current = this.foreground.get(tty);
|
|
10587
|
+
if (settling && pgid === self.pgid && current !== pgid && this.reclaimed(pgid, self.ppid))
|
|
10588
|
+
return;
|
|
10589
|
+
if (pgid !== self.pgid) this.handoffs.set(pgid, { by: caller, reclaimed: false });
|
|
10590
|
+
else if (current !== void 0) {
|
|
10591
|
+
const handoff = this.handoffs.get(current);
|
|
10592
|
+
if (handoff?.by === caller) handoff.reclaimed = true;
|
|
10593
|
+
}
|
|
10574
10594
|
this.foreground.set(tty, pgid);
|
|
10575
10595
|
}
|
|
10596
|
+
reclaimed(pgid, parent) {
|
|
10597
|
+
const handoff = this.handoffs.get(pgid);
|
|
10598
|
+
return handoff?.reclaimed === true && handoff.by === parent;
|
|
10599
|
+
}
|
|
10576
10600
|
signalOwnedForeground(tty, sig) {
|
|
10577
10601
|
const pgid = this.foreground.get(tty);
|
|
10578
10602
|
if (pgid !== void 0) this.killGroup(pgid, sig);
|
|
@@ -10616,6 +10640,9 @@ var SettlingChildren = class {
|
|
|
10616
10640
|
this.parked = this.parked.filter((p) => p.pgid !== pgid);
|
|
10617
10641
|
this.held = this.held.filter((h) => !(h.copy && h.pgid === pgid));
|
|
10618
10642
|
}
|
|
10643
|
+
isSettling(pid) {
|
|
10644
|
+
return this.children.has(pid);
|
|
10645
|
+
}
|
|
10619
10646
|
syscall(pid, op) {
|
|
10620
10647
|
const child = this.children.get(pid);
|
|
10621
10648
|
if (!child) return;
|
|
@@ -12222,6 +12249,7 @@ var Launcher = class {
|
|
|
12222
12249
|
this.decide(pid);
|
|
12223
12250
|
this.settling.syscall(pid, call.op);
|
|
12224
12251
|
},
|
|
12252
|
+
settling: () => this.settling.isSettling(pid),
|
|
12225
12253
|
...req.ignored ? { ignored: req.ignored } : {},
|
|
12226
12254
|
...req.umask !== void 0 ? { umask: req.umask } : {},
|
|
12227
12255
|
cred: req.cred,
|