@ai-ecoverse/slicc-kernel 1.59.0 → 1.59.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +25 -7
- package/dist/index.js.map +2 -2
- package/dist/kernel-worker.js +7 -2
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +1 -1
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +103 -24
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +1 -1
- package/dist/process-worker.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
231
231
|
- `spawn({ argv, env, cwd, stdin, stdout, stderr })` 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. A command that does not exist rejects with `ENOENT`;
|
|
232
232
|
- `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);
|
|
233
233
|
- `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`;
|
|
234
|
-
- `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
|
|
234
|
+
- `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`. One that cannot connect rejects with the `code` its error carries (an errno, such as `EAI_NONAME`, `ECONNREFUSED`, `ETIMEDOUT` or `EPROTO`), else `ECONNREFUSED`. `ws://host.slicc.internal:port` reaches the real machine's loopback, as it does through the proxy;
|
|
235
235
|
- `fetch(input, init)`, the web's `fetch` over the kernel's transport, the one the proxy uses, so the embedder's transport, its WebSocket and proxy settings and the kernel's routes apply. The response body streams as the program reads it. The request body is read whole first, since a transport takes it whole (streaming it is [#238](https://github.com/ai-ecoverse/slicc-kernel/issues/238)), and one larger than the transport's `maxRequestBody` is answered `413`, as through the proxy. A signal aborts the request at any stage. Redirects follow the fetch rules: at most 20, a 301 or 302 after a `POST` and a 303 turn into a `GET` without a body, `Authorization` is dropped across origins, and `redirect: 'manual'` returns the redirect itself. On a transport that follows redirects on its own (`fetchTransport()` in a browser) only `'follow'` works: `'manual'` and `'error'` reject with a `TypeError` whose `cause.code` is `ENOTSUP`, since such a transport never shows the program a redirect. A status the transport chose comes back as a response whose body is the reason. An unreachable host, or no transport at all, rejects with a `TypeError` whose `cause.code` is `ECONNREFUSED` or `ENETUNREACH`, as Node's `fetch` does. A `data:` URL is answered by `fetch` itself, as the fetch standard decodes it, without a transport: for any method, a `HEAD` without a body, and its type parsed and serialized as a MIME type (lowercase, no spaces, invalid parameters dropped), as Node's `fetch` answers; any other scheme than `http:` and `https:` rejects with a `TypeError` whose `cause.code` is `EPROTONOSUPPORT`, at the start as for a redirect target. The global `fetch` stays withheld;
|
|
236
236
|
- `tty`, for programs on a terminal: `getAttr(fd)` and `setAttr(fd, termios)` read and set its termios (`{ c_iflag, c_oflag, c_cflag, c_lflag, c_cc }`, Linux values), `setRaw(fd, raw)` switches raw mode on as libuv (and Node's `setRawMode`) does (no echo, no line editing, no `^C` signal, each key read as it comes) and off again to the termios it had before (per terminal: after raw mode on fds 0 and 1 of one terminal, turning it off on either restores the termios from before the first), `size(fd)` answers `{ columns, rows }`, and `foreground(fd)` and `setForeground(fd, pgrp)` get and set the foreground process group, under the same job-control rules as other programs. A window resize arrives as `SIGWINCH` through `signals`. A terminal the program left raw is set back when the program ends, whether it returns, exits or throws (a program killed by a signal cannot do so). On a descriptor that is not a terminal, each call fails with `ENOTTY`;
|
|
237
237
|
- `exit(status)`, which ends the program once its pending writes are done;
|
|
@@ -259,7 +259,7 @@ await kernel.run(['curl', '-sS', 'https://registry.npmjs.org/@ai-ecoverse/wasm-b
|
|
|
259
259
|
- **TLS** needs [`@ai-ecoverse/wasm-tls-engine`](https://www.npmjs.com/package/@ai-ecoverse/wasm-tls-engine) installed under `modules`, like a command. Without it, `CONNECT` to port 443 is answered `501` with the reason. A tunnel to another port is accepted, and what the program sends first decides: plain HTTP is served as `http://host:port`, without TLS, and a TLS handshake is terminated as above, or the tunnel closes when it cannot be.
|
|
260
260
|
- **The transport** is an object on the page with `traits` (`manualRedirects`, `encodedBodies`, `maxRequestBody`, and `crossOrigin`: `'cors'` or `'any'`) and `fetch(request)`, which answers `{ status, statusText, headers, body, cancel }` with `body` an async iterable of `Uint8Array`, and optionally `url` and `redirected: true` when it followed redirects itself (`fetchTransport()` does), so a JS program's `fetch` reports where it ended. The kernel pulls the body one chunk at a time, so a slow program slows the download. A rejection with a numeric `status` is answered with that status; any other with `502`. `fetchTransport()` is the plain `fetch` of the page, bound by CORS; its `hint` option is appended to the message of a request it could not make, so a program's 502 can name a way around CORS (`registry.npmjs.org` and jsDelivr allow it); an embedder with a way around CORS passes its own. A response body that delivers no data for `bodyIdleMs` (default 5 minutes; `0` or `Infinity` turns it off) fails with `ETIMEDOUT` instead of waiting forever, which matters because Chrome under heavy load can stop handing a page the rest of a body. A slow body that keeps delivering is never cut off.
|
|
261
261
|
- **Redirect hops** ([#302](https://github.com/ai-ecoverse/slicc-kernel/issues/302)): a transport that follows redirects but can see them (a browser extension's `webRequest.onBeforeRedirect` can) sets `traits.redirectHops` and answers `redirects: [{ status, url, headers }]`, one entry per 3xx it followed, in order (`NetworkRedirectHop`). The kernel then treats it as one that does not follow. Programs get each 3xx as it was answered: through the proxy, curl, requests and `hf` see the real redirect, `X-Repo-Commit` and `ETag` included, and `ctx.fetch` with `redirect: 'manual'` answers it. The rest of the chain is held for the process that asked, at most 30 s and 8 chains per process (64 in all), with the final body unread. A follow-up from that process, with the method the redirect leads to and the same `Range`, `If-None-Match`, `If-Modified-Since`, `If-Range` and `Accept-Encoding` (and an `Authorization` if the first had one), is answered from it without a second request; any other request goes to the transport. A chain that expires or is pushed out is cancelled, and so is one whose follow-up arrives and does not match. After a redirect to another origin the follow-up is expected without `Authorization`, which HTTP clients drop when they follow one; a `Location` matches without its `#fragment`. The page's own `fetch` cannot show a redirect, so `fetchTransport()` in a page stays a transport that follows, and `redirect: 'manual'` fails there as before. `fetchTransport({ redirect: 'manual' })` hands redirects back unfollowed where the platform's `fetch` shows them (Node; a browser answers an opaque status 0), and `nodeTransport()` does so. A kernel socket also records who connected it (`SO_PEERCRED`): the connecting process's pid, uid and gid on the accepting side, and the listener's on the connecting side, kept when the socket passes to another process. The proxy uses this to keep each process's held chains its own.
|
|
262
|
-
- **WebSockets:** a `GET` with `Upgrade: websocket`, in absolute form or inside a tunnel, is bridged to the transport's optional `websocket({ url, protocols, headers, signal })`. It answers `{ protocol, buffered, send(data), messages, close(code, reason), closed }`, with `messages` an async iterable of strings and `Uint8Array`s and `closed` resolving with `{ code, reason }`; a transport that has it says so with `traits.websocket`. The program gets `101 Switching Protocols` with the far end's protocol, then its frames go out as messages and the far end's come back as frames. Pings are answered by the proxy, and close codes pass both ways (a code that may not go on the wire becomes 1000 or 1011). A message over 64 MiB, or more than 16 MiB of payload from the far end waiting for the program, closes with 1009. The proxy stops reading the program while 1 MiB of its messages wait to be sent. A refused connection is `502` with the reason. A transport without WebSockets answers `501` with a message naming the option below, and the kernel logs a warning once, so a program never hangs. `fetchTransport({ webSocket: true })` opens them with the page's `WebSocket` (or pass a constructor); they are off by default because a browser WebSocket carries the user's cookies for the target site, which `fetch` here never sends (`credentials: 'omit'`), so a program could reach a site as the user. A page also cannot send headers on a WebSocket, and an `https:` page cannot open `ws:` to anything but loopback, which is a `502` that says so. `nodeTransport()` has WebSockets on (Node keeps no cookies) and sends the request's headers. The credential-free path for a page is the coming slicc-node relay (`localProxyTransport()`, phase 2 of [#204](https://github.com/ai-ecoverse/slicc-kernel/issues/204)), which has no WebSockets yet.
|
|
262
|
+
- **WebSockets:** a `GET` with `Upgrade: websocket`, in absolute form or inside a tunnel, is bridged to the transport's optional `websocket({ url, protocols, headers, signal })`. It answers `{ protocol, buffered, send(data), messages, close(code, reason), closed }`, with `messages` an async iterable of strings and `Uint8Array`s and `closed` resolving with `{ code, reason }`; a transport that has it says so with `traits.websocket`. The program gets `101 Switching Protocols` with the far end's protocol, then its frames go out as messages and the far end's come back as frames. Pings are answered by the proxy, and close codes pass both ways (a code that may not go on the wire becomes 1000 or 1011). A message over 64 MiB, or more than 16 MiB of payload from the far end waiting for the program, closes with 1009. The proxy stops reading the program while 1 MiB of its messages wait to be sent. A refused connection is `502` with the reason. A transport without WebSockets answers `501` with a message naming the option below, and the kernel logs a warning once, so a program never hangs. `fetchTransport({ webSocket: true })` opens them with the page's `WebSocket` (or pass a constructor); they are off by default because a browser WebSocket carries the user's cookies for the target site, which `fetch` here never sends (`credentials: 'omit'`), so a program could reach a site as the user. A page also cannot send headers on a WebSocket, and an `https:` page cannot open `ws:` to anything but loopback, which is a `502` that says so. `nodeTransport()` has WebSockets on (Node keeps no cookies) and sends the request's headers. An open fails as soon as Node's `WebSocket` reports an error, also on Node 22, which never follows the error with a close. A transport's `websocket()` may reject with an `Error` whose `code` is an errno, and the kernel hands that on to the program. Node's `WebSocket` doesn't say why an open failed, so when one fails, `nodeTransport()` finds out: it looks up the host's name (`EAI_NONAME` when that fails), then makes one TCP connection to the target, or a TLS one for `wss:`, for at most 3 s, and closes it at once. A refusal there is `ECONNREFUSED`, an unreachable host `EHOSTUNREACH` or `ENETUNREACH`, no answer `ETIMEDOUT`, and a TLS failure (a bad certificate) or a server that takes the connection but refused the upgrade `EPROTO`. A server that refused the upgrade therefore sees a second connection. The diagnosis never runs when an open succeeds, and never in a page (`fetchTransport()`), whose failed opens stay `ECONNREFUSED`. The credential-free path for a page is the coming slicc-node relay (`localProxyTransport()`, phase 2 of [#204](https://github.com/ai-ecoverse/slicc-kernel/issues/204)), which has no WebSockets yet.
|
|
263
263
|
- **`localProxyTransport({ url, key })`** sends every request to a local proxy on loopback, such as [slicc-node](https://github.com/ai-ecoverse/slicc-node) or slicc-swift, which fetches it without CORS. Redirects reach the program unfollowed, with every `Set-Cookie`; bodies arrive decoded. `probeLocalProxy({ url, key })` resolves with the proxy's capabilities, or `null` when it is gone or refuses the key, so a page can fall back to `fetchTransport()`. `checkLocalProxy({ url, key })` says why: it resolves with `{ state }`, which is `ready` (with `probe`), `blocked` (the user denied Chrome's Local Network Access permission, `loopback-network`), `unreachable` (nothing answered; its `permission` is `granted`, `prompt` or `unknown`, and with `prompt` the page cannot tell a dismissed prompt from a proxy that is not running, since Chrome asks only once it has a connection), `refused` (with `status` and `error`, such as a stale key) or `incompatible`. The permission is not consulted when the page itself is on loopback. Both take a `fetch` option, and the transport `maxRequestBody` (default 64 MiB) and `bodyIdleMs` like `fetchTransport`.
|
|
264
264
|
- **The host machine** is `host.slicc.internal` (`SLICC_HOST_LOOPBACK`), as `host.docker.internal` is in Docker, while `localhost` and `127.0.0.1` stay the kernel's own loopback. The proxy forwards `http(s)://host.slicc.internal:<port>/…` to the transport as `http(s)://127.0.0.1:<port>/…`, so it reaches the real machine's loopback through the local proxy or Node's `fetch` (through the page's own `fetch`, a request needs CORS from that server and Chrome's Local Network Access permission); other loopback addresses are still refused. This is HTTP and HTTPS through the proxy only: `/etc/hosts` and WASIX name resolution give the name `10.0.2.2`, QEMU's address for its host, so a program that connects a socket to it directly fails with `ENETUNREACH` instead of reaching the kernel's loopback.
|
|
265
265
|
- **Without a transport**, every request is answered `502` with `slicc-kernel: no network transport (createKernel({ network: { transport } }))`.
|
package/dist/index.js
CHANGED
|
@@ -604,6 +604,11 @@ function blockedReason(url, e) {
|
|
|
604
604
|
}
|
|
605
605
|
return `${url}: ${e instanceof Error ? e.message : String(e)}`;
|
|
606
606
|
}
|
|
607
|
+
function unopened(url, code) {
|
|
608
|
+
return Object.assign(new Error(`${url}: the WebSocket closed (${code}) before it opened`), {
|
|
609
|
+
status: 502
|
|
610
|
+
});
|
|
611
|
+
}
|
|
607
612
|
function openWebSocket(Socket, request, withHeaders) {
|
|
608
613
|
const { url, protocols, signal } = request;
|
|
609
614
|
signal.throwIfAborted();
|
|
@@ -617,13 +622,22 @@ function openWebSocket(Socket, request, withHeaders) {
|
|
|
617
622
|
const queue = new MessageQueue(() => socket.close(1e3, "too many waiting messages"));
|
|
618
623
|
return new Promise((resolve, reject) => {
|
|
619
624
|
let open = false;
|
|
625
|
+
let settled = false;
|
|
626
|
+
const fail = (error) => {
|
|
627
|
+
if (settled) return;
|
|
628
|
+
settled = true;
|
|
629
|
+
signal.removeEventListener("abort", abort);
|
|
630
|
+
reject(error);
|
|
631
|
+
};
|
|
620
632
|
const abort = () => {
|
|
621
633
|
socket.close(1e3, "aborted");
|
|
622
|
-
|
|
634
|
+
fail(signal.reason);
|
|
623
635
|
};
|
|
624
636
|
signal.addEventListener("abort", abort, { once: true });
|
|
625
637
|
socket.addEventListener("open", () => {
|
|
638
|
+
if (settled) return;
|
|
626
639
|
open = true;
|
|
640
|
+
settled = true;
|
|
627
641
|
signal.removeEventListener("abort", abort);
|
|
628
642
|
resolve({
|
|
629
643
|
protocol: socket.protocol,
|
|
@@ -639,15 +653,19 @@ function openWebSocket(Socket, request, withHeaders) {
|
|
|
639
653
|
socket.addEventListener("message", (event) => {
|
|
640
654
|
queue.push(typeof event.data === "string" ? event.data : new Uint8Array(event.data));
|
|
641
655
|
});
|
|
656
|
+
socket.addEventListener("error", () => {
|
|
657
|
+
if (open || settled) return;
|
|
658
|
+
try {
|
|
659
|
+
socket.close();
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
queue.end({ code: 1006, reason: "" });
|
|
663
|
+
fail(unopened(url, 1006));
|
|
664
|
+
});
|
|
642
665
|
socket.addEventListener("close", (event) => {
|
|
643
666
|
queue.end({ code: event.code, reason: event.reason });
|
|
644
667
|
if (open) return;
|
|
645
|
-
|
|
646
|
-
reject(
|
|
647
|
-
Object.assign(new Error(`${url}: the WebSocket closed (${event.code}) before it opened`), {
|
|
648
|
-
status: 502
|
|
649
|
-
})
|
|
650
|
-
);
|
|
668
|
+
fail(unopened(url, event.code));
|
|
651
669
|
});
|
|
652
670
|
});
|
|
653
671
|
}
|