@ai-ecoverse/slicc-kernel 1.18.1 → 1.20.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 +13 -3
- package/dist/driver-worker.js +254 -5
- package/dist/driver-worker.js.map +4 -4
- package/dist/index.js +280 -6
- package/dist/index.js.map +4 -4
- package/dist/kernel-worker.js +1900 -1768
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-driver-worker.js +254 -5
- package/dist/node-driver-worker.js.map +4 -4
- package/dist/node-process-worker.js +23 -0
- package/dist/node-process-worker.js.map +3 -3
- package/dist/node.js +5066 -4692
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +23 -0
- package/dist/process-worker.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,13 +76,22 @@ Stops the kernel worker and every process. Pending and later calls reject.
|
|
|
76
76
|
|
|
77
77
|
Makes a port for another client of the same kernel. Hand it to a dedicated worker, or through a SharedWorker to another tab: it can be transferred any number of times. All clients share one process table, so a terminal on the page can `ps` and `kill` what a worker started.
|
|
78
78
|
|
|
79
|
+
### `kernel.dial({ port, host? })` and `kernel.loopbackFetch(input, { port, host?, signal? })`
|
|
80
|
+
|
|
81
|
+
Reach a server a program runs on the kernel's loopback (a dev server, `python -m http.server`) from the page, a service worker or an attached client, which have both calls too (protocol 1.4).
|
|
82
|
+
|
|
83
|
+
- **`dial`** connects to `127.0.0.1:<port>` inside the kernel (`host` may also be `localhost` or `::1`) and resolves with `{ readable, writable, close() }`: a `ReadableStream` and a `WritableStream` of bytes. `writable.close()` half-closes the connection, as `shutdown(SHUT_WR)` does, and `close()` closes it. Each connection has a `MessagePort` of its own, and an attached client's connections close when it detaches; the other end then reads `ECONNRESET`.
|
|
84
|
+
- **`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`).
|
|
85
|
+
- **Errors** reject with an `Error` whose `code` is the errno: `ECONNREFUSED` when nothing listens on the port, `ENETUNREACH` for a host that is not the loopback, `EINVAL` for a port outside 1–65535.
|
|
86
|
+
- **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.
|
|
87
|
+
|
|
79
88
|
### `attachKernel(port, options?) → Promise<KernelClient>`
|
|
80
89
|
|
|
81
90
|
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`.
|
|
82
91
|
|
|
83
92
|
| Client | |
|
|
84
93
|
|---|---|
|
|
85
|
-
| `spawn(argv, { cwd, env, stdin, 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; a program that cannot start rejects with `code: 'ENOENT'` |
|
|
94
|
+
| `spawn(argv, { cwd, 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'` |
|
|
86
95
|
| `run(argv, options?)` | `spawn` and wait: `{ pid, status, stdout, stderr }` as text, with `onStdout`/`onStderr` streaming text |
|
|
87
96
|
| `openTerminal(argv, options?)` | a pty session, as `kernel.openTerminal` |
|
|
88
97
|
| `ps()` | the kernel's processes: `{ pid, ppid, pgid, sid, argv, tty, started, state }`, `state` being `'S'` or `'Z'` |
|
|
@@ -93,7 +102,7 @@ Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker
|
|
|
93
102
|
| `close({ kill })` | detaches: the client's processes keep running unless `kill` is set, which ends their process groups with `SIGKILL`; its terminals are hung up |
|
|
94
103
|
| `closed` | resolves with the error that ended the client |
|
|
95
104
|
|
|
96
|
-
When the kernel goes away (the page closed or reloaded, `terminate()`), pending calls and `exited` reject with `KernelGoneError`, and so do later calls. Each side holds a Web Lock and waits on the other's, since a `MessagePort` reports no close in browsers; in Node, the port's `close` event does the same. The first message is a handshake on the protocol version, `1.
|
|
105
|
+
When the kernel goes away (the page closed or reloaded, `terminate()`), pending calls and `exited` reject with `KernelGoneError`, and so do later calls. Each side holds a Web Lock and waits on the other's, since a `MessagePort` reports no close in browsers; in Node, the port's `close` event does the same. The first message is a handshake on the protocol version, `1.4` (`1.0` had no `watch`, `1.1` no mounts, `1.2` no `spawn` into a group and `1.3` no `dial`, which reject with `code: 'ENOSYS'` on such a kernel): a client or kernel of another major version is refused with an error naming both.
|
|
97
106
|
|
|
98
107
|
### Headless in Node, for tests
|
|
99
108
|
|
|
@@ -164,6 +173,7 @@ await kernel.run(['curl', '-sS', 'https://registry.npmjs.org/@ai-ecoverse/wasm-b
|
|
|
164
173
|
- **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` is answered `501` with the reason.
|
|
165
174
|
- **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`. 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.
|
|
166
175
|
- **`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`.
|
|
176
|
+
- **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.
|
|
167
177
|
- **Without a transport**, every request is answered `502` with `slicc-kernel: no network transport (createKernel({ network: { transport } }))`.
|
|
168
178
|
|
|
169
179
|
Process workers can also use the transport directly, without a socket: the `net-request`, `net-read` and `net-close` syscalls (`Module.sliccKernel.http` in a process) open a request and read its body in pieces. Requests are per process and closed when it exits, and a request body has no size limit of its own (the transport's `maxRequestBody` applies to the proxy). `net-traits` answers the transport's `traits` with `crossOrigin`: `'cors'` when it is bound by CORS like `fetchTransport()`, `'any'` (the default for a transport that does not say) when it is not; without a transport it fails with `ENETUNREACH`.
|
|
@@ -176,7 +186,7 @@ The page and the proxy speak raw mode of SLICC's `/api/fetch-proxy`. Every reque
|
|
|
176
186
|
- `X-Slicc-Raw-Request: <json>`, the head to send upstream: `{ "url", "method", "headers": [[name, value], …] }`, with the headers in order and repeats kept apart. Characters past U+007E are `\u`-escaped so the value is a valid header.
|
|
177
187
|
- The request body, if any, as the hop's body.
|
|
178
188
|
|
|
179
|
-
The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a big-endian `u32` length, that many bytes of UTF-8 JSON `{ "status", "statusText", "headers": [[name, value], …], "url" }`, then the upstream body until the end of the hop. Any other answer is a refusal with a JSON `{ "error" }` body and `X-Proxy-Error: 1`; programs see its status (`400` malformed head, `403` wrong origin or key, `413` body too large, `502` upstream unreachable). A probe is a `POST` with the key and `X-Slicc-Raw-Probe: 1` instead of a head; it is answered with JSON `{ "rawFetch": 1, "requestBodyStreaming", "maxRequestBodyBytes" }` and fetches nothing. The server side (origin allowlist, CORS, the Private Network Access preflight) is specified in [slicc-node's README](https://github.com/ai-ecoverse/slicc-node#protocol).
|
|
189
|
+
The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a big-endian `u32` length, that many bytes of UTF-8 JSON `{ "status", "statusText", "headers": [[name, value], …], "url" }`, then the upstream body until the end of the hop. Any other answer is a refusal with a JSON `{ "error" }` body and `X-Proxy-Error: 1`; programs see its status (`400` malformed head, `403` wrong origin or key, `413` body too large, `502` upstream unreachable). A probe is a `POST` with the key and `X-Slicc-Raw-Probe: 1` instead of a head; it is answered with JSON `{ "rawFetch": 1, "requestBodyStreaming", "maxRequestBodyBytes" }` and fetches nothing. A proxy that tunnels to the kernel for `<port>.kernel.localhost` adds `"kernelTunnel"` and `"kernelPort"`, which `checkLocalProxy` and `probeLocalProxy` pass on when they are numbers. The server side (origin allowlist, CORS, the Private Network Access preflight) is specified in [slicc-node's README](https://github.com/ai-ecoverse/slicc-node#protocol).
|
|
180
190
|
|
|
181
191
|
## Filesystem
|
|
182
192
|
|
package/dist/driver-worker.js
CHANGED
|
@@ -110,8 +110,247 @@ var RemoteTransport = class {
|
|
|
110
110
|
}
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
// src/client/dial-stream.ts
|
|
114
|
+
function errorWithCode(message) {
|
|
115
|
+
const code = /^E[A-Z]+/.exec(message)?.[0];
|
|
116
|
+
return Object.assign(new Error(message), code ? { code } : {});
|
|
117
|
+
}
|
|
118
|
+
function dialStream(port, onClose) {
|
|
119
|
+
let closed = false;
|
|
120
|
+
let reader;
|
|
121
|
+
const acks = [];
|
|
122
|
+
const readable = new ReadableStream({
|
|
123
|
+
start(controller) {
|
|
124
|
+
reader = controller;
|
|
125
|
+
},
|
|
126
|
+
pull() {
|
|
127
|
+
if (!closed) port.postMessage({ more: true });
|
|
128
|
+
},
|
|
129
|
+
cancel() {
|
|
130
|
+
reader = void 0;
|
|
131
|
+
close();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const finish = (fail2) => {
|
|
135
|
+
const controller = reader;
|
|
136
|
+
reader = void 0;
|
|
137
|
+
for (const ack of acks.splice(0)) {
|
|
138
|
+
if (fail2) ack.reject(fail2);
|
|
139
|
+
else ack.resolve();
|
|
140
|
+
}
|
|
141
|
+
if (!controller) return;
|
|
142
|
+
if (fail2) controller.error(fail2);
|
|
143
|
+
else controller.close();
|
|
144
|
+
};
|
|
145
|
+
function shut() {
|
|
146
|
+
if (closed) return;
|
|
147
|
+
closed = true;
|
|
148
|
+
port.close();
|
|
149
|
+
onClose?.();
|
|
150
|
+
}
|
|
151
|
+
function close() {
|
|
152
|
+
if (!closed) port.postMessage({ close: true });
|
|
153
|
+
shut();
|
|
154
|
+
finish();
|
|
155
|
+
}
|
|
156
|
+
port.addEventListener("message", ({ data }) => {
|
|
157
|
+
if (data instanceof Uint8Array) reader?.enqueue(data);
|
|
158
|
+
else if ("ack" in data) acks.shift()?.resolve();
|
|
159
|
+
else if ("end" in data) finish();
|
|
160
|
+
else finish(errorWithCode(data.error));
|
|
161
|
+
});
|
|
162
|
+
port.start?.();
|
|
163
|
+
const writable = new WritableStream({
|
|
164
|
+
write(chunk) {
|
|
165
|
+
if (closed) throw errorWithCode("EPIPE: the connection is closed");
|
|
166
|
+
const copy = chunk.slice();
|
|
167
|
+
const acked = new Promise((resolve, reject) => acks.push({ resolve, reject }));
|
|
168
|
+
port.postMessage(copy, [copy.buffer]);
|
|
169
|
+
return acked;
|
|
170
|
+
},
|
|
171
|
+
close() {
|
|
172
|
+
if (!closed) port.postMessage({ end: true });
|
|
173
|
+
},
|
|
174
|
+
abort() {
|
|
175
|
+
close();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const fail = (error) => {
|
|
179
|
+
shut();
|
|
180
|
+
finish(error);
|
|
181
|
+
};
|
|
182
|
+
return { readable, writable, close, fail };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/client/loopback-fetch.ts
|
|
186
|
+
var OWN_HEADERS = /* @__PURE__ */ new Set(["host", "connection", "content-length", "transfer-encoding"]);
|
|
187
|
+
var encoder = new TextEncoder();
|
|
188
|
+
var latin1 = new TextDecoder("latin1");
|
|
189
|
+
var ByteSource = class {
|
|
190
|
+
buffered = new Uint8Array(0);
|
|
191
|
+
reader;
|
|
192
|
+
constructor(reader) {
|
|
193
|
+
this.reader = reader;
|
|
194
|
+
}
|
|
195
|
+
async more() {
|
|
196
|
+
const { done, value } = await this.reader.read();
|
|
197
|
+
if (done) return false;
|
|
198
|
+
const joined = new Uint8Array(this.buffered.length + value.length);
|
|
199
|
+
joined.set(this.buffered);
|
|
200
|
+
joined.set(value, this.buffered.length);
|
|
201
|
+
this.buffered = joined;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
async until(marker) {
|
|
205
|
+
const needle = encoder.encode(marker);
|
|
206
|
+
for (; ; ) {
|
|
207
|
+
const at = indexOf(this.buffered, needle);
|
|
208
|
+
if (at >= 0) return this.take(at + needle.length);
|
|
209
|
+
if (!await this.more()) return void 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async some(max) {
|
|
213
|
+
if (this.buffered.length === 0 && !await this.more()) return void 0;
|
|
214
|
+
return this.take(Math.min(max, this.buffered.length));
|
|
215
|
+
}
|
|
216
|
+
async exactly(count) {
|
|
217
|
+
while (this.buffered.length < count) if (!await this.more()) return void 0;
|
|
218
|
+
return this.take(count);
|
|
219
|
+
}
|
|
220
|
+
take(count) {
|
|
221
|
+
const out = this.buffered.slice(0, count);
|
|
222
|
+
this.buffered = this.buffered.slice(count);
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function indexOf(haystack, needle) {
|
|
227
|
+
outer: for (let i = 0; i + needle.length <= haystack.length; i++) {
|
|
228
|
+
for (let j = 0; j < needle.length; j++) if (haystack[i + j] !== needle[j]) continue outer;
|
|
229
|
+
return i;
|
|
230
|
+
}
|
|
231
|
+
return -1;
|
|
232
|
+
}
|
|
233
|
+
function parseHead(bytes) {
|
|
234
|
+
const [statusLine = "", ...lines] = latin1.decode(bytes).split("\r\n");
|
|
235
|
+
const match = /^HTTP\/1\.[01] (\d{3})(?: (.*))?$/.exec(statusLine);
|
|
236
|
+
if (!match) throw errorWithCode(`EPROTO: not an HTTP/1.x response: ${statusLine.slice(0, 80)}`);
|
|
237
|
+
const headers = [];
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
const colon = line.indexOf(":");
|
|
240
|
+
if (colon > 0) headers.push([line.slice(0, colon).trim(), line.slice(colon + 1).trim()]);
|
|
241
|
+
}
|
|
242
|
+
return { status: Number(match[1]), statusText: match[2] ?? "", headers };
|
|
243
|
+
}
|
|
244
|
+
function header(head, name) {
|
|
245
|
+
return head.headers.find(([key]) => key.toLowerCase() === name)?.[1];
|
|
246
|
+
}
|
|
247
|
+
async function* chunked(source) {
|
|
248
|
+
for (; ; ) {
|
|
249
|
+
const line = await source.until("\r\n");
|
|
250
|
+
if (!line) throw errorWithCode("EPROTO: the chunked body ended early");
|
|
251
|
+
const size = Number.parseInt(latin1.decode(line).trim().split(";")[0] ?? "", 16);
|
|
252
|
+
if (!Number.isFinite(size)) throw errorWithCode("EPROTO: a malformed chunk size");
|
|
253
|
+
if (size === 0) return;
|
|
254
|
+
const data = await source.exactly(size);
|
|
255
|
+
if (!data || !await source.exactly(2)) throw errorWithCode("EPROTO: a chunk ended early");
|
|
256
|
+
yield data;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function* counted(source, length) {
|
|
260
|
+
let left = length;
|
|
261
|
+
while (left > 0) {
|
|
262
|
+
const data = await source.some(left);
|
|
263
|
+
if (!data) throw errorWithCode("EPROTO: the body ended early");
|
|
264
|
+
left -= data.length;
|
|
265
|
+
yield data;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function* untilClose(source) {
|
|
269
|
+
for (let data = await source.some(65536); data; data = await source.some(65536)) yield data;
|
|
270
|
+
}
|
|
271
|
+
function bodyOf(source, head, method) {
|
|
272
|
+
if (method === "HEAD" || head.status === 204 || head.status === 304) return void 0;
|
|
273
|
+
if (header(head, "transfer-encoding")?.toLowerCase().includes("chunked")) return chunked(source);
|
|
274
|
+
const length = header(head, "content-length");
|
|
275
|
+
if (length === void 0) return untilClose(source);
|
|
276
|
+
const size = Number(length);
|
|
277
|
+
if (!/^\d+$/.test(length.trim()) || !Number.isSafeInteger(size)) {
|
|
278
|
+
throw errorWithCode(`EPROTO: a malformed Content-Length: ${length}`);
|
|
279
|
+
}
|
|
280
|
+
return counted(source, size);
|
|
281
|
+
}
|
|
282
|
+
function stream(chunks, socket, signal) {
|
|
283
|
+
return new ReadableStream({
|
|
284
|
+
start(controller) {
|
|
285
|
+
signal?.addEventListener("abort", () => controller.error(signal.reason), { once: true });
|
|
286
|
+
},
|
|
287
|
+
async pull(controller) {
|
|
288
|
+
try {
|
|
289
|
+
const next = await chunks.next();
|
|
290
|
+
if (next.done) {
|
|
291
|
+
controller.close();
|
|
292
|
+
socket.close();
|
|
293
|
+
} else controller.enqueue(next.value);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
controller.error(err);
|
|
296
|
+
socket.close();
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
cancel() {
|
|
300
|
+
socket.close();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async function requestBytes(request) {
|
|
305
|
+
const url = new URL(request.url);
|
|
306
|
+
const body = request.body ? new Uint8Array(await request.arrayBuffer()) : void 0;
|
|
307
|
+
const lines = [`${request.method} ${url.pathname}${url.search} HTTP/1.1`, `Host: ${url.host}`];
|
|
308
|
+
request.headers.forEach((value, name) => {
|
|
309
|
+
if (!OWN_HEADERS.has(name)) lines.push(`${name}: ${value}`);
|
|
310
|
+
});
|
|
311
|
+
if (body) lines.push(`Content-Length: ${body.length}`);
|
|
312
|
+
lines.push("Connection: close", "", "");
|
|
313
|
+
const head = encoder.encode(lines.join("\r\n"));
|
|
314
|
+
const out = new Uint8Array(head.length + (body?.length ?? 0));
|
|
315
|
+
out.set(head);
|
|
316
|
+
if (body) out.set(body, head.length);
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
async function loopbackFetch(dial, input, options) {
|
|
320
|
+
options.signal?.throwIfAborted();
|
|
321
|
+
const request = input instanceof Request ? input : new Request(input);
|
|
322
|
+
const bytes = await requestBytes(request);
|
|
323
|
+
const socket = await dial({
|
|
324
|
+
port: options.port,
|
|
325
|
+
...options.host ? { host: options.host } : {}
|
|
326
|
+
});
|
|
327
|
+
options.signal?.addEventListener("abort", () => socket.close(), { once: true });
|
|
328
|
+
try {
|
|
329
|
+
const writer = socket.writable.getWriter();
|
|
330
|
+
await writer.write(bytes);
|
|
331
|
+
writer.releaseLock();
|
|
332
|
+
const source = new ByteSource(socket.readable.getReader());
|
|
333
|
+
let head;
|
|
334
|
+
do {
|
|
335
|
+
const raw = await source.until("\r\n\r\n");
|
|
336
|
+
if (!raw) throw errorWithCode("ECONNRESET: the connection closed before a response");
|
|
337
|
+
head = parseHead(raw);
|
|
338
|
+
} while (head.status >= 100 && head.status < 200);
|
|
339
|
+
const chunks = bodyOf(source, head, request.method);
|
|
340
|
+
if (!chunks) socket.close();
|
|
341
|
+
return new Response(chunks ? stream(chunks, socket, options.signal) : null, {
|
|
342
|
+
status: head.status,
|
|
343
|
+
statusText: head.statusText,
|
|
344
|
+
headers: head.headers
|
|
345
|
+
});
|
|
346
|
+
} catch (err) {
|
|
347
|
+
socket.close();
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
113
352
|
// src/client/protocol.ts
|
|
114
|
-
var PROTOCOL = [1,
|
|
353
|
+
var PROTOCOL = [1, 4];
|
|
115
354
|
function locksOf(scope = globalThis) {
|
|
116
355
|
const locks = scope.navigator?.locks;
|
|
117
356
|
return typeof locks?.request === "function" ? locks : void 0;
|
|
@@ -138,8 +377,8 @@ var KernelCallError = class extends Error {
|
|
|
138
377
|
this.code = code;
|
|
139
378
|
}
|
|
140
379
|
};
|
|
141
|
-
var
|
|
142
|
-
var bytesOf = (data) => typeof data === "string" ?
|
|
380
|
+
var encoder2 = new TextEncoder();
|
|
381
|
+
var bytesOf = (data) => typeof data === "string" ? encoder2.encode(data) : data;
|
|
143
382
|
var UNAVAILABLE = {
|
|
144
383
|
manualRedirects: false,
|
|
145
384
|
encodedBodies: false,
|
|
@@ -247,8 +486,15 @@ var Channel = class {
|
|
|
247
486
|
else call.resolve(reply.result);
|
|
248
487
|
}
|
|
249
488
|
};
|
|
489
|
+
async function dialOn(channel, { port, host }) {
|
|
490
|
+
const req = { op: "dial", port, ...host ? { host } : {} };
|
|
491
|
+
return dialStream(await since(channel, 4, "dial", () => channel.call(req)));
|
|
492
|
+
}
|
|
250
493
|
async function spawnOn(channel, argv, opts) {
|
|
251
494
|
const { onStdout, onStderr, stdin, group: _, ...rest } = opts;
|
|
495
|
+
if (opts.pgid !== void 0)
|
|
496
|
+
await since(channel, 3, "spawn into a process group", async () => {
|
|
497
|
+
});
|
|
252
498
|
const started = Promise.withResolvers();
|
|
253
499
|
const options = { ...rest, ...stdin !== void 0 ? { stdin: bytesOf(stdin) } : {} };
|
|
254
500
|
const { done } = channel.request(
|
|
@@ -260,11 +506,12 @@ async function spawnOn(channel, argv, opts) {
|
|
|
260
506
|
);
|
|
261
507
|
done.catch(started.reject);
|
|
262
508
|
const pid = await started.promise;
|
|
509
|
+
const pgid = opts.pgid ?? pid;
|
|
263
510
|
return {
|
|
264
511
|
pid,
|
|
265
|
-
pgid
|
|
512
|
+
pgid,
|
|
266
513
|
exited: done,
|
|
267
|
-
signal: async (name = "SIGTERM") => void await channel.call({ op: "kill", pid: -
|
|
514
|
+
signal: async (name = "SIGTERM") => void await channel.call({ op: "kill", pid: -pgid, signal: name })
|
|
268
515
|
};
|
|
269
516
|
}
|
|
270
517
|
async function runOn(channel, argv, opts) {
|
|
@@ -421,6 +668,8 @@ async function attachKernel(port, options = {}) {
|
|
|
421
668
|
const { signal, ...rest } = req;
|
|
422
669
|
return transport.fetch({ ...rest, signal: signal ?? new AbortController().signal });
|
|
423
670
|
},
|
|
671
|
+
dial: (options2) => dialOn(channel, options2),
|
|
672
|
+
loopbackFetch: (input, options2) => loopbackFetch((o) => dialOn(channel, o), input, options2),
|
|
424
673
|
fs: fsOn(channel),
|
|
425
674
|
async close(o = {}) {
|
|
426
675
|
if (channel.failure) return;
|