@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 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 concat(a, b) {
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 = concat(buffer, next.value);
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,