@ai-ecoverse/slicc-kernel 1.8.11 → 1.9.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 +27 -1
- package/dist/index.js +391 -2
- package/dist/index.js.map +4 -4
- package/dist/kernel-worker.js +486 -66
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +382 -28
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +945 -179
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +382 -28
- package/dist/process-worker.js.map +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,6 +67,28 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
|
|
|
67
67
|
|
|
68
68
|
Stops the kernel worker and every process. Pending and later calls reject.
|
|
69
69
|
|
|
70
|
+
### `kernel.connect() → Promise<MessagePort>`
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
### `attachKernel(port, options?) → Promise<KernelClient>`
|
|
75
|
+
|
|
76
|
+
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`.
|
|
77
|
+
|
|
78
|
+
| Client | |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `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'` |
|
|
81
|
+
| `run(argv, options?)` | `spawn` and wait: `{ pid, status, stdout, stderr }` as text, with `onStdout`/`onStderr` streaming text |
|
|
82
|
+
| `openTerminal(argv, options?)` | a pty session, as `kernel.openTerminal` |
|
|
83
|
+
| `ps()` | the kernel's processes: `{ pid, ppid, pgid, sid, argv, tty, started, state }`, `state` being `'S'` or `'Z'` |
|
|
84
|
+
| `kill(pid, signal?)` | any process, or a process group with a negative pid; no such process rejects with `code: 'ESRCH'` |
|
|
85
|
+
| `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; failures reject with `KernelCallError` and a POSIX `code` |
|
|
86
|
+
| `fetch({ url, method, headers, body, signal })` | through the kernel's network transport (the one the page passed to `createKernel`), with a streaming body; `transport` is the same as a `NetworkTransport` |
|
|
87
|
+
| `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 |
|
|
88
|
+
| `closed` | resolves with the error that ended the client |
|
|
89
|
+
|
|
90
|
+
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.0`: a client or kernel of another major version is refused with an error naming both.
|
|
91
|
+
|
|
70
92
|
### Headless in Node, for tests
|
|
71
93
|
|
|
72
94
|
`@ai-ecoverse/slicc-kernel/node` runs the same kernel in Node without a browser, so packages built for SLICC can test against it. It is a testing entry, not a supported runtime: there is no OPFS and no isolation, and nothing beyond what its tests use is promised.
|
|
@@ -80,7 +102,7 @@ const { status, stdout } = await kernel.run(['bash', '-c', 'echo hi'], { cwd: '/
|
|
|
80
102
|
kernel.terminate();
|
|
81
103
|
```
|
|
82
104
|
|
|
83
|
-
`createNodeKernel({ root, modules, env, network, worker })` takes the options of `createKernel` except `metadata`. `root` is an in-memory directory by default (`memoryRoot()` makes another), and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal` and `terminate` as above, plus `root`, `writeFile(path, data)` (creating the parent directories) and `readFile(path)` to put files in place and read results. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`).
|
|
105
|
+
`createNodeKernel({ root, modules, env, network, worker })` takes the options of `createKernel` except `metadata`. `root` is an in-memory directory by default (`memoryRoot()` makes another), and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal` and `terminate` as above, plus `root`, `writeFile(path, data)` (creating the parent directories) and `readFile(path)` to put files in place and read results. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect()` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
|
|
84
106
|
|
|
85
107
|
## Commands
|
|
86
108
|
|
|
@@ -156,6 +178,10 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
|
|
|
156
178
|
|
|
157
179
|
Directories are renamed with `FileSystemHandle.move()` where available, else by copy and delete.
|
|
158
180
|
|
|
181
|
+
### `/proc`
|
|
182
|
+
|
|
183
|
+
Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self/` has the same files for the process that reads them. They come from the kernel's process table when they are opened, so a terminal sees the processes of every client; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real; CPU times, memory sizes, load and `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) are placeholders. Only Emscripten processes see this `/proc`.
|
|
184
|
+
|
|
159
185
|
### Metadata
|
|
160
186
|
|
|
161
187
|
OPFS stores names, bytes, sizes and modification times, nothing else. Everything POSIX needs on top of that lives in an IndexedDB sidecar, so it survives reloads and new kernels:
|
package/dist/index.js
CHANGED
|
@@ -164,6 +164,391 @@ function serveTransport(peer, transport) {
|
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// src/kernel/net/remote-transport.ts
|
|
168
|
+
var TransportError = class extends Error {
|
|
169
|
+
status;
|
|
170
|
+
code;
|
|
171
|
+
constructor(message, status, code) {
|
|
172
|
+
super(message);
|
|
173
|
+
this.status = status;
|
|
174
|
+
this.code = code;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
var RemoteTransport = class {
|
|
178
|
+
traits;
|
|
179
|
+
port;
|
|
180
|
+
waiting = /* @__PURE__ */ new Map();
|
|
181
|
+
nextId = 0;
|
|
182
|
+
failure;
|
|
183
|
+
constructor(port, traits) {
|
|
184
|
+
this.port = port;
|
|
185
|
+
this.traits = traits;
|
|
186
|
+
}
|
|
187
|
+
receive(reply) {
|
|
188
|
+
const waiter = this.waiting.get(reply.nid);
|
|
189
|
+
if (!waiter) return;
|
|
190
|
+
this.waiting.delete(reply.nid);
|
|
191
|
+
if (reply.net === "error") {
|
|
192
|
+
waiter.reject(new TransportError(reply.message, reply.status, reply.code));
|
|
193
|
+
} else waiter.resolve(reply);
|
|
194
|
+
}
|
|
195
|
+
fail(error) {
|
|
196
|
+
this.failure = error;
|
|
197
|
+
for (const waiter of this.waiting.values()) waiter.reject(error);
|
|
198
|
+
this.waiting.clear();
|
|
199
|
+
}
|
|
200
|
+
ask(call) {
|
|
201
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
this.waiting.set(call.nid, { resolve, reject });
|
|
204
|
+
this.port.postMessage(call);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
cancel(nid) {
|
|
208
|
+
const waiter = this.waiting.get(nid);
|
|
209
|
+
this.waiting.delete(nid);
|
|
210
|
+
waiter?.reject(new TransportError("the request was cancelled"));
|
|
211
|
+
this.port.postMessage({ net: "cancel", nid });
|
|
212
|
+
}
|
|
213
|
+
async fetch(request) {
|
|
214
|
+
const nid = ++this.nextId;
|
|
215
|
+
const { signal } = request;
|
|
216
|
+
signal.throwIfAborted();
|
|
217
|
+
let finished = false;
|
|
218
|
+
const abort = () => {
|
|
219
|
+
finished = true;
|
|
220
|
+
this.cancel(nid);
|
|
221
|
+
};
|
|
222
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
223
|
+
const finish = () => {
|
|
224
|
+
if (finished) return;
|
|
225
|
+
finished = true;
|
|
226
|
+
signal.removeEventListener("abort", abort);
|
|
227
|
+
};
|
|
228
|
+
let head;
|
|
229
|
+
try {
|
|
230
|
+
head = await this.ask({
|
|
231
|
+
net: "fetch",
|
|
232
|
+
nid,
|
|
233
|
+
url: request.url,
|
|
234
|
+
method: request.method,
|
|
235
|
+
headers: request.headers,
|
|
236
|
+
...request.body ? { body: request.body } : {}
|
|
237
|
+
});
|
|
238
|
+
} catch (e) {
|
|
239
|
+
finish();
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
const { status, statusText, headers } = head;
|
|
243
|
+
const read = async () => {
|
|
244
|
+
if (finished) return void 0;
|
|
245
|
+
const reply = await this.ask({ net: "read", nid }).catch((e) => {
|
|
246
|
+
finish();
|
|
247
|
+
throw e;
|
|
248
|
+
});
|
|
249
|
+
if (reply.net === "chunk") return reply.bytes;
|
|
250
|
+
finish();
|
|
251
|
+
return void 0;
|
|
252
|
+
};
|
|
253
|
+
const cancel = async () => {
|
|
254
|
+
if (finished) return;
|
|
255
|
+
finish();
|
|
256
|
+
this.cancel(nid);
|
|
257
|
+
};
|
|
258
|
+
return {
|
|
259
|
+
status,
|
|
260
|
+
statusText,
|
|
261
|
+
headers,
|
|
262
|
+
cancel,
|
|
263
|
+
body: {
|
|
264
|
+
[Symbol.asyncIterator]: () => ({
|
|
265
|
+
next: async () => {
|
|
266
|
+
const bytes = await read();
|
|
267
|
+
return bytes ? { value: bytes, done: false } : { value: void 0, done: true };
|
|
268
|
+
},
|
|
269
|
+
return: async () => {
|
|
270
|
+
await cancel();
|
|
271
|
+
return { value: void 0, done: true };
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// src/client/protocol.ts
|
|
280
|
+
var PROTOCOL = [1, 0];
|
|
281
|
+
function locksOf(scope = globalThis) {
|
|
282
|
+
const locks = scope.navigator?.locks;
|
|
283
|
+
return typeof locks?.request === "function" ? locks : void 0;
|
|
284
|
+
}
|
|
285
|
+
function versionError(theirs) {
|
|
286
|
+
const major = Array.isArray(theirs) ? theirs[0] : void 0;
|
|
287
|
+
if (major === PROTOCOL[0]) return void 0;
|
|
288
|
+
const named = typeof major === "number" ? `${major}.x` : "without a version";
|
|
289
|
+
return `slicc-kernel client protocol ${named} is not supported: this side speaks ${PROTOCOL.join(".")}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/client/attach.ts
|
|
293
|
+
var KernelGoneError = class extends Error {
|
|
294
|
+
constructor(message = "the slicc-kernel this client was attached to is gone") {
|
|
295
|
+
super(message);
|
|
296
|
+
this.name = "KernelGoneError";
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
var KernelCallError = class extends Error {
|
|
300
|
+
code;
|
|
301
|
+
constructor(message, code) {
|
|
302
|
+
super(message);
|
|
303
|
+
this.name = "KernelCallError";
|
|
304
|
+
this.code = code;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var encoder = new TextEncoder();
|
|
308
|
+
var bytesOf = (data) => typeof data === "string" ? encoder.encode(data) : data;
|
|
309
|
+
var UNAVAILABLE = {
|
|
310
|
+
manualRedirects: false,
|
|
311
|
+
encodedBodies: false,
|
|
312
|
+
maxRequestBody: 0,
|
|
313
|
+
unavailable: true
|
|
314
|
+
};
|
|
315
|
+
async function holdLock(locks) {
|
|
316
|
+
if (!locks) return { release() {
|
|
317
|
+
} };
|
|
318
|
+
const name = `slicc-kernel-client:${crypto.randomUUID()}`;
|
|
319
|
+
const granted = Promise.withResolvers();
|
|
320
|
+
const held = Promise.withResolvers();
|
|
321
|
+
void locks.request(name, () => {
|
|
322
|
+
granted.resolve();
|
|
323
|
+
return held.promise;
|
|
324
|
+
});
|
|
325
|
+
await granted.promise;
|
|
326
|
+
return { name, release: () => held.resolve() };
|
|
327
|
+
}
|
|
328
|
+
function concat(chunks3) {
|
|
329
|
+
const out = new Uint8Array(chunks3.reduce((n, c) => n + c.length, 0));
|
|
330
|
+
let at = 0;
|
|
331
|
+
for (const chunk of chunks3) {
|
|
332
|
+
out.set(chunk, at);
|
|
333
|
+
at += chunk.length;
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
var Channel = class {
|
|
338
|
+
greeted = Promise.withResolvers();
|
|
339
|
+
ended = Promise.withResolvers();
|
|
340
|
+
remote;
|
|
341
|
+
failure;
|
|
342
|
+
pending = /* @__PURE__ */ new Map();
|
|
343
|
+
nextId = 0;
|
|
344
|
+
port;
|
|
345
|
+
release;
|
|
346
|
+
constructor(port, release) {
|
|
347
|
+
this.port = port;
|
|
348
|
+
this.release = release;
|
|
349
|
+
this.greeted.promise.catch(() => void 0);
|
|
350
|
+
port.addEventListener("message", (event) => this.receive(event.data));
|
|
351
|
+
port.addEventListener("close", () => this.end(new KernelGoneError()));
|
|
352
|
+
port.start?.();
|
|
353
|
+
}
|
|
354
|
+
post(message) {
|
|
355
|
+
if (!this.failure) this.port.postMessage(message);
|
|
356
|
+
}
|
|
357
|
+
end(error) {
|
|
358
|
+
if (this.failure) return;
|
|
359
|
+
this.failure = error;
|
|
360
|
+
this.greeted.reject(error);
|
|
361
|
+
this.remote?.fail(error);
|
|
362
|
+
for (const call of this.pending.values()) call.reject(error);
|
|
363
|
+
this.pending.clear();
|
|
364
|
+
this.release();
|
|
365
|
+
this.port.close?.();
|
|
366
|
+
this.ended.resolve(error);
|
|
367
|
+
}
|
|
368
|
+
request(call, handlers = {}) {
|
|
369
|
+
const id = ++this.nextId;
|
|
370
|
+
const { failure: failure2 } = this;
|
|
371
|
+
if (failure2) return { id, done: Promise.reject(failure2) };
|
|
372
|
+
const done = new Promise((resolve, reject) => {
|
|
373
|
+
this.pending.set(id, { resolve, reject, ...handlers });
|
|
374
|
+
this.port.postMessage({ ...call, id });
|
|
375
|
+
});
|
|
376
|
+
return { id, done };
|
|
377
|
+
}
|
|
378
|
+
call(call) {
|
|
379
|
+
return this.request(call).done;
|
|
380
|
+
}
|
|
381
|
+
receive(data) {
|
|
382
|
+
if (data.bye !== void 0) {
|
|
383
|
+
this.end(new KernelGoneError(data.bye));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (data.hello) {
|
|
387
|
+
this.greeted.resolve(data.hello);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (data.net !== void 0) {
|
|
391
|
+
this.remote?.receive(data);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const reply = data;
|
|
395
|
+
const call = this.pending.get(reply.id);
|
|
396
|
+
if (!call) return;
|
|
397
|
+
if (reply.fd !== void 0) {
|
|
398
|
+
call.output?.(reply.fd, reply.bytes);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (reply.started !== void 0) {
|
|
402
|
+
call.started?.(reply.started);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
this.pending.delete(reply.id);
|
|
406
|
+
if (reply.error !== void 0) call.reject(new KernelCallError(reply.error, reply.code));
|
|
407
|
+
else call.resolve(reply.result);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
async function spawnOn(channel, argv, opts) {
|
|
411
|
+
const { onStdout, onStderr, stdin, group: _, ...rest } = opts;
|
|
412
|
+
const started = Promise.withResolvers();
|
|
413
|
+
const options = { ...rest, ...stdin !== void 0 ? { stdin: bytesOf(stdin) } : {} };
|
|
414
|
+
const { done } = channel.request(
|
|
415
|
+
{ op: "spawn", argv, options },
|
|
416
|
+
{
|
|
417
|
+
output: (fd, bytes) => (fd === 1 ? onStdout : onStderr)?.(bytes),
|
|
418
|
+
started: started.resolve
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
done.catch(started.reject);
|
|
422
|
+
const pid = await started.promise;
|
|
423
|
+
return {
|
|
424
|
+
pid,
|
|
425
|
+
pgid: pid,
|
|
426
|
+
exited: done,
|
|
427
|
+
signal: async (name = "SIGTERM") => void await channel.call({ op: "kill", pid: -pid, signal: name })
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function runOn(channel, argv, opts) {
|
|
431
|
+
const { onStdout, onStderr, ...rest } = opts;
|
|
432
|
+
const chunks3 = { 1: [], 2: [] };
|
|
433
|
+
const live = { 1: new TextDecoder(), 2: new TextDecoder() };
|
|
434
|
+
const tee = (fd, on) => (bytes) => {
|
|
435
|
+
chunks3[fd].push(bytes);
|
|
436
|
+
on?.(live[fd].decode(bytes, { stream: true }));
|
|
437
|
+
};
|
|
438
|
+
const child = await spawnOn(channel, argv, {
|
|
439
|
+
...rest,
|
|
440
|
+
onStdout: tee(1, onStdout),
|
|
441
|
+
onStderr: tee(2, onStderr)
|
|
442
|
+
});
|
|
443
|
+
const status = await child.exited;
|
|
444
|
+
const text = (fd) => new TextDecoder().decode(concat(chunks3[fd]));
|
|
445
|
+
return { pid: child.pid, status, stdout: text(1), stderr: text(2) };
|
|
446
|
+
}
|
|
447
|
+
async function terminalOn(channel, argv, opts) {
|
|
448
|
+
const { onData, ...options } = opts;
|
|
449
|
+
let listener = onData ?? null;
|
|
450
|
+
const backlog = [];
|
|
451
|
+
const started = Promise.withResolvers();
|
|
452
|
+
const { id, done } = channel.request(
|
|
453
|
+
{ op: "open-terminal", argv, options },
|
|
454
|
+
{
|
|
455
|
+
output: (_fd, bytes) => listener ? listener(bytes) : backlog.push(bytes),
|
|
456
|
+
started: started.resolve
|
|
457
|
+
}
|
|
458
|
+
);
|
|
459
|
+
done.catch(started.reject);
|
|
460
|
+
const pid = await started.promise;
|
|
461
|
+
const send2 = (action) => void channel.call({ op: "terminal", terminal: id, ...action }).catch(() => void 0);
|
|
462
|
+
return {
|
|
463
|
+
pid,
|
|
464
|
+
exited: done,
|
|
465
|
+
get onData() {
|
|
466
|
+
return listener;
|
|
467
|
+
},
|
|
468
|
+
set onData(next) {
|
|
469
|
+
listener = next;
|
|
470
|
+
if (next) for (const bytes of backlog.splice(0)) next(bytes);
|
|
471
|
+
},
|
|
472
|
+
write: (data) => send2({ action: "write", bytes: bytesOf(data) }),
|
|
473
|
+
resize: (cols, rows) => send2({ action: "resize", cols, rows }),
|
|
474
|
+
signal: (name) => send2({ action: "signal", signal: name }),
|
|
475
|
+
close: () => send2({ action: "close" })
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function fsOn(channel) {
|
|
479
|
+
const fs = (method, ...args) => channel.call({ op: "fs", method, args });
|
|
480
|
+
const done = async (method, ...args) => void await fs(method, ...args);
|
|
481
|
+
return {
|
|
482
|
+
readFile: (path) => fs("readFile", path),
|
|
483
|
+
readText: async (path) => new TextDecoder().decode(await fs("readFile", path)),
|
|
484
|
+
writeFile: (path, data) => done("writeFile", path, data),
|
|
485
|
+
stat: (path) => fs("stat", path),
|
|
486
|
+
lstat: (path) => fs("lstat", path),
|
|
487
|
+
readdir: (path) => fs("readdir", path),
|
|
488
|
+
mkdir: (path) => done("mkdir", path),
|
|
489
|
+
rm: (path, o = {}) => done("rm", path, o.force === true),
|
|
490
|
+
rename: (from, to) => done("rename", from, to),
|
|
491
|
+
realpath: (path) => fs("realpath", path),
|
|
492
|
+
symlink: (target, path) => done("symlink", target, path),
|
|
493
|
+
readlink: (path) => fs("readlink", path),
|
|
494
|
+
exists: (path) => fs("exists", path)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
async function greet(channel, lock, timeoutMs) {
|
|
498
|
+
channel.post({ hello: { protocol: PROTOCOL, ...lock ? { lock } : {} } });
|
|
499
|
+
const timer = setTimeout(
|
|
500
|
+
() => channel.end(
|
|
501
|
+
new KernelGoneError(`no slicc-kernel answered on this port within ${timeoutMs} ms`)
|
|
502
|
+
),
|
|
503
|
+
timeoutMs
|
|
504
|
+
);
|
|
505
|
+
let hello;
|
|
506
|
+
try {
|
|
507
|
+
hello = await channel.greeted.promise;
|
|
508
|
+
} finally {
|
|
509
|
+
clearTimeout(timer);
|
|
510
|
+
}
|
|
511
|
+
const refused = hello.error ?? versionError(hello.protocol);
|
|
512
|
+
if (refused) {
|
|
513
|
+
const error = new Error(refused);
|
|
514
|
+
channel.end(error);
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
return hello;
|
|
518
|
+
}
|
|
519
|
+
async function attachKernel(port, options = {}) {
|
|
520
|
+
const locks = options.locks === null ? void 0 : options.locks ?? locksOf();
|
|
521
|
+
const own = await holdLock(locks);
|
|
522
|
+
const channel = new Channel(port, own.release);
|
|
523
|
+
const hello = await greet(channel, own.name, options.timeoutMs ?? 1e4);
|
|
524
|
+
if (hello.lock && locks) void locks.request(hello.lock, () => channel.end(new KernelGoneError()));
|
|
525
|
+
const transport = new RemoteTransport(
|
|
526
|
+
{ postMessage: (call) => channel.post(call) },
|
|
527
|
+
hello.traits ?? UNAVAILABLE
|
|
528
|
+
);
|
|
529
|
+
channel.remote = transport;
|
|
530
|
+
return {
|
|
531
|
+
protocol: hello.protocol,
|
|
532
|
+
transport,
|
|
533
|
+
closed: channel.ended.promise,
|
|
534
|
+
spawn: (argv, opts = {}) => spawnOn(channel, argv, opts),
|
|
535
|
+
run: (argv, opts = {}) => runOn(channel, argv, opts),
|
|
536
|
+
openTerminal: (argv, opts = {}) => terminalOn(channel, argv, opts),
|
|
537
|
+
ps: () => channel.call({ op: "ps" }),
|
|
538
|
+
kill: async (pid, signal = "SIGTERM") => void await channel.call({ op: "kill", pid, signal }),
|
|
539
|
+
fetch: (req) => {
|
|
540
|
+
const { signal, ...rest } = req;
|
|
541
|
+
return transport.fetch({ ...rest, signal: signal ?? new AbortController().signal });
|
|
542
|
+
},
|
|
543
|
+
fs: fsOn(channel),
|
|
544
|
+
async close(o = {}) {
|
|
545
|
+
if (channel.failure) return;
|
|
546
|
+
await channel.call({ op: "detach", ...o.kill ? { kill: true } : {} }).catch(() => void 0);
|
|
547
|
+
channel.end(new KernelGoneError("this client is closed"));
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
167
552
|
// src/local-proxy-transport.ts
|
|
168
553
|
var PATH = "/api/fetch-proxy";
|
|
169
554
|
var RAW_CONTENT_TYPE = "application/vnd.slicc.raw-fetch";
|
|
@@ -212,7 +597,7 @@ function isHead(value) {
|
|
|
212
597
|
(pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string"
|
|
213
598
|
);
|
|
214
599
|
}
|
|
215
|
-
function
|
|
600
|
+
function concat2(a, b) {
|
|
216
601
|
const out = new Uint8Array(a.byteLength + b.byteLength);
|
|
217
602
|
out.set(a);
|
|
218
603
|
out.set(b, a.byteLength);
|
|
@@ -236,7 +621,7 @@ async function readHead(reader, idleMs) {
|
|
|
236
621
|
}
|
|
237
622
|
const next = await readWithin(reader, idleMs);
|
|
238
623
|
if (next.done) throw failure("closed before the response head");
|
|
239
|
-
buffer =
|
|
624
|
+
buffer = concat2(buffer, next.value);
|
|
240
625
|
}
|
|
241
626
|
}
|
|
242
627
|
async function* chunks2(rest, reader, idleMs) {
|
|
@@ -446,6 +831,7 @@ async function createKernel(options = {}) {
|
|
|
446
831
|
};
|
|
447
832
|
},
|
|
448
833
|
openTerminal,
|
|
834
|
+
connect: async () => await call({ op: "connect" }),
|
|
449
835
|
terminate() {
|
|
450
836
|
fail(new Error("slicc-kernel terminated"));
|
|
451
837
|
worker.terminate();
|
|
@@ -453,6 +839,9 @@ async function createKernel(options = {}) {
|
|
|
453
839
|
};
|
|
454
840
|
}
|
|
455
841
|
export {
|
|
842
|
+
KernelCallError,
|
|
843
|
+
KernelGoneError,
|
|
844
|
+
attachKernel,
|
|
456
845
|
checkLocalProxy,
|
|
457
846
|
createKernel,
|
|
458
847
|
fetchTransport,
|