@ai-ecoverse/slicc-kernel 1.28.0 → 1.29.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 +3 -3
- package/dist/kernel-worker.js +62 -3
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +22 -7
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +62 -3
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +22 -7
- package/dist/process-worker.js.map +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
|
|
|
140
140
|
{ "slicc": { "abi": "wasi", "commands": { "rg": { "wasm": "bin/rg.wasm" }, "rustc": { "script": "bin/rustc" } } } }
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. Commands also come from pnpm's global directory. Every process gets `PNPM_HOME=/home/.local/share/pnpm`, unless the embedder sets its own, and has `$PNPM_HOME/bin` on `PATH`. The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
|
|
143
|
+
Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. Commands also come from pnpm's global directory. Every process gets `PNPM_HOME=/home/.local/share/pnpm`, unless the embedder sets its own, and has `$PNPM_HOME/bin` on `PATH`. The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
|
|
144
144
|
|
|
145
145
|
### WASI programs
|
|
146
146
|
|
|
@@ -360,12 +360,12 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
|
|
|
360
360
|
|
|
361
361
|
An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux: the kernel keeps one copy for all holders, shared by their descriptors (writes through one are seen through the others), and frees it when the last one closes. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
|
|
362
362
|
|
|
363
|
-
- **Emscripten:** `Module.sliccKernel.execve(file, argv, env, cwd)` runs `file` as this process's new image on its fds 0, 1 and 2, waits for it, and returns its wait status, or a negative errno when it cannot start. `env` and `cwd` may be `null` for this process's own. An exec shim detects it with `typeof Module.sliccKernel.execve === 'function'` and exits with the status it returns, as `execve` never returns on success. Older shims spawn the program and then call `Module.sliccKernel.execWait(pid)`; that
|
|
363
|
+
- **Emscripten:** `Module.sliccKernel.execve(file, argv, env, cwd)` runs `file` as this process's new image on its fds 0, 1 and 2, waits for it, and returns its wait status, or a negative errno when it cannot start. `env` and `cwd` may be `null` for this process's own. An exec shim detects it with `typeof Module.sliccKernel.execve === 'function'` and exits with the status it returns, as `execve` never returns on success. Older shims spawn the program and then call `Module.sliccKernel.execWait(pid)`; that works the same way: a new program asks the kernel for its pid before `main`, and the kernel answers once its parent has made its next call, so an image an older shim execs also reports the pid it replaced (the wait starts once the program is loaded; should the parent make no call within 100 ms after that, the kernel answers anyway, and the image may then report its own pid; `launcher.identities` counts the programs that asked and those answered by that timeout).
|
|
364
364
|
- **WASIX:** `proc_exec` does this by itself.
|
|
365
365
|
|
|
366
366
|
### `/proc`
|
|
367
367
|
|
|
368
|
-
Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike. Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so is each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times, load and `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) are placeholders. Only Emscripten processes see this `/proc`.
|
|
368
|
+
Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so is each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times, load and `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) are placeholders. Only Emscripten processes see this `/proc`.
|
|
369
369
|
|
|
370
370
|
While OPFS has no `/etc/passwd`, `/etc/group` or `/etc/mtab`, reading them gets per-process ones: root is uid 0 and the realm user, uid 1000, is named after `USER`, or `web_user` (Emscripten's default) without one, so it matches `$USER`, with `HOME` as its home, so `id`, `whoami`, `ls -l` and `ps` show names. Nothing is written to OPFS, and a real file there wins.
|
|
371
371
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -4527,6 +4527,7 @@ function commandOf(pkg, name2, raw, packageAbi, shared) {
|
|
|
4527
4527
|
glue,
|
|
4528
4528
|
wasm,
|
|
4529
4529
|
argv0,
|
|
4530
|
+
...raw.argv0Path === true ? { argv0Path: true } : {},
|
|
4530
4531
|
...args ? { args } : {},
|
|
4531
4532
|
...withEnv,
|
|
4532
4533
|
...imports ? { imports } : {}
|
|
@@ -7731,6 +7732,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
|
|
|
7731
7732
|
"mount",
|
|
7732
7733
|
"umount",
|
|
7733
7734
|
"proc-exec",
|
|
7735
|
+
"proc-identity",
|
|
7734
7736
|
"proc-setpgid",
|
|
7735
7737
|
"proc-getpgid",
|
|
7736
7738
|
"proc-getsid",
|
|
@@ -7830,6 +7832,7 @@ var WasmProcess = class {
|
|
|
7830
7832
|
for (const listener of this.stateListeners) listener("continued", SIG.CONT);
|
|
7831
7833
|
}
|
|
7832
7834
|
async syscall(req) {
|
|
7835
|
+
this.options.onSyscall?.();
|
|
7833
7836
|
for (; ; ) {
|
|
7834
7837
|
await this.resumed;
|
|
7835
7838
|
const stops = this.stops;
|
|
@@ -8175,6 +8178,8 @@ var WasmProcess = class {
|
|
|
8175
8178
|
const waited = await this.children.wait(req.pid, req.nohang, signal, flags);
|
|
8176
8179
|
return { ok: true, kind: "json", json: waited };
|
|
8177
8180
|
}
|
|
8181
|
+
case "proc-identity":
|
|
8182
|
+
return { ok: true, kind: "json", json: await this.options.identity?.() ?? null };
|
|
8178
8183
|
case "proc-exec": {
|
|
8179
8184
|
this.execChild = req.pid;
|
|
8180
8185
|
this.options.jobs?.exec(this.pid, req.pid);
|
|
@@ -8325,6 +8330,8 @@ function spawnWasmProcess(opts) {
|
|
|
8325
8330
|
const header2 = new Int32Array(sab, 0, SAB_HEADER_I32);
|
|
8326
8331
|
const process = new WasmProcess(opts.pid, opts.fds, {
|
|
8327
8332
|
...opts.ignored ? { ignored: opts.ignored } : {},
|
|
8333
|
+
...opts.identity ? { identity: opts.identity } : {},
|
|
8334
|
+
...opts.onSyscall ? { onSyscall: opts.onSyscall } : {},
|
|
8328
8335
|
spawner: opts.spawner,
|
|
8329
8336
|
forker: opts.forker,
|
|
8330
8337
|
fs: opts.fs,
|
|
@@ -9797,6 +9804,7 @@ var ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
|
9797
9804
|
var WASM_MAGIC = [0, 97, 115, 109];
|
|
9798
9805
|
var SHEBANG_MAX = 256;
|
|
9799
9806
|
var NOT_FOUND = 127;
|
|
9807
|
+
var DECIDE_MS = 100;
|
|
9800
9808
|
var INIT_PID = 1;
|
|
9801
9809
|
var SHARED_DIRS = ["/tmp", "/home"];
|
|
9802
9810
|
var encoder3 = new TextEncoder();
|
|
@@ -9895,6 +9903,8 @@ var Launcher = class {
|
|
|
9895
9903
|
fstabRetries;
|
|
9896
9904
|
inserted = /* @__PURE__ */ new Map();
|
|
9897
9905
|
openFiles = /* @__PURE__ */ new Set();
|
|
9906
|
+
deciding = /* @__PURE__ */ new Map();
|
|
9907
|
+
identities = { asked: 0, timedOut: 0 };
|
|
9898
9908
|
nodes;
|
|
9899
9909
|
held = /* @__PURE__ */ new Set();
|
|
9900
9910
|
processMounts;
|
|
@@ -9958,9 +9968,14 @@ var Launcher = class {
|
|
|
9958
9968
|
const command = (await this.commands()).get(name2);
|
|
9959
9969
|
return command && !command.script ? targetOf(command) : void 0;
|
|
9960
9970
|
}
|
|
9961
|
-
const
|
|
9971
|
+
const path = this.fs.resolvePath(cwd, file);
|
|
9972
|
+
const glue = await followLinks(this.base, path);
|
|
9962
9973
|
const linked = COMMAND2.exec(glue)?.[1];
|
|
9963
|
-
if (linked !== void 0)
|
|
9974
|
+
if (linked !== void 0) {
|
|
9975
|
+
const target = await this.resolve(`/bin/${linked}`, argv0, cwd);
|
|
9976
|
+
const byPath = !COMMAND2.test(path) && (await this.commands()).get(linked)?.argv0Path === true;
|
|
9977
|
+
return target && byPath ? { ...target, argv0: path } : target;
|
|
9978
|
+
}
|
|
9964
9979
|
const head = await this.head(glue);
|
|
9965
9980
|
if (!head) return void 0;
|
|
9966
9981
|
if (isWasm(head)) {
|
|
@@ -10096,8 +10111,19 @@ var Launcher = class {
|
|
|
10096
10111
|
}
|
|
10097
10112
|
const pid = this.nextPid++;
|
|
10098
10113
|
const terminal = req.fds.stdioTerminal();
|
|
10114
|
+
const decided = req.decided ?? Promise.resolve(true);
|
|
10115
|
+
const count = (byParent) => {
|
|
10116
|
+
this.identities.asked++;
|
|
10117
|
+
if (!byParent) this.identities.timedOut++;
|
|
10118
|
+
};
|
|
10119
|
+
let asked;
|
|
10099
10120
|
const handle = spawnWasmProcess({
|
|
10100
10121
|
pid,
|
|
10122
|
+
identity: async () => {
|
|
10123
|
+
await (asked ??= decided.then(count));
|
|
10124
|
+
return this.identityOf(pid);
|
|
10125
|
+
},
|
|
10126
|
+
onSyscall: () => this.decide(pid),
|
|
10101
10127
|
...req.ignored ? { ignored: req.ignored } : {},
|
|
10102
10128
|
program: req.program,
|
|
10103
10129
|
argv0: req.argv0,
|
|
@@ -10170,7 +10196,8 @@ var Launcher = class {
|
|
|
10170
10196
|
};
|
|
10171
10197
|
const shown = (pid) => {
|
|
10172
10198
|
const member = pid === void 0 ? void 0 : members.get(pid);
|
|
10173
|
-
|
|
10199
|
+
const root = member && rootOf(member).pid;
|
|
10200
|
+
return root !== void 0 && this.running(root) ? root : INIT_PID;
|
|
10174
10201
|
};
|
|
10175
10202
|
const listed = [];
|
|
10176
10203
|
for (const member of members.values()) {
|
|
@@ -10199,7 +10226,9 @@ var Launcher = class {
|
|
|
10199
10226
|
throw new SpawnError(await this.unrunnable(req.file, req.cwd));
|
|
10200
10227
|
}
|
|
10201
10228
|
const exec = req.exec ? { exec: true } : {};
|
|
10229
|
+
const deciding = req.exec ? void 0 : this.undecided(ppid);
|
|
10202
10230
|
const handle = await this.launch(planned, {
|
|
10231
|
+
...deciding ? { decided: deciding.promise } : {},
|
|
10203
10232
|
env: req.env,
|
|
10204
10233
|
cwd: req.cwd,
|
|
10205
10234
|
fds,
|
|
@@ -10208,9 +10237,39 @@ var Launcher = class {
|
|
|
10208
10237
|
ignored: this.ignoredBy(ppid),
|
|
10209
10238
|
...exec
|
|
10210
10239
|
});
|
|
10240
|
+
deciding?.attach();
|
|
10211
10241
|
return childHandle(handle);
|
|
10212
10242
|
};
|
|
10213
10243
|
}
|
|
10244
|
+
undecided(ppid) {
|
|
10245
|
+
const decided = Promise.withResolvers();
|
|
10246
|
+
return {
|
|
10247
|
+
promise: decided.promise,
|
|
10248
|
+
attach: () => {
|
|
10249
|
+
const timer = setTimeout(() => decided.resolve(false), DECIDE_MS);
|
|
10250
|
+
void decided.promise.then(() => clearTimeout(timer));
|
|
10251
|
+
const waiting = this.deciding.get(ppid) ?? /* @__PURE__ */ new Set();
|
|
10252
|
+
waiting.add(decided.resolve);
|
|
10253
|
+
this.deciding.set(ppid, waiting);
|
|
10254
|
+
}
|
|
10255
|
+
};
|
|
10256
|
+
}
|
|
10257
|
+
identityOf(pid) {
|
|
10258
|
+
const parent = this.jobs.shownParent(pid);
|
|
10259
|
+
const alive = parent !== void 0 && this.running(parent);
|
|
10260
|
+
return { pid: this.jobs.shown(pid), ppid: alive ? parent : INIT_PID };
|
|
10261
|
+
}
|
|
10262
|
+
running(shown) {
|
|
10263
|
+
return this.jobs.list().some((member) => !this.zombies.has(member.pid) && this.jobs.shown(member.pid) === shown);
|
|
10264
|
+
}
|
|
10265
|
+
decide(ppid) {
|
|
10266
|
+
const waiting = this.deciding.get(ppid);
|
|
10267
|
+
if (!waiting) return;
|
|
10268
|
+
this.deciding.delete(ppid);
|
|
10269
|
+
setTimeout(() => {
|
|
10270
|
+
for (const resolve of waiting) resolve(true);
|
|
10271
|
+
}, 0);
|
|
10272
|
+
}
|
|
10214
10273
|
forker(ppid, parent) {
|
|
10215
10274
|
const { exec: _exec, ...image } = parent;
|
|
10216
10275
|
return async (state, fds) => childHandle(
|