@ai-ecoverse/slicc-kernel 1.47.2 → 1.48.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
@@ -66,7 +66,7 @@ Starts `argv` as the session leader on a new terminal and resolves once it is ru
66
66
  | --- | --- |
67
67
  | `pid` | the session leader's pid |
68
68
  | `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
69
- | `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
+ | `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. For the same reason, a child that is still setting up cannot give the terminal to its own group once its parent has already given the terminal to that group and taken it back: on Linux the child's `tcsetpgrp` would have landed first, so the parent's later choice stands. Otherwise `tcsetpgrp` behaves as before. |
70
70
  | `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
71
71
  | `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
72
72
  | `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
@@ -188,6 +188,7 @@ On top of preview1 a WASI program gets:
188
188
  - **Threads**: `wasi.thread-spawn` (wasm32-wasip1-threads) and WASIX `thread_spawn_v2` start a worker per thread on the process's shared memory, sized from the module's own import (a 2 GiB maximum when the engine cannot reserve more). The threads share one descriptor table; `exit` or a trap in any thread ends the process. At most 64 threads per process; `SLICC_WASM_THREADS` sets another cap, up to 256.
189
189
  - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid`, pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`.
190
190
  - **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names.
191
+ - **Pipes** show to WASI and WASIX programs as stream sockets (`fstat` gives `S_IFSOCK`), because WASI has no FIFO file type and an unknown type made programs refuse them (Python's asyncio subprocess pipes). They are still read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. In a WASIX shell, `[ -p ]` is therefore false for a pipe and `[ -S ]` true. Emscripten programs, bash included, still see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output: WASI and WASIX programs see it as a stream socket too.
191
192
  - **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream pair, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names.
192
193
  - **Diagnostics**: a trap ends the program with 134 and its message on stderr; with `SLICC_WASM_BACKTRACE=1` the wasm frames follow, named from the module's name section, or from a sidecar for a module shipped without one (`<module>.names` beside it, or the same path in a `<package>-names` package). `SLICC_WASI_STATS=1` makes a program print its calls, counted and timed, as it ends.
193
194
 
@@ -459,6 +460,16 @@ WASI and WASIX have no call for file modes, so the kernel adds the import module
459
460
  | `fd_mode(fd, mode_out)` | the permission bits of an open file or directory |
460
461
  | `path_mode(dirfd, path, path_len, flags, mode_out)` | those of a path, of the link itself with `flags & 1` |
461
462
 
463
+ WASIX's own terminal calls carry only echo and line buffering (`tty_set`) and answer for "the" terminal whatever the descriptor (`tty_get`), so a WASIX program's `cfmakeraw` would leave `^C` a signal and its output post-processed. The kernel adds the import module `slicc_tty`, with the full Linux `struct termios` (musl's 60-byte layout: four `u32` flag words, `c_line`, `c_cc[32]`, then input and output speed; speeds read as `B38400` and are ignored when set) and answers per descriptor, again as a preview1 errno (`ENOTTY` for a descriptor that is no terminal, `EBADF` for one that isn't open, `ENOSYS` from kernels without them):
464
+
465
+ | Import | Does |
466
+ | --- | --- |
467
+ | `tcgetattr(fd, termios_out)` | writes the terminal's termios |
468
+ | `tcsetattr(fd, actions, termios)` | sets every flag and `c_cc` as given (unknown bits are kept and read back), at once for `TCSANOW`, `TCSADRAIN` and `TCSAFLUSH` (0 to 2; anything else is `EINVAL`) |
469
+ | `winsize(fd, winsize_out)` | writes `struct winsize` (`u16` rows, columns, then zero pixels) |
470
+
471
+ A wasix-sysroot that builds `tcgetattr`, `tcsetattr`, `isatty` and `ioctl(TCGETS, TCSETS*, TIOCGWINSZ)` on them gives WASIX programs (python's `tty.setraw`, curses, ruby's `IO#raw`) a really raw terminal. Stock `tty_get` answers `ENOTTY` when none of the process's descriptors is a terminal, instead of an 80×24 terminal that isn't there, so stdio on pipes and files isn't taken for a terminal.
472
+
462
473
  Lowering a soft resource limit (`ulimit -S -n 64`) still fails with `EPERM` in Emscripten programs: their libc's `setrlimit` is a stub compiled into the program, not a call to the kernel.
463
474
 
464
475
  ### Users
@@ -8131,9 +8131,9 @@ var KernelTty = class {
8131
8131
  this.line = [];
8132
8132
  } else if (byte === 10) {
8133
8133
  this.line.push(byte);
8134
- if (echo) this.output(encoder2.encode("\n"));
8135
8134
  this.readable.push(Uint8Array.from(this.line));
8136
8135
  this.line = [];
8136
+ if (echo) this.output(encoder2.encode("\n"));
8137
8137
  } else {
8138
8138
  this.line.push(byte);
8139
8139
  if (echo) this.echo(byte);
@@ -9494,6 +9494,7 @@ var WasmProcess = class {
9494
9494
  }
9495
9495
  signal(sig) {
9496
9496
  if (this.execChild !== void 0) {
9497
+ if (sig === SIG.CONT) this.cont();
9497
9498
  void Promise.resolve(this.options.kill?.(this.execChild, sig)).catch(() => void 0);
9498
9499
  if (sig !== SIG.KILL) return "forward";
9499
9500
  if (!this.options.writesBack?.(this.execChild)) return "terminate";
@@ -9874,7 +9875,7 @@ var WasmProcess = class {
9874
9875
  case "tty-pgrp-get":
9875
9876
  return { ok: true, kind: "json", json: jobs ? jobs.tcgetpgrp(tty, this.sid()) : this.pid };
9876
9877
  case "tty-pgrp-set":
9877
- if (jobs) jobs.tcsetpgrp(this.pid, tty, req.pgrp);
9878
+ if (jobs) jobs.tcsetpgrp(this.pid, tty, req.pgrp, this.options.settling?.() ?? false);
9878
9879
  else if (req.pgrp !== this.pid) throw new KernelError("EPERM");
9879
9880
  return { ok: true, kind: "void" };
9880
9881
  }
@@ -10259,6 +10260,7 @@ function processOptions(opts, header2, hooks) {
10259
10260
  ...inherited(opts),
10260
10261
  ...opts.identity ? { identity: opts.identity } : {},
10261
10262
  ...opts.onSyscall ? { onSyscall: opts.onSyscall } : {},
10263
+ ...opts.settling ? { settling: opts.settling } : {},
10262
10264
  spawner: opts.spawner,
10263
10265
  forker: opts.forker,
10264
10266
  fs: opts.fs,
@@ -10431,6 +10433,7 @@ function spawnWasmProcess(opts) {
10431
10433
  // src/kernel/jobs.ts
10432
10434
  var JobTable = class {
10433
10435
  members = /* @__PURE__ */ new Map();
10436
+ handoffs = /* @__PURE__ */ new Map();
10434
10437
  foreground = /* @__PURE__ */ new Map();
10435
10438
  terminals = /* @__PURE__ */ new Map();
10436
10439
  add(pid, parentPid, signal, terminal) {
@@ -10450,7 +10453,12 @@ var JobTable = class {
10450
10453
  return [...this.members.values()];
10451
10454
  }
10452
10455
  remove(pid) {
10456
+ const member = this.members.get(pid);
10453
10457
  this.members.delete(pid);
10458
+ if (member) this.prune(member.pgid);
10459
+ }
10460
+ prune(pgid) {
10461
+ if (![...this.members.values()].some((m) => m.pgid === pgid)) this.handoffs.delete(pgid);
10454
10462
  }
10455
10463
  exec(pid, child, adopt = false) {
10456
10464
  const member = this.members.get(child);
@@ -10514,7 +10522,12 @@ var JobTable = class {
10514
10522
  if (id2 === target.sid) throw new KernelError("EPERM");
10515
10523
  const exists = [...this.members.values()].some((m) => m.pgid === group && m.sid === target.sid);
10516
10524
  if (group !== id2 && !exists) throw new KernelError("EPERM");
10525
+ const left = target.pgid;
10517
10526
  target.pgid = group;
10527
+ this.prune(left);
10528
+ }
10529
+ get handoffCount() {
10530
+ return this.handoffs.size;
10518
10531
  }
10519
10532
  getpgid(caller, pid) {
10520
10533
  return this.byId(pid || this.idOf(caller)).pgid;
@@ -10567,12 +10580,24 @@ var JobTable = class {
10567
10580
  tcgetpgrp(tty, fallback) {
10568
10581
  return this.foreground.get(tty) ?? fallback;
10569
10582
  }
10570
- tcsetpgrp(caller, tty, pgid) {
10583
+ tcsetpgrp(caller, tty, pgid, settling = false) {
10571
10584
  const self = this.member(caller);
10572
10585
  const inSession = [...this.members.values()].some((m) => m.pgid === pgid && m.sid === self.sid);
10573
10586
  if (!inSession) throw new KernelError("EPERM");
10587
+ const current = this.foreground.get(tty);
10588
+ if (settling && pgid === self.pgid && current !== pgid && this.reclaimed(pgid, self.ppid))
10589
+ return;
10590
+ if (pgid !== self.pgid) this.handoffs.set(pgid, { by: caller, reclaimed: false });
10591
+ else if (current !== void 0) {
10592
+ const handoff = this.handoffs.get(current);
10593
+ if (handoff?.by === caller) handoff.reclaimed = true;
10594
+ }
10574
10595
  this.foreground.set(tty, pgid);
10575
10596
  }
10597
+ reclaimed(pgid, parent) {
10598
+ const handoff = this.handoffs.get(pgid);
10599
+ return handoff?.reclaimed === true && handoff.by === parent;
10600
+ }
10576
10601
  signalOwnedForeground(tty, sig) {
10577
10602
  const pgid = this.foreground.get(tty);
10578
10603
  if (pgid !== void 0) this.killGroup(pgid, sig);
@@ -10616,6 +10641,9 @@ var SettlingChildren = class {
10616
10641
  this.parked = this.parked.filter((p) => p.pgid !== pgid);
10617
10642
  this.held = this.held.filter((h) => !(h.copy && h.pgid === pgid));
10618
10643
  }
10644
+ isSettling(pid) {
10645
+ return this.children.has(pid);
10646
+ }
10619
10647
  syscall(pid, op) {
10620
10648
  const child = this.children.get(pid);
10621
10649
  if (!child) return;
@@ -12222,6 +12250,7 @@ var Launcher = class {
12222
12250
  this.decide(pid);
12223
12251
  this.settling.syscall(pid, call.op);
12224
12252
  },
12253
+ settling: () => this.settling.isSettling(pid),
12225
12254
  ...req.ignored ? { ignored: req.ignored } : {},
12226
12255
  ...req.umask !== void 0 ? { umask: req.umask } : {},
12227
12256
  cred: req.cred,