@ai-ecoverse/slicc-kernel 1.125.10 → 1.125.11
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 -3
- package/dist/kernel-worker.js +8 -4
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +76 -15
- package/dist/node-process-worker.js.map +3 -3
- package/dist/node.js +8 -4
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +76 -15
- package/dist/process-worker.js.map +3 -3
- package/dist/types/kernel/process.d.ts +2 -0
- package/dist/types/process/js/js-children.d.ts +5 -0
- package/dist/types/process/js/js-context.d.ts +2 -0
- package/dist/types/process/js/js-sync.d.ts +3 -0
- package/dist/types/process/kernel-streams.d.ts +1 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -321,7 +321,7 @@ On top of preview1 a WASI program gets:
|
|
|
321
321
|
- `pthread_kill` to another thread gives `SI_TKILL` (-6) from the process. wasix-sysroot's `pthread_kill` of the calling thread itself goes through `kill(getpid())` and so arrives as `SI_USER`, until its libc takes the thread route for itself too;
|
|
322
322
|
- `SIGCHLD` gives `CLD_EXITED` (1) with the exit status, `CLD_KILLED` (2) with the signal, `CLD_STOPPED` (5) or `CLD_CONTINUED` (6), with the child's pid and real uid;
|
|
323
323
|
- a terminal's ^C/^Z/^\, `alarm` and the kernel's own signals give `SI_KERNEL` (0x80) with pid and uid 0, and so does a `sigwait_info` take with nothing recorded;
|
|
324
|
-
- a standard signal doesn't queue, so the first pending instance's sender is kept until the signal is taken.
|
|
324
|
+
- a standard signal doesn't queue, so the first pending instance's sender is kept until the signal is taken. A caught signal sent again while it's still pending coalesces with it: the handler runs once and the program goes on, as on Linux ([#1088](https://github.com/ai-ecoverse/slicc-kernel/issues/1088)). One deliberate deviation: a second `SIGINT`, `SIGTERM`, `SIGQUIT` or `SIGHUP` while the first is still pending **and the program has made no kernel call since** ends the program with the signal's default action. A WASI program that computes without making calls can't run a handler, so this is what lets a second ^C end it (a Python `while True: pass`). Timer and user signals (`SIGALRM`, `SIGUSR1` …) always coalesce, and so does a signal the program has blocked, or is still handling, while it makes calls.
|
|
325
325
|
A kernel without the imports answers `ENOSYS`, and the libc reports `SI_USER` with pid and uid 0, as before. **SIGCHLD:** with `SIGCHLD` set to `SIG_IGN` (by the program, or inherited), or with `SA_NOCLDWAIT` in the flags `slicc.sigaction_set` gets for `SIGCHLD`, an exited child is reaped at once and never stays a zombie: `waitpid` for it is `ECHILD`, and `wait` blocks until every child has exited, then fails with `ECHILD`, as on Linux. `SIGCHLD` is still sent. An explicit `SIG_DFL` turns it back off. A child that a process waits on for `exec`, or that `kernel.run` and the run calls collect, is never reaped that way.
|
|
326
326
|
- **Pipes** have no file type for WASI and WASIX programs' `fstat` (`st_mode` type 0), because WASI has no FIFO file type. A socket type, as kernels 1.47.3 to 1.48.1 gave them, made Ruby treat a `popen` pipe as a socket (`close_write` then never ends the child's input). The kernel's `slicc_fs.fd_mode` answers `S_IFIFO | 0600` (Linux numbering) for a pipe, and wasix-sysroot 2025.9.30-21 and later take the type from it, so programs built on it see a FIFO (`S_ISFIFO`); older libcs keep only the permission bits. Until then Python's asyncio refuses a pipe for a subprocess (`Pipe transport is only for pipes, sockets and character devices`). They are read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. Emscripten programs, bash included, see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output.
|
|
327
327
|
- **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream or datagram 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. A write to a pipe, FIFO or terminal whose reader is gone follows the program's `SIGPIPE` disposition, as the kernel knows it (#810): at the default it ends the program with status 141; ignored (also an ignore inherited across `exec` and spawn, as bash's `trap '' PIPE` passes on), the write fails with `EPIPE`; with a handler, the handler runs on the thread that wrote and then the write fails with `EPIPE` (#820); blocked, `SIGPIPE` stays pending and the write fails with `EPIPE`. An Emscripten program's write to a pipe, and its `send` without `MSG_NOSIGNAL`, follow the same rules, except that the kernel doesn't see an Emscripten program's signal mask, so a blocked `SIGPIPE` there still ends it. A WASIX program's libc reads an inherited ignore back through `slicc.sigaction_inherited` (wasix-sysroot -32 and later; see below). An Emscripten program's shim reads it from `Module.sliccKernel.sigInherited()` (#965, homescoop#973). That's a mask with bit n set for signal n (not bit n-1, as the WASIX import has it) holding only inherited `SIG_IGN`s, never `SIGKILL` or `SIGSTOP`. The shim seeds its `sigaction` table from it in a constructor that runs before any user constructor. The kernel puts the function on `Module.sliccKernel` before the glue runs constructors, ahead of the rest of `sliccKernel`. On a kernel without the function it is undefined, and the shim keeps the defaults. 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. `sock_open(SOCK_DGRAM)` on `AF_INET`/`AF_INET6` gives a [datagram socket](#datagram-sockets-udp); `sock_send_to` and `sock_recv_from` carry its addresses, and `RECV_DATA_TRUNCATED` is set in `ro_flags` when a datagram did not fit.
|
|
@@ -383,10 +383,19 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
383
383
|
- `status` is the exit status, or `null` with `signal` set when a signal ended the child. A non-zero status is not an error. A command that cannot start (it does not exist, or is not executable) answers `error: { code: 'ENOENT' }` (or the errno), `status: null` and `pid: 0` instead of throwing.
|
|
384
384
|
- More than `maxBuffer` bytes on `stdout` or `stderr` (1 MiB by default) sends the child `killSignal` (`SIGTERM` by default), keeps the first `maxBuffer` bytes, and answers `error: { code: 'ENOBUFS' }`. The kernel keeps at most 64 MiB of each stream whatever `maxBuffer` says, `Infinity` included, since the buffers live in the kernel worker that all processes share. `timeout` (ms) sends `killSignal` when it runs out and answers `error: { code: 'ETIMEDOUT' }`. A child that ignores `killSignal` is still waited for, as in Node.
|
|
385
385
|
- The child is in the program's process group, so `^C` reaches it. The kernel reaps it, so it leaves no zombie, and its CPU time counts as the program's children's. A signal the program catches does not cut the run short; its handler runs when the call returns. A signal that kills the program kills it as usual;
|
|
386
|
-
- `sync.spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr })` starts a child as `spawn` below does, with the same options and the same child, but synchronously: it returns `{ pid, stdin, stdout, stderr, wait(), kill(signal) }` at once, so a runtime can set Node's `child.pid` before `spawn()` returns. A spawn error (`ENOENT`, `EACCES`, `ESRCH` or `EPERM` for a group, `EINVAL` for a bad option, `EBADF` for a descriptor the program doesn't have) is thrown at once, with its `code`. The streams, `wait()` and `kill()` are the asynchronous ones, used afterwards from async code; the child doesn't wait for the program to read, since its output goes to a kernel pipe, which holds what it writes until the program reads it (a child that fills the pipe waits, as with `spawn`);
|
|
386
|
+
- `sync.spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a child as `spawn` below does, with the same options and the same child, but synchronously: it returns `{ pid, stdin, stdout, stderr, wait(), kill(signal) }` at once, so a runtime can set Node's `child.pid` before `spawn()` returns. A spawn error (`ENOENT`, `EACCES`, `ESRCH` or `EPERM` for a group, `EINVAL` for a bad option, `EBADF` for a descriptor the program doesn't have) is thrown at once, with its `code`. The streams, `wait()` and `kill()` are the asynchronous ones, used afterwards from async code; the child doesn't wait for the program to read, since its output goes to a kernel pipe, which holds what it writes until the program reads it (a child that fills the pipe waits, as with `spawn`);
|
|
387
387
|
- `sync.kill(pid, signal)`, a synchronous `kill`. When the signal reaches the program itself (its own pid, `0` for its process group, or `-pgid` of its own group) and its action is to end the program (no handler, not ignored), the call never returns: the program's writes reach their descriptors and its terminal modes are put back, as for `sync.exit`, then the rest of the group is signalled and the program ends by the signal, so a parent's `wait` and `sync.run` report `signal`, as Node does for `process.kill(process.pid)`. A signal with a handler runs it when the call returns, an ignored one does nothing, and a stop signal stops the program. A signal whose default action dumps core (`SIGQUIT`, `SIGABRT`, `SIGSEGV`, …) ends it the same way; the kernel writes no core and sets no core flag in the wait status. `-1` signals every process the program may signal except itself and pid 1, as on Linux, and returns (`ESRCH` when there was none, `EPERM` when there were some but it may signal none of them); a process that exec'd gets the signal once. Without users, every process runs as root, so `kill -9 -1` from any program ends every other process; with users it reaches only what the sender may signal (its own uid's processes, or all of them as root);
|
|
388
388
|
- `sync.exit(status)`, which ends the process at once, as Node's `process.exit()` does: no `catch`, `finally`, microtask, timer or signal handler runs after it, because the call never returns. First the writes the program has made reach their descriptors, in order (a broken pipe drops the rest for that descriptor), and terminal modes the program set with `tty` are put back. Then the process ends the way any exit does: its descriptors are closed and written back, its parent gets `SIGCHLD`, and `wait` reports `status`. Children keep running. A JS program has no threads of its own (`Worker` is withheld), so nothing of it outlives the call. `exit(status)` (below) unwinds instead, so the program's `catch` and `finally` blocks run;
|
|
389
|
-
- `
|
|
389
|
+
- `socketpair()` (and `sync.socketpair()`) answers `[a, b]`, two descriptors of the program on a connected `AF_UNIX` stream pair, and `pipe()` (and `sync.pipe()`) answers `[readFd, writeFd]` ([#1093](https://github.com/ai-ecoverse/slicc-kernel/issues/1093)).
|
|
390
|
+
- **The same descriptors either way:** the async and `sync` forms give the program's own kernel descriptors, which `read`, `write`, `close` and their `sync` forms all take, until the program closes them or ends.
|
|
391
|
+
- **Reading** a socket or pipe whose peer has closed gives an empty `Uint8Array` (end of file). A `read` still pending when the program `close`s that descriptor rejects with `EBADF` instead of waiting. A `read` and a `write` on one socket can be pending at the same time, as a socket is full duplex.
|
|
392
|
+
- **Writing to a socket whose peer is gone** fails with `EPIPE` and raises no `SIGPIPE`, at the default disposition too, as for WASIX and Emscripten programs (as if `MSG_NOSIGNAL` were set). A pipe's broken write follows `SIGPIPE` as described below.
|
|
393
|
+
- **`fds`**, an option of `spawn` and `sync.spawn`, gives the child more descriptors: `{ 3: f }` puts the program's descriptor `f` at the child's fd 3, on the same open file description, as `dup2` would.
|
|
394
|
+
- **Keys:** each key must be an integer of 3 or more, as stdio stays `stdin`, `stdout` and `stderr`. Each value must be an open descriptor of the program. Anything else is `EINVAL` before the child starts.
|
|
395
|
+
- **Only the named descriptors reach the child.** Others the program holds, the other end of a pair included, don't, and a grandchild gets fd 3 only if the child names it again.
|
|
396
|
+
- **The parent's copy:** the program keeps `f` and closes it itself when it no longer needs it, so the child sees end of file once it's the only one left.
|
|
397
|
+
- **Why a `Record`:** it can't name a child fd twice, and it reads like Node's `stdio` index, so it's this rather than a list of `{ fd, from }`.
|
|
398
|
+
- `spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The command run is `argv[0]`; `argv0`, when given, is the `argv[0]` the child sees instead, as given (no basename; an empty string too), as with Node's `options.argv0`: `spawn({ argv: ['/bin/sh', '-c', 'echo $0'], argv0: '/bin/sh' })` prints `/bin/sh`, where without it the child sees the command's own name (`sh`). A command whose package manifest sets its `argv0` (a multi-call program's applets, such as coreutils') keeps that one. An `argv0` that isn't a string is `EINVAL`. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`. `parentDeathSignal` (a number or `'SIGTERM'`, as Linux's `PR_SET_PDEATHSIG`) is sent to the child when the program ends, killed or not: to the child's whole group when it leads one (`pgid: 0` or `session: true`), else to the child alone. It's sent with the program's credentials, so a child the program may no longer signal (one that gained privilege) doesn't get it. If the program has already ended by the time the spawn completes, it's sent at once. A child of its own (not a group leader) that has been waited for gets nothing, and `sync.run` has no such option. A bad signal is `EINVAL`;
|
|
390
399
|
- `wait(pid, { untraced })` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; with `untraced: true` it also resolves when the child stops, with `{ status: 128 + signal, signal, stopped: true }`, as `waitpid` with `WUNTRACED` (a child's `wait({ untraced })` is the same), and options that aren't an object are `EINVAL`; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
|
|
391
400
|
- `sudo(args)`, what the [`sudo`](#sudo) command calls: `args` are its arguments after `sudo`, and it resolves with `{ pid }`, the command the kernel started as the program's child, to `wait` for; `{ run }`, an argv to run as the caller (with `users: 'off'`); `{ list }`, the text of `sudo -l`; or `{ error, errno }`, the message to print;
|
|
392
401
|
- `crontab({ action, user?, text? })`, what the [`crontab`](#cron) command calls: `action` is `'list'`, `'remove'` or `'install'` (with the table as `text`), on the caller's own table or, for an effective uid of 0, on `user`'s. It resolves with `{ text }` (the table for `list`, `''` otherwise) or `{ error }`, the message to print. A malformed call is `EINVAL`;
|
package/dist/kernel-worker.js
CHANGED
|
@@ -6656,7 +6656,7 @@ function satisfies(version, range2) {
|
|
|
6656
6656
|
}
|
|
6657
6657
|
|
|
6658
6658
|
// src/version.ts
|
|
6659
|
-
var KERNEL_VERSION = true ? "1.125.
|
|
6659
|
+
var KERNEL_VERSION = true ? "1.125.11" : "0.0.0-dev";
|
|
6660
6660
|
var OS_RELEASE = "6.0.0-slicc";
|
|
6661
6661
|
function procVersion() {
|
|
6662
6662
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -19354,6 +19354,7 @@ var MAX_READ = 1024 * 1024;
|
|
|
19354
19354
|
var O_NONBLOCK = 2048;
|
|
19355
19355
|
var WRITE_ACCESS = 3;
|
|
19356
19356
|
var KEYBOARD = /* @__PURE__ */ new Set([SIG.INT, SIG.QUIT, SIG.TSTP]);
|
|
19357
|
+
var STUCK_ESCAPES = /* @__PURE__ */ new Set([SIG.INT, SIG.TERM, SIG.QUIT, SIG.HUP]);
|
|
19357
19358
|
var DEFAULT_UMASK2 = 18;
|
|
19358
19359
|
function words(value2) {
|
|
19359
19360
|
if (!Array.isArray(value2) || !value2.every((w) => typeof w === "string")) {
|
|
@@ -19410,6 +19411,8 @@ var WasmProcess = class {
|
|
|
19410
19411
|
cred;
|
|
19411
19412
|
execChild;
|
|
19412
19413
|
killed = false;
|
|
19414
|
+
calls = 0;
|
|
19415
|
+
pendingAt = /* @__PURE__ */ new Map();
|
|
19413
19416
|
execTermsig;
|
|
19414
19417
|
stopped = 0;
|
|
19415
19418
|
stops = 0;
|
|
@@ -19508,9 +19511,9 @@ var WasmProcess = class {
|
|
|
19508
19511
|
const action = defaultAction(sig);
|
|
19509
19512
|
return action === "stop" ? this.stop(sig) : action;
|
|
19510
19513
|
}
|
|
19511
|
-
if (((this.options.pendingBits?.() ?? 0) & bit) !== 0
|
|
19512
|
-
return "terminate";
|
|
19513
|
-
}
|
|
19514
|
+
if (((this.options.pendingBits?.() ?? 0) & bit) !== 0) {
|
|
19515
|
+
if (STUCK_ESCAPES.has(sig) && this.pendingAt.get(sig) === this.calls) return "terminate";
|
|
19516
|
+
} else this.pendingAt.set(sig, this.calls);
|
|
19514
19517
|
this.options.onPending?.(sig);
|
|
19515
19518
|
if (this.options.jobs?.keyboard && KEYBOARD.has(sig) && this.waiting.size === 0)
|
|
19516
19519
|
this.owed = true;
|
|
@@ -19633,6 +19636,7 @@ var WasmProcess = class {
|
|
|
19633
19636
|
}
|
|
19634
19637
|
async syscall(req, cancelled, block) {
|
|
19635
19638
|
this.options.onSyscall?.(req);
|
|
19639
|
+
this.calls += 1;
|
|
19636
19640
|
const started = performance.now();
|
|
19637
19641
|
let restarted = false;
|
|
19638
19642
|
for (; ; ) {
|