@ai-ecoverse/slicc-kernel 1.73.2 → 1.74.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
@@ -270,14 +270,14 @@ The module's default export (or its `main` export) is called with a context and
270
270
  - `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`). `/dev/shm` and `/dev/ptmx` aren't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Creating, removing or renaming names in `/dev` goes to the root file system as before;
271
271
  - `signals`: `on(signal, handler)` catches a signal (by number or as `'SIGINT'`), `ignore(signal)` ignores it and `reset(signal)` restores its default action. A call blocked in a read or write is restarted after a handler runs; signals without a handler keep their default actions, so an uncaught `SIGINT` ends the program with 130 and `SIGKILL` always does;
272
272
  - `sync`: `read`, `write`, `close`, `open` (whose files' methods are synchronous too) and `fs`, made synchronously, for code that cannot `await`, such as a compiled program's file system or a `readFileSync`. A synchronous call holds the worker until the kernel answers, so nothing else of the program runs meanwhile, timers and callbacks included; a signal that arrives during a blocked read or write runs its handler, and the call is then restarted. Descriptors are shared with the rest of the context, so a file opened with `sync.open` can be closed with `close`, and the other way around; `sync.run` and `sync.exit` are described next;
273
- - `sync.run({ argv, env, cwd, input, stdin, stdout, stderr, maxBuffer, timeout, killSignal })` runs a command to its end and answers `{ pid, status, signal, stdout, stderr, error }`, as Node's `spawnSync` does. It suits `execSync`, `spawnSync` and `execFileSync`. The kernel does the whole run while the program's worker waits; other processes go on.
273
+ - `sync.run({ argv, env, cwd, pgid, session, input, stdin, stdout, stderr, maxBuffer, timeout, killSignal })` runs a command to its end and answers `{ pid, status, signal, stdout, stderr, error }`, as Node's `spawnSync` does. `pgid` and `session` work as for `spawn` below; a group that doesn't exist or is another session's answers `error: { code: 'ESRCH' }` or `'EPERM'`, and a bad option throws `EINVAL`. It suits `execSync`, `spawnSync` and `execFileSync`. The kernel does the whole run while the program's worker waits; other processes go on.
274
274
  - Each stdio is `'pipe'` (the default), `'ignore'`, `'inherit'` (the program's own, a terminal included), or one of the program's descriptors by number, which the child gets as it is (two slots on one descriptor share it, so their writes keep their order). `stderr: 'stdout'` sends stderr to whatever stdout is, the stdout capture included, in the order the child writes. With `'pipe'`, stdin gets `input` (bytes, or a string as UTF-8) and then its end, so `cat` stops; `stdout` and `stderr` come back as bytes and are absent for the other choices. Both are drained while the child runs, so a child that writes more than a pipe holds never stalls. As in Node, the call returns once the child has exited and every captured stream has reached its end, so output a background grandchild writes later is still collected; `timeout` bounds that wait too, so, as with Node's `spawnSync`, a run blocks for as long as a background process keeps a captured stream open unless `timeout` is set; pass one for a command that may leave something in the background. Once the outcome is decided (the end of output, an overflow or a timeout), the captured streams are closed, so a writer still left on them gets `EPIPE` (and `SIGPIPE`) instead of running on.
275
275
  - `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.
276
276
  - 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.
277
277
  - 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;
278
278
  - `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). 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);
279
279
  - `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;
280
- - `spawn({ argv, env, cwd, 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. A command that does not exist rejects with `ENOENT`;
280
+ - `spawn({ argv, env, cwd, pgid, session, 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 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`;
281
281
  - `wait(pid)` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
282
282
  - `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;
283
283
  - `net`: `listen({ port, host, backlog })` and `connect({ host, port, signal })` (`host` defaults to `127.0.0.1`) on the kernel's loopback, the same sockets WASIX programs use, so a JS server is reached by other processes, by the page as `<port>.kernel.localhost` (`loopbackFetch`, `dial`) and by attached clients. A listener has its `port` (as bound, so port `0` picks a free one), `accept({ signal })`, `close()` and `for await` over its connections. A connection has `local` and `remote` addresses, `readable` and `writable` web streams (closing the writable shuts down the sending side, so the peer reads the end), and `close()`. The loopback is IPv4 (`127.0.0.1` by default, or `0.0.0.0`), so an IPv6 host such as `::1` fails with `EADDRNOTAVAIL` for both `listen` and `connect`, and binding and connecting fail as they would for a WASIX program (`EADDRINUSE`, `ECONNREFUSED`, `EADDRNOTAVAIL`). A blocked accept, read or write waits without holding anything in the kernel, so aborting it or closing the socket leaves nothing behind. A write to a connection the peer closed fails with `EPIPE` and no `SIGPIPE`;
@@ -650,7 +650,7 @@ The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#us
650
650
  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:
651
651
 
652
652
  - mode bits (`chmod`);
653
- - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks;
653
+ - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
654
654
  - inode numbers that stay stable across renames;
655
655
  - symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar, with their own times: `touch -h`, `lutimes` and `utimensat` with `AT_SYMLINK_NOFOLLOW` set the link's, never the target's. A link's own mode can't be changed, as on Linux: `chmod -h`, `lchmod` and `fchmodat` with `AT_SYMLINK_NOFOLLOW` fail with `EOPNOTSUPP` (which `chmod -h` and `tar` ignore for links) and never follow it, and `open` with `O_PATH | O_NOFOLLOW` gives a descriptor for the link itself.
656
656
 
@@ -1071,13 +1071,10 @@ var OpfsFs = class {
1071
1071
  this.dirDirty.delete(path);
1072
1072
  const ms = this.dirTimes.get(path);
1073
1073
  if (ms === void 0) continue;
1074
- await this.meta.update(path, (entry) => ({
1075
- ...entry,
1074
+ await this.meta.update(
1076
1075
  path,
1077
- mtimeMs: ms,
1078
- mtimeFor: 0,
1079
- ctimeMs: ms
1080
- }));
1076
+ (entry) => (entry?.timesAt ?? -1) >= ms ? entry : { ...entry, path, mtimeMs: ms, mtimeFor: 0, ctimeMs: ms }
1077
+ );
1081
1078
  if (this.dirTimes.get(path) === ms) this.dirTimes.delete(path);
1082
1079
  }
1083
1080
  }
@@ -1531,7 +1528,8 @@ var OpfsFs = class {
1531
1528
  atimeMs: atime.getTime(),
1532
1529
  mtimeMs: mtime.getTime(),
1533
1530
  mtimeFor: file?.lastModified ?? 0,
1534
- ctimeMs
1531
+ ctimeMs,
1532
+ timesAt: ctimeMs
1535
1533
  }));
1536
1534
  }
1537
1535
  async reconcile() {
@@ -8785,6 +8783,36 @@ function revokeSyncFsToken(token) {
8785
8783
  }
8786
8784
 
8787
8785
  // src/realm/sync-fs-wire.ts
8786
+ var SYNC_FS_OPS = [
8787
+ "read",
8788
+ "write",
8789
+ "exists",
8790
+ "stat",
8791
+ "lstat",
8792
+ "readdir",
8793
+ "readdir-stat",
8794
+ "mkdir",
8795
+ "rm",
8796
+ "rename",
8797
+ "link",
8798
+ "unlink",
8799
+ "rmdir",
8800
+ "symlink",
8801
+ "readlink",
8802
+ "chmod",
8803
+ "utimes",
8804
+ "lutimes",
8805
+ "statfs",
8806
+ "hold",
8807
+ "create",
8808
+ "kept",
8809
+ "pread",
8810
+ "pwrite",
8811
+ "truncate",
8812
+ "chown",
8813
+ "lchown",
8814
+ "access"
8815
+ ];
8788
8816
  var HOLD_READ = 4;
8789
8817
  var HOLD_WRITE = 8;
8790
8818
  var HOLD_CREATED = 16;
@@ -9647,6 +9675,9 @@ var AsyncRangedFile = class {
9647
9675
  }
9648
9676
  };
9649
9677
 
9678
+ // src/realm/sync-sab-bridge.ts
9679
+ var PATH_OPS = new Set(SYNC_FS_OPS);
9680
+
9650
9681
  // src/realm/live-vfs-fs.ts
9651
9682
  var EARLY_WHOLE_FILE = 64 * 1024;
9652
9683
 
@@ -11144,7 +11175,14 @@ async function runChild(req, deps) {
11144
11175
  ];
11145
11176
  try {
11146
11177
  pid = await deps.children.spawn(
11147
- { file: req.file, argv: req.argv, env: req.env, cwd: req.cwd },
11178
+ {
11179
+ file: req.file,
11180
+ argv: req.argv,
11181
+ env: req.env,
11182
+ cwd: req.cwd,
11183
+ ...req.pgid !== void 0 ? { pgid: req.pgid } : {},
11184
+ ...req.session ? { session: req.session } : {}
11185
+ },
11148
11186
  stdio
11149
11187
  );
11150
11188
  } catch (e) {
@@ -12097,8 +12135,9 @@ var WasmProcess = class {
12097
12135
  return "deliver";
12098
12136
  }
12099
12137
  async spawn(req) {
12100
- const { file, argv, env, cwd, stdio, inherit: inherit2, exec } = req;
12101
- const spawned = { file, argv, env, cwd, ...exec ? { exec } : {} };
12138
+ const { file, argv, env, cwd, stdio, inherit: inherit2, exec, pgid, session } = req;
12139
+ const group = { ...pgid !== void 0 ? { pgid } : {}, ...session ? { session } : {} };
12140
+ const spawned = { file, argv, env, cwd, ...group, ...exec ? { exec } : {} };
12102
12141
  const pid = await this.children.spawn(spawned, stdio, inherit2);
12103
12142
  if (!exec) return pid;
12104
12143
  if (this.killed) void Promise.allSettled([this.options.kill?.(pid, SIG.KILL)]);
@@ -13392,6 +13431,9 @@ var JobTable = class {
13392
13431
  member.pgid = pgid;
13393
13432
  member.sid = sid;
13394
13433
  }
13434
+ lead(pid) {
13435
+ this.member(pid).pgid = this.idOf(pid);
13436
+ }
13395
13437
  pgidOf(pid) {
13396
13438
  return this.members.get(pid)?.pgid;
13397
13439
  }
@@ -16377,7 +16419,7 @@ var Launcher = class {
16377
16419
  start(req) {
16378
16420
  this.sampling();
16379
16421
  if (req.pgid !== void 0 && this.jobs.sessionOf(req.pgid) === void 0) {
16380
- throw fsError("ESRCH", `no process group ${req.pgid}`);
16422
+ throw new SpawnError("ESRCH");
16381
16423
  }
16382
16424
  const pid = this.nextPid++;
16383
16425
  const stack = this.stackFor(req.tag);
@@ -16452,6 +16494,8 @@ var Launcher = class {
16452
16494
  this.jobs.add(pid, req.ppid, (sig) => handle.signal(sig), terminal);
16453
16495
  if (req.exec && req.ppid !== void 0) this.jobs.exec(req.ppid, pid, true);
16454
16496
  if (req.pgid !== void 0) this.jobs.join(pid, req.pgid);
16497
+ if (req.group === "lead") this.jobs.lead(pid);
16498
+ if (req.group === "session") this.jobs.setsid(pid);
16455
16499
  this.described.set(pid, {
16456
16500
  argv: [req.argv0, ...req.args],
16457
16501
  tty: terminal?.name ?? null,
@@ -16613,6 +16657,11 @@ var Launcher = class {
16613
16657
  await fds.closeAll();
16614
16658
  throw new SpawnError("EACCES");
16615
16659
  }
16660
+ const group = this.childGroup(ppid, req);
16661
+ if (typeof group === "string") {
16662
+ await fds.closeAll();
16663
+ throw new SpawnError(group);
16664
+ }
16616
16665
  const cred = grant ? grant.cred : this.credOf(ppid);
16617
16666
  const scope = grant ? "" : await this.scopeOf(cred.euid);
16618
16667
  const planned = await this.plan(req.file, req.argv, req.cwd, scope);
@@ -16639,6 +16688,7 @@ var Launcher = class {
16639
16688
  umask: this.umaskOf(ppid),
16640
16689
  cred,
16641
16690
  tag: this.tagOf(ppid),
16691
+ ...group,
16642
16692
  ...exec
16643
16693
  });
16644
16694
  deciding?.attach();
@@ -16646,6 +16696,17 @@ var Launcher = class {
16646
16696
  return childHandle(handle);
16647
16697
  };
16648
16698
  }
16699
+ childGroup(ppid, req) {
16700
+ const { pgid, session } = req;
16701
+ if (pgid === void 0 && session === void 0) return {};
16702
+ const valid = pgid === void 0 || Number.isInteger(pgid) && pgid >= 0;
16703
+ if (req.exec || !valid || session !== void 0 && session !== true) return "EINVAL";
16704
+ if (session) return pgid === void 0 ? { group: "session" } : "EINVAL";
16705
+ if (pgid === 0) return { group: "lead" };
16706
+ const sid = this.jobs.sessionOf(pgid);
16707
+ if (sid === void 0) return "ESRCH";
16708
+ return sid === this.jobs.sidOf(ppid) ? { pgid } : "EPERM";
16709
+ }
16649
16710
  undecided(ppid) {
16650
16711
  const decided = Promise.withResolvers();
16651
16712
  return {
@@ -16676,7 +16737,7 @@ var Launcher = class {
16676
16737
  }, 0);
16677
16738
  }
16678
16739
  forker(ppid, parent) {
16679
- const { exec: _exec, ...image } = parent;
16740
+ const { exec: _exec, pgid: _pgid, group: _group, ...image } = parent;
16680
16741
  return async (state, fds) => childHandle(
16681
16742
  this.start({
16682
16743
  ...image,