@ai-ecoverse/slicc-kernel 1.62.0 → 1.62.1
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 +75 -3
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +58 -15
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +75 -3
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +58 -15
- package/dist/process-worker.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -210,7 +210,7 @@ A descriptor a program inherits at one of those numbers (from bash's `3<file`, o
|
|
|
210
210
|
|
|
211
211
|
On top of preview1 a WASI program gets:
|
|
212
212
|
|
|
213
|
-
- **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, and a file written, created, renamed or removed in one thread shows its new size and metadata to `stat` in every other thread at once, under any of its names (a symlink, a hard link, a new name after a rename in the same process); another process's changes show at close and after a `spawn` or `wait`, as with close-to-open caching, so a file another process renames while this one holds it open may show its old size under the new name until then; `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.
|
|
213
|
+
- **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, and a file written, created, renamed or removed in one thread shows its new size and metadata to `stat` in every other thread at once, under any of its names (a symlink, a hard link, a new name after a rename in the same process), also when another thread renames a file over that name meanwhile, since the kernel reports which file an open or a change reached; this holds among the kernel's own processes, not against other tabs, `hostfs` writers or programs that aren't WASI; another process's changes show at close and after a `spawn` or `wait`, as with close-to-open caching, so a file another process renames while this one holds it open may show its old size under the new name until then; `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.
|
|
214
214
|
- **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`.
|
|
215
215
|
- **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`, before the function is called); without it, missing arguments are zero (one cut short keeps the bytes given, with zeros above them) 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.
|
|
216
216
|
- **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 pending signal is delivered at the program's next WASI or WASIX call, including the calls its worker answers without the kernel (`random_get`, `args_get`, …), except the exempt ones: `clock_time_get` (a tight loop of it measured up to 30% slower), `proc_exit`, `proc_exit2`, `thread_exit`, `thread_spawn_v2` and `wasi.thread-spawn` (wasi-threads), the calls that install handlers (`callback_signal`, `slicc.sigaction_set`), and calls made while Asyncify unwinds or rewinds the stack for `fork` or `longjmp`. 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 sleep, or a clock in `poll_oneoff`, never ends before the time asked (it can end later, never earlier). 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).
|
package/dist/kernel-worker.js
CHANGED
|
@@ -8315,9 +8315,37 @@ async function holdOp(entry, path, req) {
|
|
|
8315
8315
|
entry.hold?.(path, on, open, void 0, req.handle);
|
|
8316
8316
|
return done;
|
|
8317
8317
|
}
|
|
8318
|
+
var IDENTITY_OPS = /* @__PURE__ */ new Set([
|
|
8319
|
+
"write",
|
|
8320
|
+
"pwrite",
|
|
8321
|
+
"truncate",
|
|
8322
|
+
"chmod",
|
|
8323
|
+
"utimes",
|
|
8324
|
+
"chown",
|
|
8325
|
+
"lchown"
|
|
8326
|
+
]);
|
|
8318
8327
|
async function dispatchSyncFs(req) {
|
|
8319
8328
|
const entry = resolveSyncFsToken(req.token);
|
|
8320
8329
|
if (!entry) return { ok: false, errno: "EACCES", message: "sync-fs: unknown or revoked token" };
|
|
8330
|
+
if (!req.identity || !IDENTITY_OPS.has(req.op)) return dispatchOne(entry, req);
|
|
8331
|
+
const { fs, cwd } = entry;
|
|
8332
|
+
const path = fs.resolvePath(cwd, req.path);
|
|
8333
|
+
const identified = async (held) => {
|
|
8334
|
+
const own = held ? { ...entry, fs: { ...fs, writeFile: held.writeFile } } : entry;
|
|
8335
|
+
const result = await dispatchOne(own, req);
|
|
8336
|
+
if (!result.ok) return result;
|
|
8337
|
+
if (held && !held.direct) return json2(null);
|
|
8338
|
+
const look = req.op === "lchown" ? fs.lstat : fs.stat;
|
|
8339
|
+
const st = await look.call(fs, path).catch(() => void 0);
|
|
8340
|
+
return json2(st ? statJson(st) : null);
|
|
8341
|
+
};
|
|
8342
|
+
try {
|
|
8343
|
+
return await (fs.exclusive ? fs.exclusive(path, identified, req.op !== "lchown") : identified());
|
|
8344
|
+
} catch (err) {
|
|
8345
|
+
return toErrno(err);
|
|
8346
|
+
}
|
|
8347
|
+
}
|
|
8348
|
+
async function dispatchOne(entry, req) {
|
|
8321
8349
|
const { fs, cwd } = entry;
|
|
8322
8350
|
try {
|
|
8323
8351
|
const path = fs.resolvePath(cwd, req.path);
|
|
@@ -10317,7 +10345,9 @@ var VfsNode = class {
|
|
|
10317
10345
|
this.ranged = new AsyncRangedFile(io, this.path, this.pin.size, this.pin.version);
|
|
10318
10346
|
return this.ranged;
|
|
10319
10347
|
}
|
|
10320
|
-
const
|
|
10348
|
+
const at = this.path;
|
|
10349
|
+
const st = io && !this.orphaned ? await this.fs.stat?.(at).catch(() => void 0) : void 0;
|
|
10350
|
+
this.note(at, st);
|
|
10321
10351
|
if (io && st?.ranged) {
|
|
10322
10352
|
this.ranged = new AsyncRangedFile(io, this.path, st.size ?? 0, st.version);
|
|
10323
10353
|
return this.ranged;
|
|
@@ -10377,12 +10407,23 @@ var VfsNode = class {
|
|
|
10377
10407
|
const stat = this.fs.stat?.bind(this.fs);
|
|
10378
10408
|
if (!stat || this.orphaned) return Promise.resolve(void 0);
|
|
10379
10409
|
const parent = this.path.slice(0, this.path.lastIndexOf("/")) || "/";
|
|
10380
|
-
|
|
10410
|
+
const at = this.path;
|
|
10411
|
+
this.cap ??= stat(at).then((st) => {
|
|
10412
|
+
this.note(at, st);
|
|
10413
|
+
return st;
|
|
10414
|
+
}).catch(() => stat(parent)).then(
|
|
10381
10415
|
(st) => st.maxFile,
|
|
10382
10416
|
() => void 0
|
|
10383
10417
|
);
|
|
10384
10418
|
return this.cap;
|
|
10385
10419
|
}
|
|
10420
|
+
seen;
|
|
10421
|
+
note(path, st) {
|
|
10422
|
+
if (st?.ino !== void 0) this.seen = { path, ino: st.ino };
|
|
10423
|
+
}
|
|
10424
|
+
known() {
|
|
10425
|
+
return this.seen?.path === this.path && !this.orphaned ? this.seen.ino : void 0;
|
|
10426
|
+
}
|
|
10386
10427
|
async fits(end) {
|
|
10387
10428
|
const max = await this.limit();
|
|
10388
10429
|
if (max !== void 0 && end > max) throw new KernelError("EFBIG");
|
|
@@ -10696,6 +10737,18 @@ async function refuseReadonly(fs, path, flags, opts = {}) {
|
|
|
10696
10737
|
function keepingOpen(fs, nodes) {
|
|
10697
10738
|
return {
|
|
10698
10739
|
...fs,
|
|
10740
|
+
async exclusive(path, run2, follow2 = true) {
|
|
10741
|
+
const at = fs.resolvePath("/", path);
|
|
10742
|
+
const key = follow2 ? nodes.targetKey(at) : nodes.entryKey(at);
|
|
10743
|
+
const locked = await key.catch(() => at);
|
|
10744
|
+
return nodes.onEntry(
|
|
10745
|
+
locked,
|
|
10746
|
+
() => run2({
|
|
10747
|
+
writeFile: (p, content, owner) => fs.writeFile(p, content, owner),
|
|
10748
|
+
direct: locked === at
|
|
10749
|
+
})
|
|
10750
|
+
);
|
|
10751
|
+
},
|
|
10699
10752
|
async rm(path, options) {
|
|
10700
10753
|
const at = await nodes.entryKey(fs.resolvePath("/", path));
|
|
10701
10754
|
await nodes.unlinking(at);
|
|
@@ -10788,6 +10841,7 @@ function vfsFile(fs, opts, nodes) {
|
|
|
10788
10841
|
size: await node.size(),
|
|
10789
10842
|
...node.orphaned ? { orphan: true } : {}
|
|
10790
10843
|
})),
|
|
10844
|
+
known: () => node.known(),
|
|
10791
10845
|
flush: () => serial(() => node.flush()),
|
|
10792
10846
|
close: () => node.serial(async () => {
|
|
10793
10847
|
try {
|
|
@@ -11182,6 +11236,20 @@ var WasmProcess = class {
|
|
|
11182
11236
|
}
|
|
11183
11237
|
return fd;
|
|
11184
11238
|
}
|
|
11239
|
+
async identified(stat, known) {
|
|
11240
|
+
const first = await stat();
|
|
11241
|
+
if (first.orphan) return first;
|
|
11242
|
+
const ino = known?.();
|
|
11243
|
+
if (ino !== void 0) return { ...first, ino };
|
|
11244
|
+
const { fs } = this.options;
|
|
11245
|
+
const look = async () => {
|
|
11246
|
+
const now = await stat();
|
|
11247
|
+
if (now.path !== first.path || now.orphan) return now;
|
|
11248
|
+
const st = await fs.stat?.(now.path).catch(() => void 0);
|
|
11249
|
+
return st?.ino === void 0 ? now : { ...now, ino: st.ino };
|
|
11250
|
+
};
|
|
11251
|
+
return fs.exclusive ? fs.exclusive(first.path, look) : look();
|
|
11252
|
+
}
|
|
11185
11253
|
newOwner() {
|
|
11186
11254
|
if (!this.options.owners) return void 0;
|
|
11187
11255
|
return { uid: this.cred.euid, gid: this.cred.egid, mode: 438 & ~this.umask };
|
|
@@ -11343,7 +11411,11 @@ var WasmProcess = class {
|
|
|
11343
11411
|
await file.resize(req.size);
|
|
11344
11412
|
return { ok: true, kind: "void" };
|
|
11345
11413
|
case "fd-vfs-stat":
|
|
11346
|
-
return {
|
|
11414
|
+
return {
|
|
11415
|
+
ok: true,
|
|
11416
|
+
kind: "json",
|
|
11417
|
+
json: req.identity ? await this.identified(file.stat.bind(file), file.known?.bind(file)) : await file.stat()
|
|
11418
|
+
};
|
|
11347
11419
|
}
|
|
11348
11420
|
}
|
|
11349
11421
|
keptPath;
|