@ai-ecoverse/slicc-kernel 1.77.1 → 1.78.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 -10
- package/dist/driver-worker.js +73 -4
- package/dist/driver-worker.js.map +3 -3
- package/dist/index.js +104 -14
- package/dist/index.js.map +4 -4
- package/dist/kernel-worker.js +20 -6
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-driver-worker.js +73 -4
- package/dist/node-driver-worker.js.map +3 -3
- package/dist/node-process-worker.js +18 -2
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +57 -15
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +18 -2
- package/dist/process-worker.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ const { status, stdout, stderr } = await kernel.run(['bash', '-c', 'echo hi > he
|
|
|
38
38
|
| `onMountPending` | none | `({ target, source, insert }) => void`: an `fsa` mount needs a folder; call `insert()` from a user gesture |
|
|
39
39
|
| `processMounts` | `true` | whether programs may call `mount(2)` and `umount2(2)`: `false`, or `(req) => boolean \| Promise<boolean>` to decide each call (see [Mounting from a program](#mounting-from-a-program)) |
|
|
40
40
|
| `hostname` | `'slicc'` | the kernel's node name (see [Node name](#node-name)) |
|
|
41
|
-
| `users` | `'
|
|
41
|
+
| `users` | `'enforce'` | `'off'`, `'report'` or `'enforce'`: whether files have owners and their modes are checked (see [File owners](#file-owners) and [Permissions](#permissions)), and whether `sudo` elevates (see [Sudo](#sudo)). `'off'` behaves exactly as kernels before owners did; any other value rejects. `createNodeKernel` takes it too, with the same default. Kernels up to 1.73 defaulted to `'off'` |
|
|
42
42
|
| `onDenial` | none | `({ pid, uid, op, path, errno, tag? }) => void`: called for every permission check that fails, under `'report'` and `'enforce'` (see [Permissions](#permissions)). `createNodeKernel` takes it too |
|
|
43
43
|
| `sealed` | none | `(req, { signal }) => Promise<object>`: answers requests written to [`/dev/sealed`](#devsealed). `createNodeKernel` takes it too |
|
|
44
44
|
| `clipboard` | none | `{ read(stamp) => Promise<string>, write(text, stamp) => Promise<void> }`: the clipboard behind [`/dev/clipboard`](#devclipboard). `createNodeKernel` takes it too |
|
|
@@ -46,9 +46,9 @@ const { status, stdout, stderr } = await kernel.run(['bash', '-c', 'echo hi > he
|
|
|
46
46
|
| `cdp` | none | `({ runtime }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
|
|
47
47
|
| `protect` | none | `{ paths, writers? }`: directories no process may change, root included, in every `users` mode, except processes with one of the `writers` tags (see [Protected paths](#protected-paths)). `createNodeKernel` takes it too |
|
|
48
48
|
|
|
49
|
-
### `kernel.run(argv, options?) → Promise<{ status, stdout, stderr }>`
|
|
49
|
+
### `kernel.run(argv, options?) → Promise<{ status, signal, stdout, stderr }>`
|
|
50
50
|
|
|
51
|
-
Runs `argv[0]` with `argv` as its arguments and resolves when it exits. `stdout` and `stderr` are decoded as UTF-8; `status` is the exit status, `128 + n` for a process killed by signal `n`, and `127` when `argv[0]` cannot be found.
|
|
51
|
+
Runs `argv[0]` with `argv` as its arguments and resolves when it exits. `stdout` and `stderr` are decoded as UTF-8; `status` is the exit status, `128 + n` for a process killed by signal `n`, and `127` when `argv[0]` cannot be found. `signal` is there only when a signal ended the process, named as `'SIGKILL'`, so a `kill -9` (`status: 137, signal: 'SIGKILL'`) can be told from an `exit 137` (`status: 137`, no `signal`).
|
|
52
52
|
|
|
53
53
|
| option | |
|
|
54
54
|
| --- | --- |
|
|
@@ -77,6 +77,7 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
|
|
|
77
77
|
| `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
|
|
78
78
|
| `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
|
|
79
79
|
| `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
|
|
80
|
+
| `ended` | resolves with `{ status, signal }` at the same time: `status` as `exited`, and `signal` named (`'SIGKILL'`) only when a signal ended it |
|
|
80
81
|
| `close()` | hangs up: `SIGHUP` to the foreground process group, and reads from the terminal return end-of-file |
|
|
81
82
|
|
|
82
83
|
### `kernel.terminate()`
|
|
@@ -131,6 +132,7 @@ Reach a server a program runs on the kernel's loopback (a dev server, `python -m
|
|
|
131
132
|
- **`loopbackFetch`** sends an HTTP/1.1 request over `dial` and resolves with a `Response` as soon as the headers arrive. The body streams (chunked, counted, or until the connection closes), so server-sent events and long polls work; cancelling the body or aborting `signal` closes the connection. The request is routed by `port`, not by the URL, whose host is sent as `Host`; a request body is sent with `Content-Length`, and every request uses a connection of its own (`Connection: close`).
|
|
132
133
|
- **Errors** reject with an `Error` whose `code` is the errno: `ECONNREFUSED` when nothing listens on the port or the port is kernel-only (`9222`, the [CDP facade](#browser-automation-cdp)), `ENETUNREACH` for a host that is not the loopback, `EINVAL` for a port outside 1–65535.
|
|
133
134
|
- **Names**: a page reaches the kernel's loopback as `<port>.kernel.localhost`, which is the loopback and a secure context, so a request no service worker routes lands on port 80 of the real machine instead of a real server on that port; the service worker routes the name to `loopbackFetch` with that port. Programs find the name in `SLICC_PAGE_LOOPBACK` (`kernel.localhost`) and `SLICC_PAGE_LOOPBACK_URL` (`http://{port}.kernel.localhost`), to print or inject the right URL.
|
|
135
|
+
- **No per-user isolation**: as on Linux, any user's process can connect to any listener on `127.0.0.1`, and the page reaches every one as `<port>.kernel.localhost`, so the loopback is no privilege boundary; a service that needs one must authenticate its clients.
|
|
134
136
|
|
|
135
137
|
### `kernel.attest(path)` and `kernel.watch(paths, onChange)`
|
|
136
138
|
|
|
@@ -152,9 +154,9 @@ Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker
|
|
|
152
154
|
|
|
153
155
|
| Client | |
|
|
154
156
|
|---|---|
|
|
155
|
-
| `spawn(argv, { cwd, user, env, stdin, pgid, onStdout, onStderr })` | runs over pipes and resolves with `{ pid, pgid, exited, signal(name) }` once the process has started; output arrives in chunks, as bytes, and the kernel keeps no copy; every spawn leads its own process group, which `signal` (default `SIGTERM`) signals, unless `pgid` names an existing group to join (protocol 1.3): the process then joins that group and that group's session, as `setpgid` would (a client has no session of its own: each of its spawns and terminals leads one, and `setpgid` cannot cross sessions), and `signal` signals the whole group. The group must have been started by this client (a spawn or a terminal it opened), even if its leader has exited; an unknown group rejects with `code: 'ESRCH'`, another session's with `code: 'EPERM'`, and against a 1.2 kernel the client rejects with `code: 'ENOSYS'` before sending anything, as an older kernel would ignore the option; a program that cannot start rejects with `code: 'ENOENT'`. `user` runs it as another user, which only a root client may (see `kernel.connect`); against a 1.7 kernel the client rejects with `code: 'ENOSYS'` |
|
|
156
|
-
| `run(argv, options?)` | `spawn` and wait: `{ pid, status, stdout, stderr }` as text, with `onStdout`/`onStderr` streaming text |
|
|
157
|
-
| `openTerminal(argv, options?)` | a pty session, as `kernel.openTerminal` |
|
|
157
|
+
| `spawn(argv, { cwd, user, env, stdin, pgid, onStdout, onStderr })` | runs over pipes and resolves with `{ pid, pgid, exited, ended, signal(name) }` once the process has started; `ended` resolves with `{ status, signal }`, `signal` named only when a signal ended the process, as for `kernel.run` (an older kernel never names one); output arrives in chunks, as bytes, and the kernel keeps no copy; every spawn leads its own process group, which `signal` (default `SIGTERM`) signals, unless `pgid` names an existing group to join (protocol 1.3): the process then joins that group and that group's session, as `setpgid` would (a client has no session of its own: each of its spawns and terminals leads one, and `setpgid` cannot cross sessions), and `signal` signals the whole group. The group must have been started by this client (a spawn or a terminal it opened), even if its leader has exited; an unknown group rejects with `code: 'ESRCH'`, another session's with `code: 'EPERM'`, and against a 1.2 kernel the client rejects with `code: 'ENOSYS'` before sending anything, as an older kernel would ignore the option; a program that cannot start rejects with `code: 'ENOENT'`. `user` runs it as another user, which only a root client may (see `kernel.connect`); against a 1.7 kernel the client rejects with `code: 'ENOSYS'` |
|
|
158
|
+
| `run(argv, options?)` | `spawn` and wait: `{ pid, status, signal, stdout, stderr }` as text, with `onStdout`/`onStderr` streaming text; `signal` as for `kernel.run` |
|
|
159
|
+
| `openTerminal(argv, options?)` | a pty session, as `kernel.openTerminal`, `ended` included |
|
|
158
160
|
| `ps()` | the kernel's processes: `{ pid, ppid, pgid, sid, argv, tty, started, state, rss, utime, stime, uid, gid }`, `state` being `'S'`, `'T'` (stopped) or `'Z'`, `rss` the process's resident memory in bytes (the size of its wasm memory, what `/proc/<pid>/status` shows as `VmRSS`; absent from kernels before 1.61), `utime`/`stime` the CPU time it has used, in milliseconds, in its program and in the kernel's calls (see [`/proc`](#proc); absent for a JS program and from kernels before 1.63) and `uid`/`gid` the effective ids (absent from a 1.7 kernel). A client bound to a user sees only that user's processes |
|
|
159
161
|
| `kill(pid, signal?)` | a process, or a process group with a negative pid; no such process rejects with `code: 'ESRCH'`, and one the client's user may not signal with `code: 'EPERM'` |
|
|
160
162
|
| `fs` | `readFile`, `readText`, `writeFile(path, string \| bytes)`, `stat`, `lstat`, `readdir`, `mkdir` (with parents), `rm(path, { force })` (recursive), `rename`, `realpath`, `symlink(target, path)`, `readlink`, `exists`, on the processes' file system, mounts included (a path under a `tmpfs`, `fsa` or `hostfs` mount reads and writes the mounted file system, as `cat` in a process would); failures reject with `KernelCallError` and a POSIX `code` |
|
|
@@ -416,7 +418,7 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
|
|
|
416
418
|
|
|
417
419
|
## Filesystem
|
|
418
420
|
|
|
419
|
-
`/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. Files directly in `/`, and directories created there after a process started, are OPFS as well, the same for Emscripten and WASI programs; only `/dev` and `/proc` live in each process's memory. A program's `mkdir` (WASI `path_create_directory`, Emscripten's `mkdir`) is exclusive, as on Linux: it fails with `EEXIST` when the name exists, and of several processes or threads making one name at once exactly one succeeds, so a directory works as a lock. Under a missing directory it fails with `ENOENT`, as on Linux (`mkdir -p` and `os.makedirs` make the parents in the program); a JS program's `ctx.fs.mkdir` makes parents itself. The kernel makes the system `PNPM_HOME` (`/usr/local/share/pnpm`, or the embedder's) when it starts, since pnpm makes only its `bin` directory there. A full origin storage (`QuotaExceededError`, which also comes early in a browser's private or off-the-record contexts, whatever `navigator.storage.estimate()` says) is `ENOSPC`, never retried. An OPFS or IndexedDB failure that has no POSIX counterpart is `EIO`; its message names the DOM error and the operation (`lookup`, `read`, `write`, `commit` or `sidecar`), and the kernel worker logs the first of each kind to its console. File contents are buffered per open file and written back on close, `fsync` and exit
|
|
421
|
+
`/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. Files directly in `/`, and directories created there after a process started, are OPFS as well, the same for Emscripten and WASI programs; only `/dev` and `/proc` live in each process's memory. A program's `mkdir` (WASI `path_create_directory`, Emscripten's `mkdir`) is exclusive, as on Linux: it fails with `EEXIST` when the name exists, and of several processes or threads making one name at once exactly one succeeds, so a directory works as a lock. Under a missing directory it fails with `ENOENT`, as on Linux (`mkdir -p` and `os.makedirs` make the parents in the program); a JS program's `ctx.fs.mkdir` makes parents itself. The kernel makes the system `PNPM_HOME` (`/usr/local/share/pnpm`, or the embedder's) when it starts, since pnpm makes only its `bin` directory there. A full origin storage (`QuotaExceededError`, which also comes early in a browser's private or off-the-record contexts, whatever `navigator.storage.estimate()` says) is `ENOSPC`, never retried. An OPFS or IndexedDB failure that has no POSIX counterpart is `EIO`; its message names the DOM error and the operation (`lookup`, `read`, `write`, `commit` or `sidecar`), and the kernel worker logs the first of each kind to its console. File contents are buffered per open file and written back on close, `fsync` and exit, so a refused write (`ENOSPC`, `EIO`) surfaces at that close or `fsync`, and a program that ignores close errors doesn't see it (`echo x > f` can exit 0 with `f` empty, as with any buffered write-back). A write-back error a close has already reported to a WASI program isn't raised again when it exits, so the program keeps its own exit status. Metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
|
|
420
422
|
|
|
421
423
|
Directories are renamed with `FileSystemHandle.move()` where available, else by copy and delete.
|
|
422
424
|
|
|
@@ -570,7 +572,7 @@ Programs ask the kernel for their ids:
|
|
|
570
572
|
|
|
571
573
|
### File owners
|
|
572
574
|
|
|
573
|
-
With `
|
|
575
|
+
With `users: 'enforce'`, the default, or `'report'`, files have owners, and [Permissions](#permissions) checks them against a process's credentials. With `'off'` nothing below happens: the kernel stores no owner, and every file looks owned by the caller as described under [Metadata](#metadata).
|
|
574
576
|
|
|
575
577
|
- **Where owners live:** on the root file system, in the sidecar record's `uid` and `gid` (see [Metadata](#metadata)). A path with no owner of its own has the owner of its nearest ancestor that has one, and `/` belongs to root, so a root written before owners existed, or by the BIOS through the OPFS API, is root's throughout. A directory whose owner changes keeps the owner its unowned contents had, in `heir`, so `chown cone /old` gives `cone` the directory and leaves the files below it to their old owner. A rename keeps the owner of what it moves.
|
|
576
578
|
- **New files:** a file, directory or symlink a process creates belongs to its effective uid, and to its effective gid, or to the directory's group when the directory has the setgid bit; a new directory in a setgid directory gets the setgid bit too. Every file and directory a process creates gets the mode it asked for (`0666` and `0777` when it can't ask, as a JS program's `ctx.fs` can't) less the process's umask, whichever runtime made it; an Emscripten program's file no longer gets `0644` whatever it asked for. A file written through a client's `fs` belongs to the client's user (root for a page) and gets `0666`, a directory `0777`, less the umask `kernel.users.add` gave that user (`022` for root and by default); the kernel's own files, such as `/etc/passwd`, have no owner of their own.
|
|
@@ -596,7 +598,7 @@ With `users: 'enforce'` the kernel checks every file call that reaches it agains
|
|
|
596
598
|
- **A client's `fs`:** checked as the client's user; a page's client (root) passes.
|
|
597
599
|
- **Removed files still open:** a process that holds a removed file may look it up (`stat`, `lstat`) under the hidden name the kernel gave it in `/.slicc-unlinked`, which is root's `0700`; every other process is refused there, so the kernel's own bookkeeping of held files raises no denials for their holders.
|
|
598
600
|
|
|
599
|
-
`users: 'enforce'` is
|
|
601
|
+
`users: 'enforce'` is the default of `createKernel` and `createNodeKernel`; pass `'report'` to find what a program would be refused without refusing it, or `'off'` for kernels without owners. A `Launcher` made directly (see [What is in here](#what-is-in-here)) still defaults to `'off'`.
|
|
600
602
|
|
|
601
603
|
### Sudo
|
|
602
604
|
|
|
@@ -608,7 +610,7 @@ With `users: 'enforce'` the kernel checks every file call that reaches it agains
|
|
|
608
610
|
- **The command** is found once, on `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` for a bare name or against the caller's directory for a path, and its real path (symlinks resolved) is what runs and what the policy sees. That file, and every file that will really run for it (a catalog command's glue, module and imports module, and the `package.json` that defines it, a script and the interpreter its `#!` line or `binfmt` suffix names), must be root's and writable by root only, and so must every directory above each, or `sudo` refuses (`EPERM`), whatever the policy says. A command that wouldn't start as the target user (a `#!` line naming an interpreter that is only the caller's, say) fails with `command not found` before anything is asked. Other files a program loads at run time (a package's libraries) are the admin's to keep safe, as on Linux. The caller's directory, which the program passes on, must be a directory the caller may enter, or `sudo` refuses (`EACCES`). The kernel resolves it to its real path once, shows that in the request, and runs the command there; if it no longer resolves the same, or the caller can no longer enter it, when the answer comes or when the command starts, nothing runs.
|
|
609
611
|
- **The decision:** a caller whose effective uid is 0 runs anything (the check on the command's path still applies). Otherwise a matching `NOPASSWD` line runs the command at once; any other matching line asks `approve`; nothing matching is refused (`sudo: cone is not allowed to run /usr/bin/ls as root`).
|
|
610
612
|
- **Approval:** `approve(req, { signal })` gets `{ id, kind: 'sudo', user, uid, target, argv, path, cwd, pid, tag?, tty?, createdAt, expiresAt, always: true }`: the caller's name and real uid, the target user, the argv as typed, the resolved `path`, the caller's directory, pid, tag and terminal, and the times in ms. It answers `'once'` (run it), `'always'` (run it, and append `NOPASSWD <user> (<target>) Cmnd <path> <args…>` to `/etc/sudoers.d/<user>`, root's `0440`, so it's never asked again; if that can't be written, nothing runs) or `'deny'`; a throw, any other answer or no hook at all is `sudo: approval denied`. After 5 minutes the request expires (`signal` aborts with `'expired'`, and `sudo` fails with `sudo: the request expired`); when the requester ends or gets a signal while it waits, `^C` included, `signal` aborts with `'withdrawn'` and `sudo` ends with that signal's status. A user has at most one request waiting; another fails with `sudo: another request is pending`. On the page, `createKernel` relays the request from the kernel worker and back, and the abort too; `terminate()` or a failed worker aborts every request still waiting with `'withdrawn'`.
|
|
611
|
-
- **Running:** the command runs as the caller's child, with all six ids and the groups of the target user, in the caller's directory, on its terminal, standard input, output and error, and in its process group; `sudo` exits with its status, and passes `SIGTERM` and `SIGHUP` on to it, also one that comes while it starts (the kernel lets it signal the command it started, which its own user may not); `SIGINT` and `SIGQUIT` it leaves to the command, which gets them from the terminal itself. It's found among the system's commands only, never a user's own. Its environment is the kernel's (the embedder's `env` and the proxy settings), the target's `HOME`, `USER`, `LOGNAME`, `SHELL`, `PNPM_HOME` and `NPM_CONFIG_PREFIX`, `PATH` set to the path above, `PWD`, the caller's `TERM` when it is a plain name (letters, digits, `.`, `_`, `+`, `-`), and `SUDO_USER`, `SUDO_UID`, `SUDO_GID` and `SUDO_COMMAND`; nothing else of the caller's comes through.
|
|
613
|
+
- **Running:** the command runs as the caller's child, with all six ids and the groups of the target user, in the caller's directory, on its terminal, standard input, output and error, and in its process group; `sudo` exits with its status, and passes `SIGTERM` and `SIGHUP` on to it, also one that comes while it starts (the kernel lets it signal the command it started, which its own user may not); `SIGINT` and `SIGQUIT` it leaves to the command, which gets them from the terminal itself. Two narrow gaps remain ([#424](https://github.com/ai-ecoverse/slicc-kernel/issues/424)): a relayed signal reaches the command but not the processes it started itself, which keep running as the target user if it ends; and right after the command starts, a `SIGINT` or `SIGQUIT` sent to `sudo`'s own pid (not through the terminal) ends `sudo` and leaves the command running. Closing both needs the command in a process group of its own, which changes job control. It's found among the system's commands only, never a user's own. Its environment is the kernel's (the embedder's `env` and the proxy settings), the target's `HOME`, `USER`, `LOGNAME`, `SHELL`, `PNPM_HOME` and `NPM_CONFIG_PREFIX`, `PATH` set to the path above, `PWD`, the caller's `TERM` when it is a plain name (letters, digits, `.`, `_`, `+`, `-`), and `SUDO_USER`, `SUDO_UID`, `SUDO_GID` and `SUDO_COMMAND`; nothing else of the caller's comes through.
|
|
612
614
|
- **Cost:** with `users: 'off'`, and with owners on for programs that don't call `sudo`, nothing is added to any call.
|
|
613
615
|
|
|
614
616
|
### `/dev/sealed`
|
package/dist/driver-worker.js
CHANGED
|
@@ -280,6 +280,54 @@ var RemoteTransport = class {
|
|
|
280
280
|
}
|
|
281
281
|
};
|
|
282
282
|
|
|
283
|
+
// src/kernel/signals.ts
|
|
284
|
+
var SIG = {
|
|
285
|
+
HUP: 1,
|
|
286
|
+
INT: 2,
|
|
287
|
+
QUIT: 3,
|
|
288
|
+
ILL: 4,
|
|
289
|
+
TRAP: 5,
|
|
290
|
+
ABRT: 6,
|
|
291
|
+
BUS: 7,
|
|
292
|
+
FPE: 8,
|
|
293
|
+
KILL: 9,
|
|
294
|
+
USR1: 10,
|
|
295
|
+
SEGV: 11,
|
|
296
|
+
USR2: 12,
|
|
297
|
+
PIPE: 13,
|
|
298
|
+
ALRM: 14,
|
|
299
|
+
TERM: 15,
|
|
300
|
+
STKFLT: 16,
|
|
301
|
+
CHLD: 17,
|
|
302
|
+
CONT: 18,
|
|
303
|
+
STOP: 19,
|
|
304
|
+
TSTP: 20,
|
|
305
|
+
TTIN: 21,
|
|
306
|
+
TTOU: 22,
|
|
307
|
+
URG: 23,
|
|
308
|
+
XCPU: 24,
|
|
309
|
+
XFSZ: 25,
|
|
310
|
+
VTALRM: 26,
|
|
311
|
+
PROF: 27,
|
|
312
|
+
WINCH: 28,
|
|
313
|
+
POLL: 29,
|
|
314
|
+
PWR: 30,
|
|
315
|
+
SYS: 31
|
|
316
|
+
};
|
|
317
|
+
var IGNORED_BY_DEFAULT = /* @__PURE__ */ new Set([SIG.CHLD, SIG.URG, SIG.WINCH]);
|
|
318
|
+
var STOPPING = /* @__PURE__ */ new Set([SIG.STOP, SIG.TSTP, SIG.TTIN, SIG.TTOU]);
|
|
319
|
+
function quiet(p) {
|
|
320
|
+
p.catch(() => void 0);
|
|
321
|
+
return p;
|
|
322
|
+
}
|
|
323
|
+
var SIGNAL_BY_NAME = {
|
|
324
|
+
SIGINT: SIG.INT,
|
|
325
|
+
SIGTERM: SIG.TERM,
|
|
326
|
+
SIGKILL: SIG.KILL,
|
|
327
|
+
SIGSTOP: SIG.STOP,
|
|
328
|
+
SIGCONT: SIG.CONT
|
|
329
|
+
};
|
|
330
|
+
|
|
283
331
|
// src/bytes.ts
|
|
284
332
|
function fresh(bytes, start = 0, end = bytes.length) {
|
|
285
333
|
return new Uint8Array(bytes.subarray(start, end));
|
|
@@ -665,6 +713,7 @@ var Channel = class {
|
|
|
665
713
|
return;
|
|
666
714
|
}
|
|
667
715
|
this.pending.delete(reply.id);
|
|
716
|
+
call.ended?.(reply.signal);
|
|
668
717
|
if (reply.error !== void 0) call.reject(new KernelCallError(reply.error, reply.code));
|
|
669
718
|
else call.resolve(reply.result);
|
|
670
719
|
}
|
|
@@ -713,12 +762,14 @@ async function spawnOn(channel, argv, opts) {
|
|
|
713
762
|
if (opts.user !== void 0) await since(channel, 8, "spawn as a user", async () => {
|
|
714
763
|
});
|
|
715
764
|
const started = Promise.withResolvers();
|
|
765
|
+
let endedBy;
|
|
716
766
|
const options = { ...rest, ...stdin !== void 0 ? { stdin: bytesOf(stdin) } : {} };
|
|
717
767
|
const { done } = channel.request(
|
|
718
768
|
{ op: "spawn", argv, options },
|
|
719
769
|
{
|
|
720
770
|
output: (fd, bytes) => (fd === 1 ? onStdout : onStderr)?.(bytes),
|
|
721
|
-
started: started.resolve
|
|
771
|
+
started: started.resolve,
|
|
772
|
+
ended: (signal) => endedBy = signal
|
|
722
773
|
}
|
|
723
774
|
);
|
|
724
775
|
done.catch(started.reject);
|
|
@@ -728,9 +779,18 @@ async function spawnOn(channel, argv, opts) {
|
|
|
728
779
|
pid,
|
|
729
780
|
pgid,
|
|
730
781
|
exited: done,
|
|
782
|
+
ended: endedOf(done, () => endedBy),
|
|
731
783
|
signal: async (name = "SIGTERM") => void await channel.call({ op: "kill", pid: -pgid, signal: name })
|
|
732
784
|
};
|
|
733
785
|
}
|
|
786
|
+
function endedOf(done, signal) {
|
|
787
|
+
return quiet(
|
|
788
|
+
done.then((status) => {
|
|
789
|
+
const by = signal();
|
|
790
|
+
return { status, ...by ? { signal: by } : {} };
|
|
791
|
+
})
|
|
792
|
+
);
|
|
793
|
+
}
|
|
734
794
|
async function runOn(channel, argv, opts) {
|
|
735
795
|
const { onStdout, onStderr, ...rest } = opts;
|
|
736
796
|
const chunks = { 1: [], 2: [] };
|
|
@@ -744,9 +804,15 @@ async function runOn(channel, argv, opts) {
|
|
|
744
804
|
onStdout: tee(1, onStdout),
|
|
745
805
|
onStderr: tee(2, onStderr)
|
|
746
806
|
});
|
|
747
|
-
const status = await child.
|
|
807
|
+
const { status, signal } = await child.ended;
|
|
748
808
|
const text = (fd) => new TextDecoder().decode(concat(chunks[fd]));
|
|
749
|
-
return {
|
|
809
|
+
return {
|
|
810
|
+
pid: child.pid,
|
|
811
|
+
status,
|
|
812
|
+
...signal ? { signal } : {},
|
|
813
|
+
stdout: text(1),
|
|
814
|
+
stderr: text(2)
|
|
815
|
+
};
|
|
750
816
|
}
|
|
751
817
|
async function terminalOn(channel, argv, opts) {
|
|
752
818
|
const { onData, ...options } = opts;
|
|
@@ -755,11 +821,13 @@ async function terminalOn(channel, argv, opts) {
|
|
|
755
821
|
let listener = onData ?? null;
|
|
756
822
|
const backlog = [];
|
|
757
823
|
const started = Promise.withResolvers();
|
|
824
|
+
let endedBy;
|
|
758
825
|
const { id, done } = channel.request(
|
|
759
826
|
{ op: "open-terminal", argv, options },
|
|
760
827
|
{
|
|
761
828
|
output: (_fd, bytes) => listener ? listener(bytes) : backlog.push(bytes),
|
|
762
|
-
started: started.resolve
|
|
829
|
+
started: started.resolve,
|
|
830
|
+
ended: (signal) => endedBy = signal
|
|
763
831
|
}
|
|
764
832
|
);
|
|
765
833
|
done.catch(started.reject);
|
|
@@ -768,6 +836,7 @@ async function terminalOn(channel, argv, opts) {
|
|
|
768
836
|
return {
|
|
769
837
|
pid,
|
|
770
838
|
exited: done,
|
|
839
|
+
ended: endedOf(done, () => endedBy),
|
|
771
840
|
get onData() {
|
|
772
841
|
return listener;
|
|
773
842
|
},
|