@ai-ecoverse/slicc-kernel 1.68.0 → 1.70.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 +2 -1
- package/dist/kernel-worker.js +57 -7
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +525 -63
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +57 -7
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +522 -60
- package/dist/process-worker.js.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -250,10 +250,11 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
250
250
|
- `fdStatus(fd)` (`{ type, tty, seekable }`, `type` being `file`, `pipe`, `tty`, `socket`, `device` or `directory`) and `isatty(fd)`, to tell `< file` from a pipe;
|
|
251
251
|
- `open(path, { read, write, append, create, exclusive, truncate })`, which returns a file with `read(position, length)`, `write(position, bytes)`, `size()`, `truncate(size)`, `sync()` and `close()`. Reads and writes go through the kernel's paged files at those positions, so a large file is never read whole;
|
|
252
252
|
- `fs`: `stat`, `lstat`, `exists`, `readdir`, `mkdir` (with parents), `rm` (recursive), `unlink`, `rename`, `symlink`, `readlink`, and `readFile`/`writeFile` for small files;
|
|
253
|
+
- `/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;
|
|
253
254
|
- `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;
|
|
254
255
|
- `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;
|
|
255
256
|
- `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.
|
|
256
|
-
- Each stdio is `'pipe'` (the default), `'ignore'
|
|
257
|
+
- 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. 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.
|
|
257
258
|
- `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.
|
|
258
259
|
- 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.
|
|
259
260
|
- 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;
|
package/dist/kernel-worker.js
CHANGED
|
@@ -1991,6 +1991,16 @@ function written(device, bytes) {
|
|
|
1991
1991
|
if (device === "full" && bytes.length > 0) throw new KernelError("ENOSPC");
|
|
1992
1992
|
return bytes.length;
|
|
1993
1993
|
}
|
|
1994
|
+
var DEVICE_PATHS = /* @__PURE__ */ new Map([
|
|
1995
|
+
["/dev/null", "null"],
|
|
1996
|
+
["/dev/zero", "zero"],
|
|
1997
|
+
["/dev/full", "full"],
|
|
1998
|
+
["/dev/urandom", "urandom"],
|
|
1999
|
+
["/dev/random", "urandom"]
|
|
2000
|
+
]);
|
|
2001
|
+
function deviceAt(path) {
|
|
2002
|
+
return DEVICE_PATHS.get(path);
|
|
2003
|
+
}
|
|
1994
2004
|
function deviceFile(device, access2) {
|
|
1995
2005
|
const read = async (max) => {
|
|
1996
2006
|
if (device === "null") return new Uint8Array(0);
|
|
@@ -6950,6 +6960,18 @@ function capturing(chunks, max = Number.POSITIVE_INFINITY, overflow) {
|
|
|
6950
6960
|
size += bytes.length;
|
|
6951
6961
|
};
|
|
6952
6962
|
}
|
|
6963
|
+
function captureFile(onData, control) {
|
|
6964
|
+
if (!control) return sinkFile(onData);
|
|
6965
|
+
control.opened = true;
|
|
6966
|
+
return new OpenFile({
|
|
6967
|
+
write: async (bytes) => {
|
|
6968
|
+
if (control.shut) throw new KernelError("EPIPE");
|
|
6969
|
+
onData(bytes.slice());
|
|
6970
|
+
return bytes.length;
|
|
6971
|
+
},
|
|
6972
|
+
close: () => control.ended()
|
|
6973
|
+
});
|
|
6974
|
+
}
|
|
6953
6975
|
function concat3(chunks) {
|
|
6954
6976
|
const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
|
|
6955
6977
|
let offset = 0;
|
|
@@ -6984,7 +7006,12 @@ var ChildTable = class {
|
|
|
6984
7006
|
const fds = new FdTable();
|
|
6985
7007
|
const captured = /* @__PURE__ */ new Map();
|
|
6986
7008
|
try {
|
|
6987
|
-
for (const [n, slot2] of stdio.entries())
|
|
7009
|
+
for (const [n, slot2] of stdio.entries()) {
|
|
7010
|
+
fds.installAt(
|
|
7011
|
+
n,
|
|
7012
|
+
"sameAs" in slot2 ? fds.get(slot2.sameAs).retain() : this.openSlot(slot2, n, captured)
|
|
7013
|
+
);
|
|
7014
|
+
}
|
|
6988
7015
|
for (const slot2 of inherit2) {
|
|
6989
7016
|
if (slot2.fd <= 2) continue;
|
|
6990
7017
|
if ("device" in slot2) {
|
|
@@ -7041,7 +7068,7 @@ var ChildTable = class {
|
|
|
7041
7068
|
if ("capture" in slot2) {
|
|
7042
7069
|
const chunks = [];
|
|
7043
7070
|
captured.set(n, chunks);
|
|
7044
|
-
return
|
|
7071
|
+
return captureFile(capturing(chunks, slot2.max, slot2.overflow), slot2.control);
|
|
7045
7072
|
}
|
|
7046
7073
|
return deviceFile("null");
|
|
7047
7074
|
}
|
|
@@ -10548,9 +10575,11 @@ function capLimit(maxBuffer) {
|
|
|
10548
10575
|
return Math.min(asked, RUN_CAPTURE_CAP);
|
|
10549
10576
|
}
|
|
10550
10577
|
function slot(how, n, extra) {
|
|
10578
|
+
if (how === "stdout") return { sameAs: 1 };
|
|
10579
|
+
if (typeof how === "number") return { fd: how };
|
|
10551
10580
|
if (how === "inherit") return { fd: n };
|
|
10552
10581
|
if (how === "ignore") return { none: true };
|
|
10553
|
-
return extra;
|
|
10582
|
+
return extra();
|
|
10554
10583
|
}
|
|
10555
10584
|
function frame2(outcome, out, err) {
|
|
10556
10585
|
const head = new TextEncoder().encode(JSON.stringify(outcome));
|
|
@@ -10580,12 +10609,20 @@ async function runChild(req, deps) {
|
|
|
10580
10609
|
if (failure2 !== void 0) return;
|
|
10581
10610
|
failure2 = code;
|
|
10582
10611
|
if (pid !== 0) kill();
|
|
10612
|
+
decided.resolve();
|
|
10613
|
+
};
|
|
10614
|
+
const controls = [];
|
|
10615
|
+
const capture = () => {
|
|
10616
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
10617
|
+
const control = { shut: false, ended: resolve, eof: promise };
|
|
10618
|
+
controls.push(control);
|
|
10619
|
+
return { capture: true, max, overflow: () => stop("ENOBUFS"), control };
|
|
10583
10620
|
};
|
|
10584
|
-
const
|
|
10621
|
+
const decided = Promise.withResolvers();
|
|
10585
10622
|
const stdio = [
|
|
10586
|
-
slot(req.stdin, 0, { input: req.input ?? new Uint8Array(0) }),
|
|
10587
|
-
slot(req.stdout, 1, capture
|
|
10588
|
-
slot(req.stderr, 2, capture
|
|
10623
|
+
slot(req.stdin, 0, () => ({ input: req.input ?? new Uint8Array(0) })),
|
|
10624
|
+
slot(req.stdout, 1, capture),
|
|
10625
|
+
slot(req.stderr, 2, capture)
|
|
10589
10626
|
];
|
|
10590
10627
|
try {
|
|
10591
10628
|
pid = await deps.children.spawn(
|
|
@@ -10602,8 +10639,11 @@ async function runChild(req, deps) {
|
|
|
10602
10639
|
let status;
|
|
10603
10640
|
try {
|
|
10604
10641
|
status = await reaped(deps, pid);
|
|
10642
|
+
const open = controls.filter((c) => c.opened);
|
|
10643
|
+
await Promise.race([Promise.all(open.map((c) => c.eof)), decided.promise]);
|
|
10605
10644
|
} finally {
|
|
10606
10645
|
clearTimeout(timer);
|
|
10646
|
+
for (const control of controls) control.shut = true;
|
|
10607
10647
|
}
|
|
10608
10648
|
const out = deps.children.captured(pid, 1);
|
|
10609
10649
|
const err = deps.children.captured(pid, 2);
|
|
@@ -11701,6 +11741,8 @@ var WasmProcess = class {
|
|
|
11701
11741
|
}
|
|
11702
11742
|
async openVfs(req) {
|
|
11703
11743
|
if (inProc(req.path)) return this.openProc(req, req.path);
|
|
11744
|
+
const device = req.orphan ? void 0 : deviceAt(req.path);
|
|
11745
|
+
if (device) return this.openDevice(req, device);
|
|
11704
11746
|
const alias = req.orphan ? void 0 : await procAlias(this.options.fs, req.path, !req.exclusive);
|
|
11705
11747
|
if (typeof alias === "object")
|
|
11706
11748
|
throw Object.assign(new Error(alias.refused), { code: alias.refused });
|
|
@@ -11738,6 +11780,14 @@ var WasmProcess = class {
|
|
|
11738
11780
|
}
|
|
11739
11781
|
return fd;
|
|
11740
11782
|
}
|
|
11783
|
+
openDevice(req, device) {
|
|
11784
|
+
if (req.exclusive) throw Object.assign(new Error(req.path), { code: "EEXIST" });
|
|
11785
|
+
const access2 = req.flags & WRITE_ACCESS;
|
|
11786
|
+
return this.fds.install(
|
|
11787
|
+
deviceFile(device, access2 === 0 ? "read" : access2 === 1 ? "write" : void 0),
|
|
11788
|
+
3
|
|
11789
|
+
);
|
|
11790
|
+
}
|
|
11741
11791
|
async identified(stat, known) {
|
|
11742
11792
|
const first = await stat();
|
|
11743
11793
|
if (first.orphan) return first;
|