@ai-ecoverse/slicc-kernel 1.33.0 → 1.34.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 CHANGED
@@ -65,7 +65,7 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
65
65
  | --- | --- |
66
66
  | `pid` | the session leader's pid |
67
67
  | `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
68
- | `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group. A key typed while a child of that group is still setting up is held until the child has finished, then sent to the group in the foreground at that point. Setting up means the child's calls before its first other one: `setpgid`, `tcsetpgrp` and signal masks. The hold ends 250 ms after the child's first call at the latest. So `^Z` right after a command line stops the new job, as it does on Linux, where that setup takes microseconds rather than a worker start. |
68
+ | `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group. A key typed while a child of that group is still setting up is held until the child has finished, then sent to the group in the foreground at that point. Setting up means the child's calls before its first other one: `setpgid`, `tcsetpgrp`, signal masks, and starting the program it execs (which then sets itself up too). The hold ends 2 s after the child's first call at the latest. A key typed while the foreground group is busy, not reading the terminal (a shell between reading a command line and starting it), goes to that group and also to the job the group starts next, once that job has taken the terminal; the second copy is dropped when the group reads the terminal again. So `^C` or `^Z` right after a command line reaches the new job, as it does on Linux, where starting a job takes microseconds rather than a worker start. |
69
69
  | `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
70
70
  | `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
71
71
  | `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
@@ -334,6 +334,8 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
334
334
 
335
335
  Directories are renamed with `FileSystemHandle.move()` where available, else by copy and delete.
336
336
 
337
+ `mmap` of a regular file works on every file system, for Emscripten programs built with `mmap` (whose glue exports `emscripten_builtin_memalign`) as well as for WASIX ones: the kernel reads the mapped range into fresh memory of the process, as Emscripten's MEMFS does. A `MAP_PRIVATE` mapping may be written, and its changes stay in the process. A writable `MAP_SHARED` mapping of such a file is `ENODEV`; a file in the process's own in-memory file system (MEMFS, as under `/dev`) keeps Emscripten's own mapping, which writes back. A read-only `MAP_SHARED` mapping is a copy taken at `mmap`, so later changes to the file do not show in it.
338
+
337
339
  ### Mounts
338
340
 
339
341
  `kernel.mount({ type, source, target, options })` mounts a file system on an existing directory. Every process sees it, Emscripten and WASI alike, and so does every attached client. `kernel.umount(target)` unmounts it, and fails with `EBUSY` while a process has a file open under it. `kernel.mounts()` lists the table, as does `/proc/mounts`. The Node entry and attached clients have the same three calls (client protocol 1.2).
@@ -347,7 +349,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
347
349
  - `@ai-ecoverse/slicc-kernel/driver` exports the types and `fsError`.
348
350
  - **Capabilities:** `readonly`, `symlinks`, `chmod`, `ranges`, `maxIo` (the largest read or write per message), `maxFile`, `listingStats`, `linkTimes`, and `attrTtl`/`entryTtl` (in ms, default 1000). `linkTimes` promises that `setattr` with `mtime` on a symlink sets the link's own time. Without it, setting a link's own times (`touch -h`, `utimensat` with `AT_SYMLINK_NOFOLLOW`) fails with `EOPNOTSUPP` and never reaches the target. tmpfs has it; hostfs does not. Setting times through a link follows it in the kernel, so `setattr` gets the target. `ranges` (driver protocol 1.1) promises that a write lands in the file at once, with no copy of the whole file on `open` or `release`, and that `setattr` takes `size`.
349
351
  - **Caching:** the kernel caches attributes and listings for those TTLs, and a driver can push `invalidate` for changes made outside.
350
- - **File contents:** on a mount with `ranges`, as `tmpfs` is, and on the root file system when it is in OPFS (in a browser with sync access handles) or an in-memory root, a program reads and writes a file in 64 KiB pages: reads come from the driver 1 MiB at a time, and changed pages go back in 1 MiB runs once 8 MiB have changed, at `fsync`, and when the file is closed (from the kernel's own descriptors also 250 ms after a write). A file of any size the mount can hold works, with memory to spare for about one more copy of it. In OPFS a write opens a sync access handle for just that write. Elsewhere (`fsa` mounts, `hostfs` with an older proxy, and a root outside OPFS), file contents are read whole when a program opens a file and written back when it closes it. Both give close-to-open consistency, as NFS has. A process that ends without closing its files keeps what it wrote: on exit, on a trap, and when a signal kills it. For a killed WASI program, the kernel lets its worker write its changed pages back before `waitpid` reports the death. The only exception is a worker that makes no call into the kernel for 2 seconds after the signal; it is stopped without that write-back.
352
+ - **File contents:** on a mount with `ranges`, as `tmpfs` is, and on the root file system when it is in OPFS (in a browser with sync access handles) or an in-memory root, a program reads and writes a file in 64 KiB pages: reads come from the driver 1 MiB at a time, and changed pages go back in 1 MiB runs once 8 MiB have changed, at `fsync`, and when the file is closed (from the kernel's own descriptors also 250 ms after a write). A file of any size the mount can hold works, with memory to spare for about one more copy of it. In OPFS a write opens a sync access handle for just that write. Elsewhere (`fsa` mounts, `hostfs` with an older proxy, and a root outside OPFS), file contents are read whole when a program opens a file and written back when it closes it. Before a program's next call into the kernel (any wait, read of a pipe or terminal, sleep or spawn), its changed pages also go back, so other processes and the embedder see a write as soon as the writer does anything but compute: a daemon's pidfile, a log another process follows. On a whole-file mount that holds only for files of up to 64 KiB; a larger one goes back at close, fsync and exit, as before. Beyond that, both give close-to-open consistency, as NFS has. A process that ends without closing its files keeps what it wrote: on exit, on a trap, and when a signal kills it. For a killed WASI program, the kernel lets its worker write its changed pages back before `waitpid` reports the death. The only exception is a worker that makes no call into the kernel for 2 seconds after the signal; it is stopped without that write-back.
351
353
  - **Rules on a mount:**
352
354
  - a rename across mounts is `EXDEV`, so `mv` copies;
353
355
  - `df` reports each mount from its driver's `statfs`;
@@ -1144,14 +1144,6 @@ function sinkFile(onData) {
1144
1144
  }
1145
1145
  });
1146
1146
  }
1147
- function nullFile() {
1148
- return new OpenFile({
1149
- read: async () => new Uint8Array(0),
1150
- write: async (bytes) => bytes.length,
1151
- close: () => {
1152
- }
1153
- });
1154
- }
1155
1147
  function written(device, bytes) {
1156
1148
  if (device === "full" && bytes.length > 0) throw new KernelError("ENOSPC");
1157
1149
  return bytes.length;
@@ -5003,7 +4995,7 @@ var ChildTable = class {
5003
4995
  captured.set(n, chunks);
5004
4996
  return sinkFile((bytes) => chunks.push(bytes));
5005
4997
  }
5006
- return nullFile();
4998
+ return deviceFile("null");
5007
4999
  }
5008
5000
  async wait(pid, nohang, signal, flags = {}) {
5009
5001
  let interrupt;
@@ -6568,6 +6560,8 @@ var KernelTty = class {
6568
6560
  waiters = [];
6569
6561
  hungUp = false;
6570
6562
  name;
6563
+ onRead;
6564
+ readers = 0;
6571
6565
  screen;
6572
6566
  signal;
6573
6567
  constructor(screen, signal) {
@@ -6718,7 +6712,19 @@ var KernelTty = class {
6718
6712
  }
6719
6713
  this.screen.write(Uint8Array.from(out));
6720
6714
  }
6715
+ get reading() {
6716
+ return this.readers > 0;
6717
+ }
6721
6718
  async read(max, signal) {
6719
+ this.onRead?.();
6720
+ this.readers++;
6721
+ try {
6722
+ return await this.take(max, signal);
6723
+ } finally {
6724
+ this.readers--;
6725
+ }
6726
+ }
6727
+ async take(max, signal) {
6722
6728
  while (this.readable.length === 0) {
6723
6729
  if (this.hungUp) return new Uint8Array(0);
6724
6730
  await this.changed(signal);
@@ -6783,13 +6789,14 @@ var PtyPair = class {
6783
6789
  slaveSeen = false;
6784
6790
  index;
6785
6791
  onFree;
6786
- constructor(index, signal, onFree) {
6792
+ constructor(index, signal, onFree, onRead) {
6787
6793
  this.index = index;
6788
6794
  this.onFree = onFree;
6789
6795
  this.slave = new KernelTty(
6790
6796
  { write: (bytes) => this.fromSlave(bytes) },
6791
6797
  (sig) => signal(this.slave, sig)
6792
6798
  );
6799
+ if (onRead) this.slave.onRead = () => onRead(this.slave);
6793
6800
  this.slave.name = `/dev/pts/${index}`;
6794
6801
  }
6795
6802
  master() {
@@ -6887,13 +6894,15 @@ var PtyPair = class {
6887
6894
  var PtyTable = class {
6888
6895
  pairs = /* @__PURE__ */ new Map();
6889
6896
  signal;
6890
- constructor(signal) {
6897
+ onRead;
6898
+ constructor(signal, onRead) {
6891
6899
  this.signal = signal;
6900
+ this.onRead = onRead;
6892
6901
  }
6893
6902
  open() {
6894
6903
  let index = 0;
6895
6904
  while (this.pairs.has(index)) index += 1;
6896
- const pair = new PtyPair(index, this.signal, (p) => this.pairs.delete(p.index));
6905
+ const pair = new PtyPair(index, this.signal, (p) => this.pairs.delete(p.index), this.onRead);
6897
6906
  this.pairs.set(index, pair);
6898
6907
  return { pair, master: pair.master() };
6899
6908
  }
@@ -8446,6 +8455,7 @@ var WasmProcess = class {
8446
8455
  async exit() {
8447
8456
  if (this.exited) return;
8448
8457
  this.exited = true;
8458
+ this.die();
8449
8459
  this.clearAlarm();
8450
8460
  this.interrupt.abort();
8451
8461
  this.options.locks?.release(this.pid);
@@ -8858,13 +8868,16 @@ var SETUP_OPS = /* @__PURE__ */ new Set([
8858
8868
  "fd-info",
8859
8869
  "sig-mask",
8860
8870
  "proc-setpgid",
8861
- "tty-pgrp-set"
8871
+ "tty-pgrp-set",
8872
+ "proc-exec",
8873
+ "proc-spawn"
8862
8874
  ]);
8863
- var SETTLE_MS = 250;
8875
+ var SETTLE_MS = 2e3;
8864
8876
  var KEY_SIGNALS = /* @__PURE__ */ new Set([2, 3, 20]);
8865
8877
  var SettlingChildren = class {
8866
8878
  children = /* @__PURE__ */ new Map();
8867
8879
  held = [];
8880
+ parked = [];
8868
8881
  pgidOf;
8869
8882
  settleMs;
8870
8883
  constructor(pgidOf, settleMs = SETTLE_MS) {
@@ -8872,7 +8885,15 @@ var SettlingChildren = class {
8872
8885
  this.settleMs = settleMs;
8873
8886
  }
8874
8887
  started(pid, ppid) {
8875
- this.children.set(pid, { ppid });
8888
+ const parent = this.children.get(ppid);
8889
+ this.children.set(pid, parent ? { ppid: parent.ppid, execer: ppid } : { ppid });
8890
+ const group = this.pgidOf(ppid);
8891
+ for (const p of this.parked) if (p.pgid === group) this.held.push({ ...p, copy: true });
8892
+ this.parked = this.parked.filter((p) => p.pgid !== group);
8893
+ }
8894
+ reading(pgid) {
8895
+ this.parked = this.parked.filter((p) => p.pgid !== pgid);
8896
+ this.held = this.held.filter((h) => !(h.copy && h.pgid === pgid));
8876
8897
  }
8877
8898
  syscall(pid, op) {
8878
8899
  const child = this.children.get(pid);
@@ -8888,13 +8909,25 @@ var SettlingChildren = class {
8888
8909
  if (!child) return;
8889
8910
  clearTimeout(child.timer);
8890
8911
  this.children.delete(pid);
8912
+ if (child.execer !== void 0) this.settled(child.execer);
8891
8913
  const ready2 = this.held.filter((h) => !this.blocking(h.pgid));
8892
8914
  this.held = this.held.filter((h) => this.blocking(h.pgid));
8893
8915
  for (const h of ready2) h.send();
8894
8916
  }
8895
- deliver(sig, pgid, send) {
8896
- if (KEY_SIGNALS.has(sig) && this.blocking(pgid)) this.held.push({ pgid, send });
8897
- else send();
8917
+ deliver(sig, pgid, send, foreground) {
8918
+ if (!KEY_SIGNALS.has(sig)) {
8919
+ send();
8920
+ } else if (this.blocking(pgid)) {
8921
+ this.held.push({ pgid, send });
8922
+ } else if (foreground) {
8923
+ send();
8924
+ const moved2 = () => {
8925
+ if (foreground() !== pgid) send();
8926
+ };
8927
+ this.parked.push({ pgid, send: moved2 });
8928
+ } else {
8929
+ send();
8930
+ }
8898
8931
  }
8899
8932
  blocking(pgid) {
8900
8933
  for (const { ppid } of this.children.values()) if (this.pgidOf(ppid) === pgid) return true;
@@ -10164,11 +10197,8 @@ var Launcher = class {
10164
10197
  locks = new LockTable();
10165
10198
  settling = new SettlingChildren((pid) => this.jobs.pgidOf(pid));
10166
10199
  ptys = new PtyTable(
10167
- (tty, sig) => this.settling.deliver(
10168
- sig,
10169
- this.jobs.tcgetpgrp(tty, 0),
10170
- () => this.jobs.signalOwnedForeground(tty, sig)
10171
- )
10200
+ (tty, sig) => this.keySignal(tty, 0, sig, () => this.jobs.signalOwnedForeground(tty, sig)),
10201
+ (tty) => this.settling.reading(this.jobs.tcgetpgrp(tty, 0))
10172
10202
  );
10173
10203
  net = new LoopbackNet();
10174
10204
  cdp;
@@ -10584,6 +10614,11 @@ var Launcher = class {
10584
10614
  })
10585
10615
  );
10586
10616
  }
10617
+ keySignal(tty, fallback, sig, send) {
10618
+ const pgid = this.jobs.tcgetpgrp(tty, fallback);
10619
+ const foreground = () => this.jobs.tcgetpgrp(tty, fallback);
10620
+ this.settling.deliver(sig, pgid, send, tty.reading ? void 0 : foreground);
10621
+ }
10587
10622
  ignoredBy(pid, fork = false) {
10588
10623
  return this.processes.get(pid)?.ignoredSignals(fork) ?? 0;
10589
10624
  }
@@ -10779,12 +10814,9 @@ var Launcher = class {
10779
10814
  let leader = 0;
10780
10815
  const tty = new KernelTty(
10781
10816
  { write: (bytes) => options.onData(bytes.slice()) },
10782
- (sig) => this.settling.deliver(
10783
- sig,
10784
- this.jobs.tcgetpgrp(tty, leader),
10785
- () => this.jobs.signalForeground(tty, leader, sig)
10786
- )
10817
+ (sig) => this.keySignal(tty, leader, sig, () => this.jobs.signalForeground(tty, leader, sig))
10787
10818
  );
10819
+ tty.onRead = () => this.settling.reading(this.jobs.tcgetpgrp(tty, leader));
10788
10820
  tty.name = `/dev/tty${++this.terminals}`;
10789
10821
  tty.setSize(options.cols ?? 80, options.rows ?? 24);
10790
10822
  const fds = new FdTable();
@@ -10833,7 +10865,7 @@ var Launcher = class {
10833
10865
  return { status: NOT_FOUND, stdout: concat5(out), stderr: concat5(err) };
10834
10866
  }
10835
10867
  const fds = new FdTable();
10836
- fds.installAt(0, options.stdin ? bytesSource(options.stdin) : nullFile());
10868
+ fds.installAt(0, options.stdin ? bytesSource(options.stdin) : deviceFile("null"));
10837
10869
  fds.installAt(1, sinkFile(stdout));
10838
10870
  fds.installAt(2, sinkFile(stderr));
10839
10871
  const group = options.pgid !== void 0 ? { pgid: options.pgid } : {};