@ai-ecoverse/slicc-kernel 1.182.0 → 1.183.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 +54 -5
- package/dist/driver-worker.js +290 -90
- package/dist/driver-worker.js.map +4 -4
- package/dist/index.js +328 -102
- package/dist/index.js.map +4 -4
- package/dist/kernel-worker.js +1705 -695
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-driver-worker.js +298 -98
- package/dist/node-driver-worker.js.map +4 -4
- package/dist/node-process-worker.cjs +849 -193
- package/dist/node-process-worker.cjs.map +4 -4
- package/dist/node-process-worker.js +849 -193
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +1875 -743
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +805 -149
- package/dist/process-worker.js.map +4 -4
- package/dist/types/client/attach.d.ts +2 -0
- package/dist/types/client/protocol.d.ts +1 -0
- package/dist/types/client/serve-client.d.ts +1 -0
- package/dist/types/devices-relay.d.ts +74 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/kernel/devices.d.ts +155 -0
- package/dist/types/kernel/embedder.d.ts +2 -0
- package/dist/types/kernel/fd-table.d.ts +3 -1
- package/dist/types/kernel/host.d.ts +2 -0
- package/dist/types/kernel/process.d.ts +20 -0
- package/dist/types/kernel/serial-ioctl.d.ts +73 -0
- package/dist/types/launcher.d.ts +4 -0
- package/dist/types/node.d.ts +6 -2
- package/dist/types/process/js/js-dev.d.ts +24 -0
- package/dist/types/process/kernel-streams.d.ts +11 -1
- package/dist/types/process/process-pty.d.ts +1 -0
- package/dist/types/process/wasi/wasi-fds.d.ts +1 -0
- package/dist/types/process/wasi/wasi-termios.d.ts +1 -0
- package/dist/types/serve.d.ts +14 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,6 +45,7 @@ TypeScript declarations ship in `dist/types/`, and each entry (`.`, `./node`, `.
|
|
|
45
45
|
| `sealed` | none | `(req, { signal }) => Promise<object>`, or `{ request, fetch? }` with that function as `request`: `request` answers requests written to [`/dev/sealed`](#devsealed), and `fetch` fills requests that carry a sealed placeholder (see **The fetch hook** under [`/dev/sealed`](#devsealed)). `createNodeKernel` takes it too. An exported `SEALED_FETCH_VERSION`, the number `1`, says that this kernel takes the object |
|
|
46
46
|
| `clipboard` | none | `{ read(stamp) => Promise<string>, write(text, stamp) => Promise<void> }`: the clipboard behind [`/dev/clipboard`](#devclipboard). `createNodeKernel` takes it too |
|
|
47
47
|
| `requests` | none | `{ kinds, handle(request) }`: the page's answers to [`/dev/embedder`](#devembedder) for the processes it starts. `createNodeKernel` takes it too |
|
|
48
|
+
| `devices` | none | `{ open(request) }`: the page's answers for the serial device nodes it adds with [`kernel.devices`](#kerneldevices) (see [Embedder devices](#embedder-devices)). `createNodeKernel` takes it too |
|
|
48
49
|
| `approve` | none | `(req, { signal }) => 'once' \| 'always' \| 'deny'` (or a promise of one), or `{ answer: 'once' \| 'always', timestamp: true }` when a person approved: decides a `sudo` request that needs approval (see [Sudo](#sudo) and [Sudo timestamps](#sudo-timestamps)); without it, such a request is denied. `createNodeKernel` takes it too |
|
|
49
50
|
| `cdp` | none | `({ runtime, stamp }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
|
|
50
51
|
| `facades` | none | `{ link: { port, hook? } }`: kernel-only loopback ports whose connections a hook on the page serves, with the connecting program's identity (see [Page facades](#page-facades)) |
|
|
@@ -95,6 +96,8 @@ Makes a port for another client of the same kernel. Hand it to a dedicated worke
|
|
|
95
96
|
|
|
96
97
|
**`requests`** binds the port's [`/dev/embedder`](#devembedder) requests when the port is made: a hook (`{ kinds, handle }`, as `createKernel`'s) answers for every process the port's client starts, in the embedder's own realm, so a worker client (an agent) never answers or registers requests itself. `'attach'` lets the client that attaches to the port bring its own hook (`attachKernel(port, { requests })`, protocol 1.13); without it, an attach with `requests` fails with `EPERM`. A port with neither answers nothing: its processes' requests are `ENOTSUP`.
|
|
97
98
|
|
|
99
|
+
**`devices`** binds a [devices hook](#embedder-devices) the same way (protocol 1.18): a hook (`{ open }`) answers the opens of the nodes the page adds for the port (`kernel.devices.add({ client: port, … })`), in the embedder's own realm, and `'attach'` lets the client that attaches to the port bring its own (`attachKernel(port, { devices })`); without it, an attach with `devices` fails with `EPERM`, and a node added for a port with no hook is `ENOTSUP`.
|
|
100
|
+
|
|
98
101
|
**`user`** (a name or uid) binds the client to that user; without it the client is root. The binding is a capability: the client's `spawn`, `run` and `openTerminal` run as that user, its `ps` lists only that user's processes, and its `kill` follows POSIX permission. A bound client can't run anything as another user (`code: 'EPERM'`). A root client may pass `user` to `spawn`, `run` or `openTerminal` to run one command as any user. An unknown user rejects with `code: 'ENOENT'`. **`tag`** is the tag (see `kernel.run`) of every process the client starts; its own `spawn`, `run` and `openTerminal` cannot name another. They may add one with their own `tag` option (protocol 1.12; an older kernel answers `ENOSYS`): the process then has both tags, the connection's first, and so does everything it starts. The option may also be a list of at most 8 tags, added in order (protocol 1.15; against an older kernel the client rejects with `code: 'ENOSYS'` before sending anything), so a caller can mark one call's tree with a tag of its own for [`trace`](#clienttracetag-onevents--limit---promise-close-) beside a tag that means something else. A per-spawn tag only adds; no client, program or child can remove or replace a tag ([#1380](https://github.com/ai-ecoverse/slicc-kernel/issues/1380)).
|
|
99
102
|
|
|
100
103
|
### `kernel.sudoRun(request, { stdout, stderr, tag }?) → Promise<{ pid, pgid, ended }>`
|
|
@@ -132,6 +135,23 @@ await kernel.users.add({ name: 'scoop-site', groups: ['cone'], homeGroup: 'cone'
|
|
|
132
135
|
|
|
133
136
|
The cone's home lets its group in only to pass through (`0710`), and the cone opens what it shares with `chmod g+r`. A scoop's home belongs to the cone's group, so the cone reads it, and new files there take that group. Change `/etc/passwd` and `/etc/group` through `kernel.users` and `kernel.groups`, which keep the two consistent and serialize their writes.
|
|
134
137
|
|
|
138
|
+
### `kernel.devices`
|
|
139
|
+
|
|
140
|
+
Serial device nodes the embedder backs, on the page and the Node entry (not on attached clients); see [Embedder devices](#embedder-devices):
|
|
141
|
+
|
|
142
|
+
| | |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| `add({ path, owner?, group?, mode?, links?, client? })` | adds a character device directly under `/dev` and resolves with `{ remove() }`. `path` is `ttyUSB<n>` or `ttyACM<n>` (with or without `/dev/`, `n` from 0 to 255); `owner` and `group` are names or ids (default `0`), `mode` the permission bits (default `0o600`), `links` names of symlinks the kernel makes to the node in `/dev/serial/by-id/` (a bare name or the full path), and `client` the port from `kernel.connect({ devices })` whose hook answers the node's opens (default: the page's own `devices` hook). A name, a link or a client that doesn't fit, or an unknown owner or group, rejects with `EINVAL`; a name or a link that exists with `EEXIST`; a client without a devices hook with `ENOTSUP` |
|
|
145
|
+
| `remove()` | deletes the node and its links. Descriptors still open on it fail every call with `EIO` from then on, and `poll` reports `POLLHUP` and `POLLERR`, as on Linux when a USB serial adapter is unplugged. A second `remove()` does nothing |
|
|
146
|
+
|
|
147
|
+
When the client whose hook answers a node goes away (its port closes), its nodes are removed the same way.
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
const kernel = await createKernel({ devices: { open: ({ path, flags, stamp, signal }) => port(path) } });
|
|
151
|
+
await kernel.groups.add('dialout', { gid: 20 });
|
|
152
|
+
const node = await kernel.devices.add({ path: 'ttyUSB0', owner: 0, group: 'dialout', mode: 0o640, links: ['usb-0403_6001-if00'] });
|
|
153
|
+
```
|
|
154
|
+
|
|
135
155
|
### `kernel.chmod(path, mode) → Promise<void>`
|
|
136
156
|
|
|
137
157
|
Sets a file's permission bits (`0` to `0o7777`, `EINVAL` otherwise) as root, through the mounts, as the processes see them, for a file the page wrote some other way. For files the page writes straight to OPFS before any kernel runs, see `writeMetadata` under [Metadata](#metadata).
|
|
@@ -177,7 +197,7 @@ Neither is on an attached client (`attachKernel`, `connect()`), so no process or
|
|
|
177
197
|
|
|
178
198
|
### `attachKernel(port, options?) → Promise<KernelClient>`
|
|
179
199
|
|
|
180
|
-
Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker, another tab, or Node. `options.timeoutMs` (10 s by default) bounds the handshake: a port with no kernel behind it rejects with `KernelGoneError`. `options.requests` (`{ kinds, handle }`) answers the [`/dev/embedder`](#devembedder) requests of the client's processes, when the port was made with `connect({ requests: 'attach' })`.
|
|
200
|
+
Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker, another tab, or Node. `options.timeoutMs` (10 s by default) bounds the handshake: a port with no kernel behind it rejects with `KernelGoneError`. `options.requests` (`{ kinds, handle }`) answers the [`/dev/embedder`](#devembedder) requests of the client's processes, when the port was made with `connect({ requests: 'attach' })`. `options.devices` (`{ open }`) answers the opens of the [embedder devices](#embedder-devices) the page added for this client (`kernel.devices.add({ client: port })`), when the port was made with `connect({ devices: 'attach' })` (protocol 1.18; an older kernel ignores it).
|
|
181
201
|
|
|
182
202
|
| Client | |
|
|
183
203
|
|---|---|
|
|
@@ -416,7 +436,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
416
436
|
- **Another tab:** a kernel sharing the OPFS root reports its changes too ([#1391](https://github.com/ai-ecoverse/slicc-kernel/issues/1391)): a structural change (create, remove, rename, mkdir) as `'rename'`, a content or metadata change as `'change'`. Their order across a name isn't guaranteed, and a new directory may also come with a `'change'`, so a watcher should treat either as "look again". Another tab's change never ends or renames a watch, and a directory watch doesn't hear one at the directory itself; a file watch hears another tab's write as `'change'`.
|
|
417
437
|
- **Lifetime:** a watch ends with its process and doesn't keep the program alive.
|
|
418
438
|
- **The older watches:** `kernel.watch` is unchanged, and a client's `fs.watch` with `{ paths }` hears another tab's changes too (see [`attachKernel`](#attachkernelport-options--promisekernelclient)).
|
|
419
|
-
- `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`). `/dev/shm` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Removing a `/dev` entry fails (see [`/dev`](#dev-entries)), and so does a new name directly in `/dev` or `/proc`;
|
|
439
|
+
- `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`), and the embedder's serial nodes and their `/dev/serial/by-id` links (see [Embedder devices](#embedder-devices), whose handles have the terminal controls as methods). `/dev/shm` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Removing a `/dev` entry fails (see [`/dev`](#dev-entries)), and so does a new name directly in `/dev` or `/proc`;
|
|
420
440
|
- `/proc`, in `fs`, `open` and their `sync` forms, the same as a WASI program's (see [`/proc`](#proc)): `stat`, `lstat`, `exists`, `readdir`, `readlink` and `readFile` answer it, and `/proc/self` is the program's own pid. It's read-only, so every change in it is `EROFS`: `writeFile`, `mkdir`, `rm`, `unlink`, `chmod`, a `rename`, `link` or `symlink` into or out of it, and an `open` for writing, creating or truncating. A file opened in it is a snapshot read at the open, with no descriptor (`fd` is `-1`): its `read` and `size` answer from the snapshot until it's closed, and `write` and `truncate` are `EBADF`. Other paths pay only a prefix check;
|
|
421
441
|
- `signals`: `on(signal, handler)` catches a signal (by number or as `'SIGINT'`), `ignore(signal)` ignores it and `reset(signal)` restores its default action. A call blocked in a read or write is restarted after a handler runs; signals without a handler keep their default actions, so an uncaught `SIGINT` ends the program with 130 and `SIGKILL` always does;
|
|
422
442
|
- `sync`: `read`, `write`, `close`, `open` (whose files' methods are synchronous too) and `fs`, made synchronously, for code that cannot `await`, such as a compiled program's file system or a `readFileSync`. A synchronous call holds the worker until the kernel answers, so nothing else of the program runs meanwhile, timers and callbacks included; a signal that arrives during a blocked read or write runs its handler, and the call is then restarted. Descriptors are shared with the rest of the context, so a file opened with `sync.open` can be closed with `close`, and the other way around; `sync.run` and `sync.exit` are described next;
|
|
@@ -1046,19 +1066,21 @@ The kernel's device nodes answer the mode and owner an Emscripten program sees f
|
|
|
1046
1066
|
- A pty's `/dev/pts/N` is its owner's, with `0620`.
|
|
1047
1067
|
- `/dev/pts` is a directory listing the open ptys, for Emscripten and WASIX programs as for JS ones, and `/dev` lists it ([#1040](https://github.com/ai-ecoverse/slicc-kernel/issues/1040)); an Emscripten program also stats a pty through a symlink to it.
|
|
1048
1068
|
- `/dev/stdin`, `/dev/stdout`, `/dev/stderr` and `/dev/fd/N` answer for the descriptor they name.
|
|
1069
|
+
- The embedder's serial nodes, `/dev/ttyUSB<n>` and `/dev/ttyACM<n>`, are character devices with the owner, group, mode and major and minor they were added with, and `/dev/serial/by-id` lists their links (see [Embedder devices](#embedder-devices)).
|
|
1049
1070
|
- A symlink elsewhere that leads to one of them, such as `/tmp/console -> /dev/tty`, answers the same when followed ([#1042](https://github.com/ai-ecoverse/slicc-kernel/issues/1042)).
|
|
1050
1071
|
|
|
1051
1072
|
An Emscripten program sees those six character devices at `0666` with `users: 'off'` too.
|
|
1052
1073
|
|
|
1053
1074
|
The owner calls follow [File owners](#file-owners): with `users: 'off'`, `fd_chown` and `path_chown` succeed and change nothing, and the four owner calls answer `ENOTSUP`, "owners not tracked", before looking at anything, so a libc can remember that once and take its own `getuid()` and `getgid()` as every file's owner, which is what an Emscripten program built against the credential calls sees. Devices are root's. A pipe's owner is the caller.
|
|
1054
1075
|
|
|
1055
|
-
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`
|
|
1076
|
+
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 the speed bits of `c_cflag`, `B38400` on a terminal, which ignores them when set, while a [serial node](#embedder-devices) uses them) 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):
|
|
1056
1077
|
|
|
1057
1078
|
| Import | Does |
|
|
1058
1079
|
| --- | --- |
|
|
1059
1080
|
| `tcgetattr(fd, termios_out)` | writes the terminal's termios |
|
|
1060
|
-
| `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 |
|
|
1081
|
+
| `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; on a [serial node](#embedder-devices), `TCSADRAIN` and `TCSAFLUSH` drain the output first |
|
|
1061
1082
|
| `winsize(fd, winsize_out)` | writes `struct winsize` (`u16` rows, columns, then zero pixels) |
|
|
1083
|
+
| `ioctl(fd, request, arg)` | a Linux tty ioctl on a [serial node](#embedder-devices), with `arg` the value or the address Linux takes: `TCGETS`, `TCSETS*` (musl's 60-byte termios), `TCGETS2`, `TCSETS2*` (Linux's 44-byte `struct termios2`), `TIOCMGET`, `TIOCMSET`, `TIOCMBIS`, `TIOCMBIC`, `TIOCSBRK`, `TIOCCBRK`, `TCSBRK`, `TCSBRKP`, `TCFLSH`, `FIONREAD`, `TIOCOUTQ`, `TIOCGWINSZ`, `TIOCSWINSZ`, `TIOCEXCL` and `TIOCNXCL`; `TIOCSCTTY` is `EPERM` and any other request `ENOTTY`. On another descriptor it answers `FIONREAD` (the bytes ready to read) and `ENOTTY` for the rest. A wasix-sysroot routes its `ioctl` there for the requests above |
|
|
1062
1084
|
|
|
1063
1085
|
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.
|
|
1064
1086
|
|
|
@@ -1304,6 +1326,33 @@ printf '{"kind":"open.url","url":"%s"}\n' "$url" | cat > /dev/embedder
|
|
|
1304
1326
|
- **Limits:** at most 16 open requests per process (`EMFILE`) and 256 MiB of request windows per kernel, 128 requests at 2 MiB each (`EAGAIN` at `open()` beyond); a closed request frees its share.
|
|
1305
1327
|
- `/dev/clipboard` stays as it is, text only. Image clipboards are kinds on this channel (`clipboard.read`, `clipboard.write`), answered by the embedder.
|
|
1306
1328
|
|
|
1329
|
+
### Embedder devices
|
|
1330
|
+
|
|
1331
|
+
Serial ports the page can reach (Web Serial, in seven) appear as Linux character devices, `/dev/ttyUSB<n>` and `/dev/ttyACM<n>`, so `cat`, `stty`, `screen`, `picocom` and pyserial use them unchanged. The page adds them with [`kernel.devices.add`](#kerneldevices); a program can't create or remove them (see [`/dev` entries](#dev-entries)).
|
|
1332
|
+
|
|
1333
|
+
- **The node:** a character device with the owner, group and mode it was added with, major 188 for `ttyUSB` and 166 for `ttyACM` and minor `n`, as on Linux, so `ls -l /dev/ttyUSB0` shows `crw-r----- 1 root dialout 188, 0`. Its links are symlinks `/dev/serial/by-id/<name> -> ../../ttyUSB0`. Every process of the kernel sees them, in every runtime (Emscripten, WASI and WASIX, JS), in `stat`, `readdir` and `readlink`, whatever its session.
|
|
1334
|
+
- **Opening:** the normal permission check applies: reading needs `r` and `O_WRONLY` or `O_RDWR` need `w`, so with `root:dialout 0640` a `dialout` member reads and only root writes (an agent writes with `sudo`); a refusal is `EACCES` and goes to `onDenial`. Then the kernel calls the hook that answers the node, `open({ path, flags, stamp, signal })`, with `flags` the access mode (0, 1 or 2) and `stamp` the opener's `{ pid, uid, gid, groups, users, tag?, tags? }`, as for [`/dev/embedder`](#devembedder). It resolves with `{ readable, writable, control(op, args) }`: a byte `ReadableStream`, a byte `WritableStream` and a function that may return a promise. Its error fails the open, mapped as for `/dev/embedder` (`NotAllowedError` `EACCES`, `NotSupportedError` `ENOTSUP`, `NotFoundError` `ENOENT`, anything else `EIO`); a signal while it waits fails the open with `EINTR` and aborts `signal`. A node has one open file description at a time: a second open is `EBUSY` until the first is closed everywhere (dups, forks and inherited descriptors share the first), as Web Serial ports behave and as `TIOCEXCL`, which pyserial sets, asks. Closing the last descriptor waits up to 30 s for the output to drain (Linux's `closing_wait`), then cancels both streams and releases their locks, and only then calls `control('close')`, so the hook can close a Web Serial port, whose `close()` rejects while its streams are locked.
|
|
1335
|
+
- **Read and write:** the kernel reads `readable` into its own buffer, at most 64 KiB ahead of the program, and reads block, return what is there, honour `O_NONBLOCK` (`EAGAIN`) and wake `poll`, `select`, epoll and `FIONREAD` (`TIOCINQ`), as for a terminal. A write waits while `writable` applies backpressure (`EAGAIN` with `O_NONBLOCK`) and returns once its bytes are queued there. The end of `readable` is a hangup: reads return 0, writes fail with `EIO` and `poll` reports `POLLHUP`; an error of either stream is `EIO`. A signal interrupts a blocked call with `EINTR`, as for pipes.
|
|
1336
|
+
- **Terminal settings:** the node is a terminal (`isatty` is true, `ttyname` names it) with its own termios, which starts as Linux's for a USB serial port made raw (`cfmakeraw`): `B9600`, `CS8`, no parity, one stop bit, no flow control (`CREAD`, `HUPCL` and `CLOCAL` set), `VMIN` 1. The settings stay with the node across opens, so `stty -F /dev/ttyUSB0 115200` holds for the next program, as on Linux, and an open whose saved settings differ from that start configures the port before it returns. `TCGETS`, `TCSETS`, `TCSETSW` (drains output first) and `TCSETSF` (also discards input), and `TCGETS2`/`TCSETS2`/`TCSETSW2`/`TCSETSF2` with `BOTHER` and `c_ispeed`/`c_ospeed` for a custom speed, take every flag and `c_cc` as given and keep them. A change to the speed (`B50` to `B4000000`, from `cfsetispeed`/`cfsetospeed` or `BOTHER`), `CSIZE`, `PARENB`/`PARODD`, `CSTOPB` or `CRTSCTS` calls `control('configure', { baudRate, dataBits, parity, stopBits, flowControl })` once and waits for it (`parity` `'none'`, `'even'` or `'odd'`, `stopBits` 1 or 2, `flowControl` `'none'` or `'hardware'`); its error fails the call and leaves the old settings. `B0` drops DTR and RTS instead, as on Linux, and the line stays hung up: an open whose saved speed is `B0` calls `control('setSignals', { dataTerminalReady: false, requestToSend: false })` rather than `configure`, and the first setting with a real speed configures the port and raises DTR and RTS again. The rest of the line discipline is the kernel's pseudo-terminal one: with `ICANON`, `ECHO`, `ICRNL` or `OPOST`/`ONLCR` set, the node edits lines, echoes to the port and translates as a pty does. A node is never a controlling terminal: `O_NOCTTY` is implied, `TIOCSCTTY` is `EPERM`, `TIOCGPGRP` and `TIOCSPGRP` are `ENOTTY`, and a `VINTR` byte with `ISIG` set signals nobody. `TIOCGWINSZ` answers 0 × 0 until a `TIOCSWINSZ`.
|
|
1337
|
+
- **Owner, mode and times:** an Emscripten program's `chmod`, `chown` and `utimensat` of a node (by its name or through its link), and `fchmod`, `fchown` and `futimens` on an open one, change the node itself under the rules for any file: the mode is the owner's or root's to change, the owner only root's, the group the owner's to change to one of its own groups, and the times the owner's or root's (setting them to now also any user who may write it); anyone else gets `EPERM` (`EACCES` for now without write permission), reported to `onDenial`. The change shows in `stat` in every runtime and lasts until the node is removed. With `users: 'off'` nothing is checked.
|
|
1338
|
+
- **Modem lines:** `TIOCMGET` asks `control('signals')`, which resolves with `{ clearToSend, dataCarrierDetect, dataSetReady, ringIndicator }` (`TIOCM_CTS`, `TIOCM_CD`, `TIOCM_DSR`, `TIOCM_RI`), and adds the DTR and RTS the kernel last set (both on at the first open, as Linux raises them). `TIOCMSET`, `TIOCMBIS` and `TIOCMBIC` call `control('setSignals', { dataTerminalReady, requestToSend })`.
|
|
1339
|
+
- **Break, flush and drain:** `TIOCSBRK` and `TIOCCBRK` call `control('setSignals', { break: true })` and `{ break: false }`; `TCSBRK` with 0 (`tcsendbreak`) drains, then sends a 250 ms break, and `TCSBRKP` one of `arg` × 100 ms. `TCSBRK` with any other value (`tcdrain`) waits until `writable` has taken everything written. `TCFLSH` (`tcflush`) with `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` discards the kernel's own input buffer for the first and third and calls `control('flush', { input, output })`. `TIOCOUTQ` answers the bytes `writable` hasn't taken yet.
|
|
1340
|
+
- **Other ioctls:** `TIOCEXCL` and `TIOCNXCL` are accepted (the node is exclusive anyway); `FIONBIO`, `FIONCLEX`, `FIOCLEX` and `FIOASYNC` go to the runtime's own handling, as for any file. Any other ioctl is `ENOTTY`, as Linux answers for an unknown tty ioctl.
|
|
1341
|
+
- **Runtimes:** an Emscripten program makes these calls with `ioctl`, `tcgetattr`/`tcsetattr`, `tcdrain`, `tcflush` and `tcsendbreak` as on Linux. A WASIX program has them through `slicc_tty` (see [WASI programs](#wasi-programs)): `tcgetattr` and `tcsetattr` (`TCSADRAIN` and `TCSAFLUSH` drain first) and `ioctl(fd, request, arg)` for the rest. A JS program's `ctx.open` of a node (and `ctx.sync.open`) returns a handle with the controls as methods:
|
|
1342
|
+
|
|
1343
|
+
| method | does |
|
|
1344
|
+
| --- | --- |
|
|
1345
|
+
| `termios()` | the node's settings, `{ c_iflag, c_oflag, c_cflag, c_lflag, c_cc, ispeed, ospeed }` (Linux values, speeds in baud) |
|
|
1346
|
+
| `setTermios(termios, when?)` | sets them, speeds from `ispeed` and `ospeed` (any value, as `BOTHER` would), `when` `'now'` (the default), `'drain'` or `'flush'`, as `TCSETS2`, `TCSETSW2` and `TCSETSF2` |
|
|
1347
|
+
| `modem()` | `{ dtr, rts, cts, dsr, cd, ri }`, as `TIOCMGET` |
|
|
1348
|
+
| `setModem({ dtr?, rts? })` | sets the lines given, as `TIOCMBIS` and `TIOCMBIC` |
|
|
1349
|
+
| `setBreak(on)`, `sendBreak()` | `TIOCSBRK`/`TIOCCBRK`, and a 250 ms break after draining |
|
|
1350
|
+
| `drain()` | waits until the port has taken everything written |
|
|
1351
|
+
| `flush(queue?)` | discards `'input'`, `'output'` or `'both'` (the default), as `tcflush` |
|
|
1352
|
+
|
|
1353
|
+
`ctx.sync.open` has the same methods, synchronous. A closed handle's methods fail with `EBADF`.
|
|
1354
|
+
- **Not there yet:** `/sys/class/tty`, through which pyserial's `list_ports` and Node `serialport`'s list find ports, and `TIOCGSERIAL`.
|
|
1355
|
+
|
|
1307
1356
|
### Protected paths
|
|
1308
1357
|
|
|
1309
1358
|
`createKernel({ protect: { paths: ['/node_modules', '/os'] } })` makes those directories read-only for every process, root included, in every `users` mode. They hold code every later process runs, which only the embedder writes, straight to OPFS.
|
|
@@ -1339,7 +1388,7 @@ printf '{"kind":"open.url","url":"%s"}\n' "$url" | cat > /dev/embedder
|
|
|
1339
1388
|
|
|
1340
1389
|
### `/dev` entries
|
|
1341
1390
|
|
|
1342
|
-
`/dev` and `/proc` hold the kernel's own entries, and a program can't add or remove names there. Only `/dev/shm`, a tmpfs, takes new files.
|
|
1391
|
+
`/dev` and `/proc` hold the kernel's own entries, and a program can't add or remove names there. Only `/dev/shm`, a tmpfs, takes new files, and only the embedder adds names: its serial nodes and their `/dev/serial/by-id` links (see [Embedder devices](#embedder-devices)), which a program can't remove either.
|
|
1343
1392
|
- **Removing** an entry fails and leaves it: `EACCES` for a user, `EPERM` for root on a device, `EBUSY` for `/dev` itself.
|
|
1344
1393
|
- **Creating** a name that doesn't exist fails in every runtime, whether it's a file, a directory, a symlink, a device node, or the new name of a link or a rename: `EACCES` for a user, `EPERM` for root in `/dev`, and `EROFS` in `/proc`.
|
|
1345
1394
|
- **Why:** an Emscripten program's `/dev` and `/proc` live in its glue's own memory, so such a file used to look written and then vanish when the process exited. On the root file system no other runtime would see it either.
|