@ai-ecoverse/slicc-kernel 1.54.3 → 1.54.4
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 +1 -1
- package/dist/kernel-worker.js +111 -25
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +71 -19
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +111 -25
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +71 -19
- package/dist/process-worker.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ A descriptor a program inherits at one of those numbers (from bash's `3<file`, o
|
|
|
186
186
|
On top of preview1 a WASI program gets:
|
|
187
187
|
|
|
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
|
-
- **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), 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`. Each thread has its own copy of the function table, so the kernel hands out its slots (a side module's range, a `dlsym`'d function, a closure): threads loading or looking up at the same time never get the same slot. `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`.
|
|
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` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), 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`. Each thread has its own copy of the function table, so the kernel hands out its slots (a side module's range, a `dlsym`'d function, a closure): threads loading or looking up at the same time never get the same slot. As in glibc, one `dlopen` at a time runs in a process, constructors included: a `dlopen` from a library's constructor goes ahead in its own thread, and another thread's `dlopen` waits, then finds the library loaded and gets the same handle. A thread waiting there still ends or stops with its process, but a signal handler it has runs once that `dlopen` returns. The lock is let go when a `dlopen` fails (an exception, a trap or `pthread_exit` out of a constructor included) and when its thread ends; a forked child starts with it free. In such a program, `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); a program that isn't position-independent gets an error from `dlopen(NULL)`, as from any `dlopen`; `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`.
|
|
190
190
|
- **Foreign calls** (libffi's WASIX backend, so Python's `ctypes`): `call_dynamic` calls a function through the program's indirect function table (a static module's exported `__indirect_function_table`, or the dynamic linker's), its arguments and results packed in memory one after another (`i32`/`f32` 4 bytes, `i64`/`f64` 8). With `strict` set, the buffers must fit the function exactly (else `EINVAL`); without it, missing arguments are zero and results that do not fit are left out. A trap or exception of the callee goes through to the caller. JS cannot ask a function for its type, so the kernel reads the types from the module files (the program's and each side module's, once, when a program first calls through `call_dynamic`): a table slot that is empty, past the end, holds a function of no module the kernel knows of, or takes or returns a `v128`, gives `EINVAL`. For closures (callbacks such as `CFUNCTYPE`), a dynamically linked program gets table slots from `closure_allocate`, and `closure_free` gives them back for reuse and empties the slot in every thread; a static program gets `ENOTSUP`. `closure_prepare` puts a trampoline with the requested signature (any of `i32`, `i64`, `f32`, `f64`, `v128`) into such a slot, in every thread: each call stores its arguments below the stack pointer and calls the backing function as `backing(values, results, user_data)`, then returns what it wrote to `results`. The trampolines are small wasm modules made and compiled at run time, once per signature in each thread (an instance per closure), so a page whose Content Security Policy forbids compiling WebAssembly (no `'wasm-unsafe-eval'`) cannot run them; it cannot run the program either. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
|
|
191
191
|
- **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. A signal that ends a sleep or a `poll`/`select` with a timeout makes the call fail with `EINTR` in a program whose libc says it is wasix-sysroot 2025.9.30-22 or later (its `slicc.libc` custom section). Older libcs turn that answer into `ENOTSUP` from `nanosleep` (up to -18), so for a program without the section the wait ends as if it had timed out, unless it imports `fd_fdflags_set` (then `EINTR`, as before).
|
|
192
192
|
- **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.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -8152,12 +8152,14 @@ var AsyncOps = class {
|
|
|
8152
8152
|
const at = this.ready.indexOf(id2);
|
|
8153
8153
|
if (at >= 0) this.ready.splice(at, 1);
|
|
8154
8154
|
}
|
|
8155
|
-
async wait(timeoutMs,
|
|
8155
|
+
async wait(timeoutMs, blocking) {
|
|
8156
|
+
let signal;
|
|
8156
8157
|
for (; ; ) {
|
|
8157
8158
|
if (this.closed) return -1;
|
|
8158
8159
|
const id2 = this.ready.shift();
|
|
8159
8160
|
if (id2 !== void 0) return id2;
|
|
8160
8161
|
if (timeoutMs !== void 0 && timeoutMs <= 0) return 0;
|
|
8162
|
+
signal ??= blocking();
|
|
8161
8163
|
const woke = await this.sleep(timeoutMs, signal);
|
|
8162
8164
|
if (!woke) return 0;
|
|
8163
8165
|
}
|
|
@@ -8218,7 +8220,7 @@ async function hostSyscall(req, ctx) {
|
|
|
8218
8220
|
if (!ctx.valid(req.req)) throw new KernelError("EINVAL");
|
|
8219
8221
|
return json3(ctx.ops.submit(req.req));
|
|
8220
8222
|
case "async-wait":
|
|
8221
|
-
return json3(await ctx.ops.wait(req.timeoutMs, ctx.blocking
|
|
8223
|
+
return json3(await ctx.ops.wait(req.timeoutMs, ctx.blocking));
|
|
8222
8224
|
case "async-take":
|
|
8223
8225
|
return ctx.ops.take(req.id);
|
|
8224
8226
|
case "async-resolve":
|
|
@@ -9820,6 +9822,9 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
|
|
|
9820
9822
|
"dl-closure",
|
|
9821
9823
|
"dl-closure-free",
|
|
9822
9824
|
"dl-reserve",
|
|
9825
|
+
"dl-load-reserve",
|
|
9826
|
+
"dl-lock",
|
|
9827
|
+
"dl-unlock",
|
|
9823
9828
|
"fd-renumber",
|
|
9824
9829
|
"fd-promote",
|
|
9825
9830
|
"fd-open-tty",
|
|
@@ -9879,7 +9884,7 @@ var WasmProcess = class {
|
|
|
9879
9884
|
stopped = 0;
|
|
9880
9885
|
stops = 0;
|
|
9881
9886
|
owed = false;
|
|
9882
|
-
|
|
9887
|
+
waiting = /* @__PURE__ */ new Set();
|
|
9883
9888
|
resumed = Promise.resolve();
|
|
9884
9889
|
wake;
|
|
9885
9890
|
stateListeners = [];
|
|
@@ -9935,7 +9940,8 @@ var WasmProcess = class {
|
|
|
9935
9940
|
return "terminate";
|
|
9936
9941
|
}
|
|
9937
9942
|
this.options.onPending?.(sig);
|
|
9938
|
-
if (this.options.jobs?.keyboard && KEYBOARD.has(sig)
|
|
9943
|
+
if (this.options.jobs?.keyboard && KEYBOARD.has(sig) && this.waiting.size === 0)
|
|
9944
|
+
this.owed = true;
|
|
9939
9945
|
const blocked = this.interrupt;
|
|
9940
9946
|
this.interrupt = new AbortController();
|
|
9941
9947
|
blocked.abort();
|
|
@@ -9980,10 +9986,15 @@ var WasmProcess = class {
|
|
|
9980
9986
|
await this.resumed;
|
|
9981
9987
|
if (this.dying) return DYING;
|
|
9982
9988
|
const stops = this.stops;
|
|
9983
|
-
const
|
|
9984
|
-
const
|
|
9989
|
+
const scope = new AbortController();
|
|
9990
|
+
const forward = () => scope.abort();
|
|
9991
|
+
if (cancelled?.aborted) scope.abort();
|
|
9992
|
+
cancelled?.addEventListener("abort", forward, { once: true });
|
|
9993
|
+
const result = await this.dispatch(req, scope.signal).finally(() => {
|
|
9994
|
+
cancelled?.removeEventListener("abort", forward);
|
|
9995
|
+
this.waiting.delete(scope.signal);
|
|
9996
|
+
});
|
|
9985
9997
|
if (this.dying) return DYING;
|
|
9986
|
-
if (!result.ok && result.errno === "EINTR" && this.waits !== waits) this.owed = false;
|
|
9987
9998
|
const restart = !result.ok && result.errno === "EINTR" && this.stops !== stops;
|
|
9988
9999
|
if (restart && !this.options.hasPending?.()) continue;
|
|
9989
10000
|
await this.resumed;
|
|
@@ -10036,12 +10047,31 @@ var WasmProcess = class {
|
|
|
10036
10047
|
refuseIfPending(restart) {
|
|
10037
10048
|
if (restart && this.options.hasPending?.()) throw new KernelError("EINTR");
|
|
10038
10049
|
}
|
|
10050
|
+
async procWait(req, cancelled) {
|
|
10051
|
+
const flags = {
|
|
10052
|
+
untraced: req.untraced,
|
|
10053
|
+
continued: req.continued,
|
|
10054
|
+
inGroup: this.waitGroup(req.pid)
|
|
10055
|
+
};
|
|
10056
|
+
if (this.owed && !req.nohang) {
|
|
10057
|
+
const now = await this.children.wait(req.pid, true, this.interrupt.signal, flags);
|
|
10058
|
+
if (now[0] !== 0) return now;
|
|
10059
|
+
}
|
|
10060
|
+
const signal = req.nohang ? this.interrupt.signal : this.blockingSignal(cancelled);
|
|
10061
|
+
const waited = await this.children.wait(req.pid, req.nohang, signal, flags);
|
|
10062
|
+
return waited;
|
|
10063
|
+
}
|
|
10039
10064
|
blockingSignal(cancelled, owing = true) {
|
|
10040
|
-
if (owing
|
|
10041
|
-
|
|
10065
|
+
if (owing && this.owed) {
|
|
10066
|
+
this.owed = false;
|
|
10042
10067
|
throw new KernelError("EINTR");
|
|
10043
10068
|
}
|
|
10044
|
-
|
|
10069
|
+
if (this.options.hasPending?.() || cancelled.aborted) throw new KernelError("EINTR");
|
|
10070
|
+
if (owing) this.waiting.add(cancelled);
|
|
10071
|
+
return this.cancelSignal(cancelled);
|
|
10072
|
+
}
|
|
10073
|
+
cancelSignal(cancelled) {
|
|
10074
|
+
return AbortSignal.any([this.interrupt.signal, cancelled]);
|
|
10045
10075
|
}
|
|
10046
10076
|
async read(req, cancelled) {
|
|
10047
10077
|
const file = this.fds.get(req.fd).file;
|
|
@@ -10065,7 +10095,7 @@ var WasmProcess = class {
|
|
|
10065
10095
|
if (nonblock) return file.write(body, AbortSignal.abort());
|
|
10066
10096
|
return file.write(
|
|
10067
10097
|
body,
|
|
10068
|
-
this.options.hasPending?.() ? AbortSignal.abort() : this.
|
|
10098
|
+
this.options.hasPending?.() ? AbortSignal.abort() : this.cancelSignal(cancelled)
|
|
10069
10099
|
);
|
|
10070
10100
|
}
|
|
10071
10101
|
async openVfs(req) {
|
|
@@ -10209,6 +10239,11 @@ var WasmProcess = class {
|
|
|
10209
10239
|
return { ok: true, kind: "json", json: this.options.jobs?.terminalNames() ?? [] };
|
|
10210
10240
|
case "fd-select": {
|
|
10211
10241
|
const { read, write, timeoutMs } = req;
|
|
10242
|
+
if (this.owed && timeoutMs !== 0) {
|
|
10243
|
+
const ready2 = await selectFds(this.fds, read, write, 0, this.interrupt.signal);
|
|
10244
|
+
if (ready2.read.length + ready2.write.length > 0)
|
|
10245
|
+
return { ok: true, kind: "json", json: ready2 };
|
|
10246
|
+
}
|
|
10212
10247
|
const signal = this.blockingSignal(cancelled, timeoutMs !== 0);
|
|
10213
10248
|
const selected = await selectFds(this.fds, read, write, timeoutMs, signal);
|
|
10214
10249
|
return { ok: true, kind: "json", json: selected };
|
|
@@ -10406,16 +10441,8 @@ var WasmProcess = class {
|
|
|
10406
10441
|
case "proc-spawn":
|
|
10407
10442
|
this.refuseIfPending(req.restart);
|
|
10408
10443
|
return { ok: true, kind: "json", json: await this.spawn(req) };
|
|
10409
|
-
case "proc-wait":
|
|
10410
|
-
|
|
10411
|
-
const flags = {
|
|
10412
|
-
untraced: req.untraced,
|
|
10413
|
-
continued: req.continued,
|
|
10414
|
-
inGroup: this.waitGroup(req.pid)
|
|
10415
|
-
};
|
|
10416
|
-
const waited = await this.children.wait(req.pid, req.nohang, signal, flags);
|
|
10417
|
-
return { ok: true, kind: "json", json: waited };
|
|
10418
|
-
}
|
|
10444
|
+
case "proc-wait":
|
|
10445
|
+
return { ok: true, kind: "json", json: await this.procWait(req, cancelled) };
|
|
10419
10446
|
case "proc-identity":
|
|
10420
10447
|
return { ok: true, kind: "json", json: await this.options.identity?.() ?? null };
|
|
10421
10448
|
case "proc-exec": {
|
|
@@ -10447,7 +10474,7 @@ var WasmProcess = class {
|
|
|
10447
10474
|
return { ok: true, kind: "json", json: this.options.mounts?.() ?? [] };
|
|
10448
10475
|
case "mount":
|
|
10449
10476
|
case "umount":
|
|
10450
|
-
return this.mountSyscall(req);
|
|
10477
|
+
return this.mountSyscall(req, cancelled);
|
|
10451
10478
|
case "proc-alarm":
|
|
10452
10479
|
this.setAlarm(req.sig, req.firstMs ?? req.ms, req.repeat ? req.ms : 0, req.timer);
|
|
10453
10480
|
return { ok: true, kind: "void" };
|
|
@@ -10468,6 +10495,18 @@ var WasmProcess = class {
|
|
|
10468
10495
|
return { ok: true, kind: "void" };
|
|
10469
10496
|
case "dl-reserve":
|
|
10470
10497
|
return { ok: true, kind: "json", json: this.reserveSlots(req.floor, req.size) };
|
|
10498
|
+
case "dl-load-reserve":
|
|
10499
|
+
return {
|
|
10500
|
+
ok: true,
|
|
10501
|
+
kind: "json",
|
|
10502
|
+
json: { base: this.reserveSlots(req.floor, req.size), handle: this.closures.handle++ }
|
|
10503
|
+
};
|
|
10504
|
+
case "dl-lock":
|
|
10505
|
+
await this.lockDl(req.owner);
|
|
10506
|
+
return { ok: true, kind: "void" };
|
|
10507
|
+
case "dl-unlock":
|
|
10508
|
+
this.unlockDl(req.owner);
|
|
10509
|
+
return { ok: true, kind: "void" };
|
|
10471
10510
|
case "sig-mask":
|
|
10472
10511
|
this.caught = req.caught;
|
|
10473
10512
|
this.ignored = req.ignored;
|
|
@@ -10482,14 +10521,14 @@ var WasmProcess = class {
|
|
|
10482
10521
|
return { ok: true, kind: "bytes", bytes: this.children.captured(req.pid, req.slot) };
|
|
10483
10522
|
}
|
|
10484
10523
|
}
|
|
10485
|
-
async mountSyscall(req) {
|
|
10524
|
+
async mountSyscall(req, cancelled) {
|
|
10486
10525
|
const { mount, umount } = this.options;
|
|
10487
10526
|
if (!mount || !umount) throw new KernelError("ENOSYS");
|
|
10488
10527
|
try {
|
|
10489
10528
|
if (req.op === "umount") await umount(req.target, req.flags);
|
|
10490
10529
|
else {
|
|
10491
10530
|
const { source, target, type, flags, data } = req;
|
|
10492
|
-
const signal = this.blockingSignal();
|
|
10531
|
+
const signal = this.blockingSignal(cancelled);
|
|
10493
10532
|
await Promise.race([mount({ source, target, type, flags, data }, signal), aborted(signal)]);
|
|
10494
10533
|
}
|
|
10495
10534
|
} catch (err) {
|
|
@@ -10504,7 +10543,12 @@ var WasmProcess = class {
|
|
|
10504
10543
|
}
|
|
10505
10544
|
alarm;
|
|
10506
10545
|
dlLog = [];
|
|
10507
|
-
closures = {
|
|
10546
|
+
closures = {
|
|
10547
|
+
next: 0,
|
|
10548
|
+
handle: 1,
|
|
10549
|
+
free: [],
|
|
10550
|
+
live: /* @__PURE__ */ new Set()
|
|
10551
|
+
};
|
|
10508
10552
|
allocateClosure(floor) {
|
|
10509
10553
|
const index = this.closures.free.pop() ?? Math.max(floor, this.closures.next);
|
|
10510
10554
|
this.closures.next = Math.max(this.closures.next, index + 1);
|
|
@@ -10517,6 +10561,47 @@ var WasmProcess = class {
|
|
|
10517
10561
|
this.closures.free.push(index);
|
|
10518
10562
|
this.dlLog.push({ kind: "closure-free", index });
|
|
10519
10563
|
}
|
|
10564
|
+
dlLock = {
|
|
10565
|
+
owner: 0,
|
|
10566
|
+
depth: 0,
|
|
10567
|
+
waiters: /* @__PURE__ */ new Set()
|
|
10568
|
+
};
|
|
10569
|
+
async lockDl(owner) {
|
|
10570
|
+
while (this.dlLock.depth > 0 && this.dlLock.owner !== owner) {
|
|
10571
|
+
if (this.dying) throw new KernelError("EINTR");
|
|
10572
|
+
const interrupt = this.interrupt.signal;
|
|
10573
|
+
const gone = await new Promise((resolve) => {
|
|
10574
|
+
const waiter = {
|
|
10575
|
+
owner,
|
|
10576
|
+
wake: (ended) => {
|
|
10577
|
+
this.dlLock.waiters.delete(waiter);
|
|
10578
|
+
interrupt.removeEventListener("abort", recheck);
|
|
10579
|
+
resolve(ended);
|
|
10580
|
+
}
|
|
10581
|
+
};
|
|
10582
|
+
const recheck = () => waiter.wake(false);
|
|
10583
|
+
this.dlLock.waiters.add(waiter);
|
|
10584
|
+
interrupt.addEventListener("abort", recheck, { once: true });
|
|
10585
|
+
});
|
|
10586
|
+
if (gone) throw new KernelError("EINTR");
|
|
10587
|
+
}
|
|
10588
|
+
this.dlLock.owner = owner;
|
|
10589
|
+
this.dlLock.depth++;
|
|
10590
|
+
}
|
|
10591
|
+
threadEnded(tid) {
|
|
10592
|
+
for (const waiter of [...this.dlLock.waiters]) if (waiter.owner === tid) waiter.wake(true);
|
|
10593
|
+
if (this.dlLock.depth === 0 || this.dlLock.owner !== tid) return;
|
|
10594
|
+
this.dlLock.depth = 0;
|
|
10595
|
+
this.wakeDl();
|
|
10596
|
+
}
|
|
10597
|
+
unlockDl(owner) {
|
|
10598
|
+
if (this.dlLock.depth === 0 || this.dlLock.owner !== owner) throw new KernelError("EPERM");
|
|
10599
|
+
this.dlLock.depth--;
|
|
10600
|
+
if (this.dlLock.depth === 0) this.wakeDl();
|
|
10601
|
+
}
|
|
10602
|
+
wakeDl() {
|
|
10603
|
+
for (const waiter of [...this.dlLock.waiters]) waiter.wake(false);
|
|
10604
|
+
}
|
|
10520
10605
|
reserveSlots(floor, size) {
|
|
10521
10606
|
const base = Math.max(floor, this.closures.next);
|
|
10522
10607
|
this.closures.next = base + size;
|
|
@@ -10828,6 +10913,7 @@ function spawnWasmProcess(opts) {
|
|
|
10828
10913
|
const endThread = (tid) => {
|
|
10829
10914
|
const end = threads.get(tid);
|
|
10830
10915
|
threads.delete(tid);
|
|
10916
|
+
process.threadEnded(tid);
|
|
10831
10917
|
end?.();
|
|
10832
10918
|
};
|
|
10833
10919
|
const spawnThread = (thread) => {
|