@ai-ecoverse/slicc-kernel 1.125.10 → 1.125.12

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
@@ -321,7 +321,7 @@ On top of preview1 a WASI program gets:
321
321
  - `pthread_kill` to another thread gives `SI_TKILL` (-6) from the process. wasix-sysroot's `pthread_kill` of the calling thread itself goes through `kill(getpid())` and so arrives as `SI_USER`, until its libc takes the thread route for itself too;
322
322
  - `SIGCHLD` gives `CLD_EXITED` (1) with the exit status, `CLD_KILLED` (2) with the signal, `CLD_STOPPED` (5) or `CLD_CONTINUED` (6), with the child's pid and real uid;
323
323
  - a terminal's ^C/^Z/^\, `alarm` and the kernel's own signals give `SI_KERNEL` (0x80) with pid and uid 0, and so does a `sigwait_info` take with nothing recorded;
324
- - a standard signal doesn't queue, so the first pending instance's sender is kept until the signal is taken.
324
+ - a standard signal doesn't queue, so the first pending instance's sender is kept until the signal is taken. A caught signal sent again while it's still pending coalesces with it: the handler runs once and the program goes on, as on Linux ([#1088](https://github.com/ai-ecoverse/slicc-kernel/issues/1088)). One deliberate deviation: a second `SIGINT`, `SIGTERM`, `SIGQUIT` or `SIGHUP` while the first is still pending **and the program has made no kernel call since** ends the program with the signal's default action. A WASI program that computes without making calls can't run a handler, so this is what lets a second ^C end it (a Python `while True: pass`). Timer and user signals (`SIGALRM`, `SIGUSR1` …) always coalesce, and so does a signal the program has blocked, or is still handling, while it makes calls.
325
325
  A kernel without the imports answers `ENOSYS`, and the libc reports `SI_USER` with pid and uid 0, as before. **SIGCHLD:** with `SIGCHLD` set to `SIG_IGN` (by the program, or inherited), or with `SA_NOCLDWAIT` in the flags `slicc.sigaction_set` gets for `SIGCHLD`, an exited child is reaped at once and never stays a zombie: `waitpid` for it is `ECHILD`, and `wait` blocks until every child has exited, then fails with `ECHILD`, as on Linux. `SIGCHLD` is still sent. An explicit `SIG_DFL` turns it back off. A child that a process waits on for `exec`, or that `kernel.run` and the run calls collect, is never reaped that way.
326
326
  - **Pipes** have no file type for WASI and WASIX programs' `fstat` (`st_mode` type 0), because WASI has no FIFO file type. A socket type, as kernels 1.47.3 to 1.48.1 gave them, made Ruby treat a `popen` pipe as a socket (`close_write` then never ends the child's input). The kernel's `slicc_fs.fd_mode` answers `S_IFIFO | 0600` (Linux numbering) for a pipe, and wasix-sysroot 2025.9.30-21 and later take the type from it, so programs built on it see a FIFO (`S_ISFIFO`); older libcs keep only the permission bits. Until then Python's asyncio refuses a pipe for a subprocess (`Pipe transport is only for pipes, sockets and character devices`). They are read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. Emscripten programs, bash included, see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output.
327
327
  - **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream or datagram 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. A write to a pipe, FIFO or terminal whose reader is gone follows the program's `SIGPIPE` disposition, as the kernel knows it (#810): at the default it ends the program with status 141; ignored (also an ignore inherited across `exec` and spawn, as bash's `trap '' PIPE` passes on), the write fails with `EPIPE`; with a handler, the handler runs on the thread that wrote and then the write fails with `EPIPE` (#820); blocked, `SIGPIPE` stays pending and the write fails with `EPIPE`. An Emscripten program's write to a pipe, and its `send` without `MSG_NOSIGNAL`, follow the same rules, except that the kernel doesn't see an Emscripten program's signal mask, so a blocked `SIGPIPE` there still ends it. A WASIX program's libc reads an inherited ignore back through `slicc.sigaction_inherited` (wasix-sysroot -32 and later; see below). An Emscripten program's shim reads it from `Module.sliccKernel.sigInherited()` (#965, homescoop#973). That's a mask with bit n set for signal n (not bit n-1, as the WASIX import has it) holding only inherited `SIG_IGN`s, never `SIGKILL` or `SIGSTOP`. The shim seeds its `sigaction` table from it in a constructor that runs before any user constructor. The kernel puts the function on `Module.sliccKernel` before the glue runs constructors, ahead of the rest of `sliccKernel`. On a kernel without the function it is undefined, and the shim keeps the defaults. 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. `sock_open(SOCK_DGRAM)` on `AF_INET`/`AF_INET6` gives a [datagram socket](#datagram-sockets-udp); `sock_send_to` and `sock_recv_from` carry its addresses, and `RECV_DATA_TRUNCATED` is set in `ro_flags` when a datagram did not fit.
@@ -383,10 +383,19 @@ The module's default export (or its `main` export) is called with a context and
383
383
  - `status` is the exit status, or `null` with `signal` set when a signal ended the child. A non-zero status is not an error. A command that cannot start (it does not exist, or is not executable) answers `error: { code: 'ENOENT' }` (or the errno), `status: null` and `pid: 0` instead of throwing.
384
384
  - More than `maxBuffer` bytes on `stdout` or `stderr` (1 MiB by default) sends the child `killSignal` (`SIGTERM` by default), keeps the first `maxBuffer` bytes, and answers `error: { code: 'ENOBUFS' }`. The kernel keeps at most 64 MiB of each stream whatever `maxBuffer` says, `Infinity` included, since the buffers live in the kernel worker that all processes share. `timeout` (ms) sends `killSignal` when it runs out and answers `error: { code: 'ETIMEDOUT' }`. A child that ignores `killSignal` is still waited for, as in Node.
385
385
  - The child is in the program's process group, so `^C` reaches it. The kernel reaps it, so it leaves no zombie, and its CPU time counts as the program's children's. A signal the program catches does not cut the run short; its handler runs when the call returns. A signal that kills the program kills it as usual;
386
- - `sync.spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr })` starts a child as `spawn` below does, with the same options and the same child, but synchronously: it returns `{ pid, stdin, stdout, stderr, wait(), kill(signal) }` at once, so a runtime can set Node's `child.pid` before `spawn()` returns. A spawn error (`ENOENT`, `EACCES`, `ESRCH` or `EPERM` for a group, `EINVAL` for a bad option, `EBADF` for a descriptor the program doesn't have) is thrown at once, with its `code`. The streams, `wait()` and `kill()` are the asynchronous ones, used afterwards from async code; the child doesn't wait for the program to read, since its output goes to a kernel pipe, which holds what it writes until the program reads it (a child that fills the pipe waits, as with `spawn`);
386
+ - `sync.spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a child as `spawn` below does, with the same options and the same child, but synchronously: it returns `{ pid, stdin, stdout, stderr, wait(), kill(signal) }` at once, so a runtime can set Node's `child.pid` before `spawn()` returns. A spawn error (`ENOENT`, `EACCES`, `ESRCH` or `EPERM` for a group, `EINVAL` for a bad option, `EBADF` for a descriptor the program doesn't have) is thrown at once, with its `code`. The streams, `wait()` and `kill()` are the asynchronous ones, used afterwards from async code; the child doesn't wait for the program to read, since its output goes to a kernel pipe, which holds what it writes until the program reads it (a child that fills the pipe waits, as with `spawn`);
387
387
  - `sync.kill(pid, signal)`, a synchronous `kill`. When the signal reaches the program itself (its own pid, `0` for its process group, or `-pgid` of its own group) and its action is to end the program (no handler, not ignored), the call never returns: the program's writes reach their descriptors and its terminal modes are put back, as for `sync.exit`, then the rest of the group is signalled and the program ends by the signal, so a parent's `wait` and `sync.run` report `signal`, as Node does for `process.kill(process.pid)`. A signal with a handler runs it when the call returns, an ignored one does nothing, and a stop signal stops the program. A signal whose default action dumps core (`SIGQUIT`, `SIGABRT`, `SIGSEGV`, …) ends it the same way; the kernel writes no core and sets no core flag in the wait status. `-1` signals every process the program may signal except itself and pid 1, as on Linux, and returns (`ESRCH` when there was none, `EPERM` when there were some but it may signal none of them); a process that exec'd gets the signal once. Without users, every process runs as root, so `kill -9 -1` from any program ends every other process; with users it reaches only what the sender may signal (its own uid's processes, or all of them as root);
388
388
  - `sync.exit(status)`, which ends the process at once, as Node's `process.exit()` does: no `catch`, `finally`, microtask, timer or signal handler runs after it, because the call never returns. First the writes the program has made reach their descriptors, in order (a broken pipe drops the rest for that descriptor), and terminal modes the program set with `tty` are put back. Then the process ends the way any exit does: its descriptors are closed and written back, its parent gets `SIGCHLD`, and `wait` reports `status`. Children keep running. A JS program has no threads of its own (`Worker` is withheld), so nothing of it outlives the call. `exit(status)` (below) unwinds instead, so the program's `catch` and `finally` blocks run;
389
- - `spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The command run is `argv[0]`; `argv0`, when given, is the `argv[0]` the child sees instead, as given (no basename; an empty string too), as with Node's `options.argv0`: `spawn({ argv: ['/bin/sh', '-c', 'echo $0'], argv0: '/bin/sh' })` prints `/bin/sh`, where without it the child sees the command's own name (`sh`). A command whose package manifest sets its `argv0` (a multi-call program's applets, such as coreutils') keeps that one. An `argv0` that isn't a string is `EINVAL`. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`. `parentDeathSignal` (a number or `'SIGTERM'`, as Linux's `PR_SET_PDEATHSIG`) is sent to the child when the program ends, killed or not: to the child's whole group when it leads one (`pgid: 0` or `session: true`), else to the child alone. It's sent with the program's credentials, so a child the program may no longer signal (one that gained privilege) doesn't get it. If the program has already ended by the time the spawn completes, it's sent at once. A child of its own (not a group leader) that has been waited for gets nothing, and `sync.run` has no such option. A bad signal is `EINVAL`;
389
+ - `socketpair()` (and `sync.socketpair()`) answers `[a, b]`, two descriptors of the program on a connected `AF_UNIX` stream pair, and `pipe()` (and `sync.pipe()`) answers `[readFd, writeFd]` ([#1093](https://github.com/ai-ecoverse/slicc-kernel/issues/1093)).
390
+ - **The same descriptors either way:** the async and `sync` forms give the program's own kernel descriptors, which `read`, `write`, `close` and their `sync` forms all take, until the program closes them or ends.
391
+ - **Reading** a socket or pipe whose peer has closed gives an empty `Uint8Array` (end of file). A `read` still pending when the program `close`s that descriptor rejects with `EBADF` instead of waiting. A `read` and a `write` on one socket can be pending at the same time, as a socket is full duplex.
392
+ - **Writing to a socket whose peer is gone** fails with `EPIPE` and raises no `SIGPIPE`, at the default disposition too, as for WASIX and Emscripten programs (as if `MSG_NOSIGNAL` were set). A pipe's broken write follows `SIGPIPE` as described below.
393
+ - **`fds`**, an option of `spawn` and `sync.spawn`, gives the child more descriptors: `{ 3: f }` puts the program's descriptor `f` at the child's fd 3, on the same open file description, as `dup2` would.
394
+ - **Keys:** each key must be an integer of 3 or more, as stdio stays `stdin`, `stdout` and `stderr`. Each value must be an open descriptor of the program. Anything else is `EINVAL` before the child starts.
395
+ - **Only the named descriptors reach the child.** Others the program holds, the other end of a pair included, don't, and a grandchild gets fd 3 only if the child names it again.
396
+ - **The parent's copy:** the program keeps `f` and closes it itself when it no longer needs it, so the child sees end of file once it's the only one left.
397
+ - **Why a `Record`:** it can't name a child fd twice, and it reads like Node's `stdio` index, so it's this rather than a list of `{ fd, from }`.
398
+ - `spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The command run is `argv[0]`; `argv0`, when given, is the `argv[0]` the child sees instead, as given (no basename; an empty string too), as with Node's `options.argv0`: `spawn({ argv: ['/bin/sh', '-c', 'echo $0'], argv0: '/bin/sh' })` prints `/bin/sh`, where without it the child sees the command's own name (`sh`). A command whose package manifest sets its `argv0` (a multi-call program's applets, such as coreutils') keeps that one. An `argv0` that isn't a string is `EINVAL`. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`. `parentDeathSignal` (a number or `'SIGTERM'`, as Linux's `PR_SET_PDEATHSIG`) is sent to the child when the program ends, killed or not: to the child's whole group when it leads one (`pgid: 0` or `session: true`), else to the child alone. It's sent with the program's credentials, so a child the program may no longer signal (one that gained privilege) doesn't get it. If the program has already ended by the time the spawn completes, it's sent at once. A child of its own (not a group leader) that has been waited for gets nothing, and `sync.run` has no such option. A bad signal is `EINVAL`;
390
399
  - `wait(pid, { untraced })` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; with `untraced: true` it also resolves when the child stops, with `{ status: 128 + signal, signal, stopped: true }`, as `waitpid` with `WUNTRACED` (a child's `wait({ untraced })` is the same), and options that aren't an object are `EINVAL`; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
391
400
  - `sudo(args)`, what the [`sudo`](#sudo) command calls: `args` are its arguments after `sudo`, and it resolves with `{ pid }`, the command the kernel started as the program's child, to `wait` for; `{ run }`, an argv to run as the caller (with `users: 'off'`); `{ list }`, the text of `sudo -l`; or `{ error, errno }`, the message to print;
392
401
  - `crontab({ action, user?, text? })`, what the [`crontab`](#cron) command calls: `action` is `'list'`, `'remove'` or `'install'` (with the table as `text`), on the caller's own table or, for an effective uid of 0, on `user`'s. It resolves with `{ text }` (the table for `list`, `''` otherwise) or `{ error }`, the message to print. A malformed call is `EINVAL`;
@@ -1096,6 +1096,7 @@ var ABSENT_NAMES = 4096;
1096
1096
  var FILE_HANDLES = 4096;
1097
1097
  var STALE = /* @__PURE__ */ new Set(["NotFoundError", "TypeMismatchError"]);
1098
1098
  var REPLACED_ATTEMPTS = 4;
1099
+ var RECONCILE_LISTINGS = 8;
1099
1100
  var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
1100
1101
  var ERRNO_BY_DOM_ERROR = {
1101
1102
  NotFoundError: "ENOENT",
@@ -2455,15 +2456,57 @@ var OpfsFs = class {
2455
2456
  timesBy: this.id
2456
2457
  }));
2457
2458
  }
2459
+ async present(paths) {
2460
+ const listed = /* @__PURE__ */ new Map();
2461
+ const done4 = /* @__PURE__ */ new Map();
2462
+ const list = (dir) => {
2463
+ const known = listed.get(dir);
2464
+ if (known !== void 0) return known;
2465
+ const listing = (async () => {
2466
+ const above = dir === "/" ? void 0 : await list(parentOf3(dir));
2467
+ if (above === null) return null;
2468
+ const handle = dir === "/" ? this.root : above?.get(baseOf(dir));
2469
+ if (handle?.kind !== "directory") return void 0;
2470
+ this.calls++;
2471
+ const children3 = /* @__PURE__ */ new Map();
2472
+ try {
2473
+ for await (const child of handle.values())
2474
+ children3.set(child.name, child);
2475
+ } catch {
2476
+ return null;
2477
+ }
2478
+ return children3;
2479
+ })().then((children3) => {
2480
+ done4.set(dir, children3);
2481
+ return children3;
2482
+ });
2483
+ listed.set(dir, listing);
2484
+ return listing;
2485
+ };
2486
+ const dirs = [...new Set(paths.filter((p) => split(p).length > 0).map(parentOf3))];
2487
+ let next3 = 0;
2488
+ const lister = async () => {
2489
+ while (next3 < dirs.length) await list(dirs[next3++]);
2490
+ };
2491
+ await Promise.all(Array.from({ length: Math.min(RECONCILE_LISTINGS, dirs.length) }, lister));
2492
+ return async (path) => {
2493
+ if (split(path).length === 0) return true;
2494
+ const listing = done4.get(parentOf3(path));
2495
+ if (listing === null) return this.real(path);
2496
+ return listing?.has(baseOf(path)) === true;
2497
+ };
2498
+ }
2458
2499
  async reconcile() {
2459
2500
  if (this.remote && await this.remote.peered()) return 0;
2460
2501
  let removed = 0;
2461
2502
  const entries = await this.meta.all();
2462
2503
  const carried = new Set(entries.filter((e) => !e.vacated).map((e) => e.ino));
2463
- for (const entry of entries) {
2464
- if (entry.vacated && carried.has(inodeOf(entry.path))) continue;
2465
- const real = await this.real(entry.path);
2466
- const stale = entry.link === void 0 ? !real : real || !await this.real(parentOf3(entry.path));
2504
+ const checked = entries.filter((e) => !(e.vacated && carried.has(inodeOf(e.path))));
2505
+ const real = await this.present(
2506
+ checked.flatMap((e) => e.link === void 0 ? [e.path] : [e.path, parentOf3(e.path)])
2507
+ );
2508
+ for (const entry of checked) {
2509
+ const stale = entry.link === void 0 ? !await real(entry.path) : await real(entry.path) || !await real(parentOf3(entry.path));
2467
2510
  if (!stale) continue;
2468
2511
  const seen = JSON.stringify(entry);
2469
2512
  await this.meta.update(
@@ -6656,7 +6699,7 @@ function satisfies(version, range2) {
6656
6699
  }
6657
6700
 
6658
6701
  // src/version.ts
6659
- var KERNEL_VERSION = true ? "1.125.10" : "0.0.0-dev";
6702
+ var KERNEL_VERSION = true ? "1.125.12" : "0.0.0-dev";
6660
6703
  var OS_RELEASE = "6.0.0-slicc";
6661
6704
  function procVersion() {
6662
6705
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -18449,9 +18492,21 @@ var VfsNode = class {
18449
18492
  }
18450
18493
  async keep() {
18451
18494
  const ranged2 = await this.prepare();
18452
- await ranged2?.pin();
18495
+ if (ranged2) await this.followed(() => ranged2.pin());
18453
18496
  }
18454
18497
  moving = /* @__PURE__ */ new Set();
18498
+ async followed(op) {
18499
+ for (; ; ) {
18500
+ const at = this.path;
18501
+ try {
18502
+ return await op();
18503
+ } catch (err) {
18504
+ if (!isMissing(err)) throw err;
18505
+ while (this.moving.size > 0) await Promise.all(this.moving);
18506
+ if (this.path === at) throw err;
18507
+ }
18508
+ }
18509
+ }
18455
18510
  async load() {
18456
18511
  if (this.revoked) throw new KernelError("EIO");
18457
18512
  while (!this.data) {
@@ -18497,7 +18552,7 @@ var VfsNode = class {
18497
18552
  }
18498
18553
  async pread(max, at) {
18499
18554
  const ranged2 = await this.prepare();
18500
- if (ranged2) return ranged2.read(at, max);
18555
+ if (ranged2) return this.followed(() => ranged2.read(at, max));
18501
18556
  const bytes = await this.load();
18502
18557
  const n = Math.max(0, Math.min(max, this.length - at));
18503
18558
  return bytes.slice(at, at + n);
@@ -18540,7 +18595,7 @@ var VfsNode = class {
18540
18595
  await this.fits(at + bytes.length);
18541
18596
  const ranged2 = await this.prepare();
18542
18597
  if (ranged2) {
18543
- await ranged2.write(at, bytes);
18598
+ await this.followed(() => ranged2.write(at, bytes));
18544
18599
  this.markDirty();
18545
18600
  return bytes.length;
18546
18601
  }
@@ -18609,8 +18664,9 @@ var VfsNode = class {
18609
18664
  this.dirty = false;
18610
18665
  const started = performance.now();
18611
18666
  try {
18612
- if (this.ranged) await this.ranged.flush();
18613
- else await this.fs.writeFile(this.path, this.data.slice(0, this.length));
18667
+ await this.followed(
18668
+ () => this.ranged ? this.ranged.flush() : this.fs.writeFile(this.path, this.data.slice(0, this.length))
18669
+ );
18614
18670
  } catch (err) {
18615
18671
  this.dirty = true;
18616
18672
  throw err;
@@ -19354,6 +19410,7 @@ var MAX_READ = 1024 * 1024;
19354
19410
  var O_NONBLOCK = 2048;
19355
19411
  var WRITE_ACCESS = 3;
19356
19412
  var KEYBOARD = /* @__PURE__ */ new Set([SIG.INT, SIG.QUIT, SIG.TSTP]);
19413
+ var STUCK_ESCAPES = /* @__PURE__ */ new Set([SIG.INT, SIG.TERM, SIG.QUIT, SIG.HUP]);
19357
19414
  var DEFAULT_UMASK2 = 18;
19358
19415
  function words(value2) {
19359
19416
  if (!Array.isArray(value2) || !value2.every((w) => typeof w === "string")) {
@@ -19410,6 +19467,8 @@ var WasmProcess = class {
19410
19467
  cred;
19411
19468
  execChild;
19412
19469
  killed = false;
19470
+ calls = 0;
19471
+ pendingAt = /* @__PURE__ */ new Map();
19413
19472
  execTermsig;
19414
19473
  stopped = 0;
19415
19474
  stops = 0;
@@ -19508,9 +19567,9 @@ var WasmProcess = class {
19508
19567
  const action = defaultAction(sig);
19509
19568
  return action === "stop" ? this.stop(sig) : action;
19510
19569
  }
19511
- if (((this.options.pendingBits?.() ?? 0) & bit) !== 0 && defaultAction(sig) === "terminate") {
19512
- return "terminate";
19513
- }
19570
+ if (((this.options.pendingBits?.() ?? 0) & bit) !== 0) {
19571
+ if (STUCK_ESCAPES.has(sig) && this.pendingAt.get(sig) === this.calls) return "terminate";
19572
+ } else this.pendingAt.set(sig, this.calls);
19514
19573
  this.options.onPending?.(sig);
19515
19574
  if (this.options.jobs?.keyboard && KEYBOARD.has(sig) && this.waiting.size === 0)
19516
19575
  this.owed = true;
@@ -19633,6 +19692,7 @@ var WasmProcess = class {
19633
19692
  }
19634
19693
  async syscall(req, cancelled, block) {
19635
19694
  this.options.onSyscall?.(req);
19695
+ this.calls += 1;
19636
19696
  const started = performance.now();
19637
19697
  let restarted = false;
19638
19698
  for (; ; ) {