@ai-ecoverse/slicc-kernel 1.102.0 → 1.102.2
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 +24 -8
- package/dist/kernel-worker.js +89 -45
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +56 -1
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +89 -45
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +56 -1
- package/dist/process-worker.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -261,7 +261,7 @@ An empty path is `ENOENT` in every path call (`path_open`, `path_filestat_get`,
|
|
|
261
261
|
On top of preview1 a WASI program gets:
|
|
262
262
|
|
|
263
263
|
- **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.
|
|
264
|
-
- **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), with the unwound frames kept in the unused part of the program's stack, below its stack pointer (for a position-independent program, the stack the kernel's linker gives it), so a fork from deep in the stack leaves the program's data alone, and a forked child's thread-locals (`errno` included) where the parent left them: the kernel reruns the module's `__wasm_init_tls` at the parent's TLS base (and puts back the block it overwrites), which also sets the internal globals that hold thread-local addresses (a position-independent program's, a side module's, and those of a variable in the `global-dynamic` TLS model), 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. A forked child also gets the side modules its parent had loaded, at the same addresses and table slots, with the slot and closure counters and the parent's thread-local blocks: they are linked again in the child before it resumes, without running their constructors again, since their data is already in the copied memory. A side-module function on the stack when `fork` is called (a callback into the main module that forks) is not instrumented by Asyncify, so it runs again when the stack is rewound, in the parent and in the child, and libffi frames there can trap; side modules would need building with Asyncify for that, which the toolchain does not do yet. 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`. Each WASIX thread has its own Asyncify driver too, so a thread can `longjmp` through Asyncify and `fork` (#710). The unwound frames go at the bottom of the thread's own stack (the `stack` and `stack_size` its libc passes to `wasi_thread_start`), never on the main thread's. A thread whose start args don't look like wasix-libc's (a `stack_size` under 4096 or over the memory, a stack top that isn't 16-aligned or not right above the args) gets no driver, so `fork` there is `ENOSYS` as before, rather than an unwind buffer at a bad address. A child forked from a thread runs as that thread: it's rewound through `wasi_thread_start`, can fork again, and ends when the thread's function returns. A program built with exceptions (every position-independent one) does `setjmp` with exceptions rather than Asyncify, so there only `fork` uses the driver.
|
|
264
|
+
- **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), with the unwound frames kept in the unused part of the program's stack, below its stack pointer (for a position-independent program, the stack the kernel's linker gives it), so a fork from deep in the stack leaves the program's data alone, and a forked child's thread-locals (`errno` included) where the parent left them: the kernel reruns the module's `__wasm_init_tls` at the parent's TLS base (and puts back the block it overwrites), which also sets the internal globals that hold thread-local addresses (a position-independent program's, a side module's, and those of a variable in the `global-dynamic` TLS model), 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. A forked child also gets the side modules its parent had loaded, at the same addresses and table slots, with the slot and closure counters and the parent's thread-local blocks: they are linked again in the child before it resumes, without running their constructors again, since their data is already in the copied memory. A side-module function on the stack when `fork` is called (a callback into the main module that forks) is not instrumented by Asyncify, so it runs again when the stack is rewound, in the parent and in the child, and libffi frames there can trap; side modules would need building with Asyncify for that, which the toolchain does not do yet. 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`. Each WASIX thread has its own Asyncify driver too, so a thread can `longjmp` through Asyncify and `fork` (#710). The unwound frames go at the bottom of the thread's own stack (the `stack` and `stack_size` its libc passes to `wasi_thread_start`), never on the main thread's. A thread whose start args don't look like wasix-libc's (a `stack_size` under 4096 or over the memory, a stack top that isn't 16-aligned or not right above the args) gets no driver, so `fork` there is `ENOSYS` as before, rather than an unwind buffer at a bad address. A child forked from a thread runs as that thread: it's rewound through `wasi_thread_start`, can fork again, and ends when the thread's function returns. A program built with exceptions (every position-independent one) does `setjmp` with exceptions rather than Asyncify, so there only `fork` uses the driver. A kernel timer can be longer than `setTimeout`'s 2^31-1 ms (about 24.8 days): an `alarm`, an interval timer, or a `select`, `poll_oneoff` or `epoll_wait` timeout waits its whole time, re-armed in steps of at most 2^31-1 ms, instead of firing at once or spinning (#767).
|
|
265
265
|
- **epoll and eventfd** (WASIX `epoll_create`, `epoll_ctl`, `epoll_wait` and `fd_event`, which tokio's reactor uses through the WASIX mio fork):
|
|
266
266
|
- **Where they live:** an epoll instance and an eventfd are kernel descriptors, so they survive `dup`, `fork` and `exec` (unless close-on-exec) and are shared by a process's threads.
|
|
267
267
|
- **What epoll watches:** sockets (listening, connecting, connected, loopback and uplink), pipes, terminals, `kernel.run`'s stdin, eventfds and other epoll instances (a cycle of instances is `ELOOP`).
|
|
@@ -307,7 +307,7 @@ On top of preview1 a WASI program gets:
|
|
|
307
307
|
- Nothing is undone when a process exits.
|
|
308
308
|
- **Limits:**
|
|
309
309
|
- Semaphores exist only within one kernel (not across tabs), and they don't appear in `/dev/shm`.
|
|
310
|
-
- A kernel holds at most
|
|
310
|
+
- A kernel holds at most 65,536 named ones (`ENOSPC`), and a process at most 65,536 open handles (`EMFILE`; a refused `O_CREAT` leaves no name behind). Those are memory bounds (a semaphore is about 100 bytes), not musl's 256 (`SEM_NSEMS_MAX`), which wasix-sysroot -31 drops: python's multiprocessing opens one per `Lock` (#770).
|
|
311
311
|
- `MAP_SHARED` mappings aren't shared between processes, so unnamed semaphores (`sem_init` with `pshared`) work only between a process's threads, and `multiprocessing.shared_memory` doesn't work.
|
|
312
312
|
- **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.
|
|
313
313
|
- **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. An Emscripten program stops at its next kernel call as well, and at its next write, read or sleep that its glue answers without the kernel (a write to its own `/dev/null`, as in `yes > /dev/null`), so a stopped one keeps no core busy unless it computes without any of these calls. A stopped program doesn't move on while stopped: a `sleep` (or a blocked read) that would end meanwhile ends after `SIGCONT`. A call that the stop interrupted runs again after `SIGCONT`, as on Linux. One with a timeout (`select`, `poll`, `ppoll`, `pselect`, `nanosleep`, a timed semaphore wait) waits only for the time that's left, counted from when the kernel first got the call, so it still ends at its deadline. `epoll_wait` and `epoll_pwait` aren't run again: they fail with `EINTR`, even without a handler (signal(7)). A signal for such a program is also delivered at those glue-answered calls, so a handler runs in a loop that only writes to `/dev/null`. 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 signal that arrives while a thread computes is thus delivered at its next call; when that call is a wait (`poll_oneoff` with a timeout or a descriptor, so `nanosleep` and `poll`/`select` too, `sigmask_poll`, or a futex wait), the handler has run during the call, so the wait ends at once: `EINTR` for a libc of -22 or later (an older one sees its clocks fire, as for any interruption), and a futex wait returns woken. 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).
|
|
@@ -347,7 +347,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
347
347
|
- `fdStatus(fd)` (`{ type, tty, seekable }`, `type` being `file`, `pipe`, `tty`, `socket`, `device` or `directory`) and `isatty(fd)`, to tell `< file` from a pipe;
|
|
348
348
|
- `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;
|
|
349
349
|
- `fs`: `stat`, `lstat`, `exists`, `readdir`, `mkdir` (with parents, or, with `{ recursive: false }`, as Node's `fs.mkdir` without `recursive`: `EEXIST` for an existing entry and `ENOENT` for a missing parent, atomically among the processes of one kernel, so it can serve as a lock there, but not between two kernels on one origin, such as two tabs ([#519](https://github.com/ai-ecoverse/slicc-kernel/issues/519)); `sync.fs.mkdir` takes the same option), `rm` (recursive), `unlink`, `rename`, `symlink`, `readlink`, `statfs`, `chmod(path, mode)`, and `readFile`/`writeFile` for small files. `chmod` takes the permission bits (`0` to `0o7777`, `EINVAL` otherwise) and follows a symlink; as `chmod(2)`, only the file's owner or root may (`EPERM`), so a program can make a script it wrote runnable by path. `sync.fs.chmod` is the same, synchronously. `statfs(path)` answers `{ type, bsize, blocks, bfree, bavail, files, ffree, quota, usage }`: the fields of Node's `StatFs`, plus `quota` and `usage` in bytes (what jsh 0.17 reads). The values come from the same source as an Emscripten program's `statfs`/`df`: the driver of the mount the path is on, or, on the root file system, the origin's storage estimate (`navigator.storage.estimate()`). `bsize` is 4096, `bfree` and `bavail` are what's left of the quota, and `type`, `files` and `ffree` are 0, since the kernel doesn't know them. Where there's no estimate (the Node entry's root), every count is 0. A missing path is `ENOENT`. `sync.fs` has it too. `rm` removes a whole tree in one kernel call (on the OPFS root, one recursive removal and one metadata delete: 50,000 files take about 3 s in Chrome, where `rm -r` takes about 25 s) with the result of a file-by-file removal: symlinks inside the tree are removed, never followed; a tree holding a protected path is refused with `EROFS` and nothing is removed; a mount inside the tree is emptied and its mount point answers `EBUSY`, while the rest is removed; under `users`, what the user may remove is removed and the call then fails with the first `EACCES`/`EPERM`; files a process holds open stay readable to it;
|
|
350
|
-
- `/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` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn'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.
|
|
350
|
+
- `/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` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn'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. Removing a `/dev` entry fails (see [`/dev`](#dev-entries)), and so does a new name directly in `/dev` or `/proc`;
|
|
351
351
|
- `/proc`, in `fs`, `open` and their `sync` forms, the same as a WASI program's (see [`/proc`](#proc)): `stat`, `lstat`, `exists`, `readdir`, `readlink` and `readFile` answer it, and `/proc/self` is the program's own pid. It's read-only, so every change in it is `EROFS`: `writeFile`, `mkdir`, `rm`, `unlink`, `chmod`, a `rename`, `link` or `symlink` into or out of it, and an `open` for writing, creating or truncating. A file opened in it is a snapshot read at the open, with no descriptor (`fd` is `-1`): its `read` and `size` answer from the snapshot until it's closed, and `write` and `truncate` are `EBADF`. Other paths pay only a prefix check;
|
|
352
352
|
- `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;
|
|
353
353
|
- `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;
|
|
@@ -825,6 +825,14 @@ A character device (`0666`), there in every `users` mode, through which programs
|
|
|
825
825
|
- **Not caught:** a file changed around the kernel between an open and a later ranged read of it is read as it is then. Checking every ranged read would cost a metadata read for each one. Nor is a same-size rewrite by a writer in another kernel on the origin (another tab), read before that tab's change notice arrives: these checks take the write count from the metadata store's cache, which the notice clears, so the window runs from the write to the notice. OPFS's own `lastModified` and size are read every time, so a change made outside every kernel is still caught.
|
|
826
826
|
- **Where it is a boundary:** it's one in a browser, where programs' own code can't reach OPFS around the kernel (see [JS programs](#js-programs)). Under the Node entry it isn't, until [#407](https://github.com/ai-ecoverse/slicc-kernel/issues/407), and there `kernel.writeFile` under a path fails too, so write those files into `root` before creating the kernel.
|
|
827
827
|
|
|
828
|
+
### `/dev` entries
|
|
829
|
+
|
|
830
|
+
`/dev` and `/proc` hold the kernel's own entries, and a program can't add or remove names there. Only `/dev/shm`, a tmpfs, takes new files.
|
|
831
|
+
- **Removing** an entry fails and leaves it: `EACCES` for a user, `EPERM` for root on a device, `EBUSY` for `/dev` itself.
|
|
832
|
+
- **Creating** a name that doesn't exist fails in every runtime, whether it's a file, a directory, a symlink, a device node, or the new name of a link or a rename: `EACCES` for a user, `EPERM` for root in `/dev`, and `EROFS` in `/proc`.
|
|
833
|
+
- **Why:** an Emscripten program's `/dev` and `/proc` live in its glue's own memory, so such a file used to look written and then vanish when the process exited. On the root file system no other runtime would see it either.
|
|
834
|
+
- **Existing entries** (`/dev/null`, the terminals, `/dev/std*`) open and write as before.
|
|
835
|
+
|
|
828
836
|
### `/proc`
|
|
829
837
|
|
|
830
838
|
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. A process that exits while a program reads the `/proc` directory doesn't fail or cut short the listing: its pid is still listed, and opening its files fails with `ENOENT`, which `ps`, `pgrep` and `pidof` skip, as on Linux; 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`. `/proc/version` names the kernel, Linux-shaped, with the slicc-kernel version that built it: `Linux version 6.0.0-slicc (slicc-kernel <version>) #1 SMP PREEMPT_DYNAMIC`. `/proc/sys/kernel/osrelease` is `6.0.0-slicc` and `/proc/sys/kernel/ostype` is `Linux`. The version is the one npm publishes, set when the package is built (`0.0.0-dev` when run from the sources). A JS program reads it as `ctx.kernel.version`. 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 are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`; a process with a tag, see `kernel.run`, also has `Tag:\t<tag>` after `Gid:`, shown to root and to the same uid only, with newlines, tabs and carriage returns written `\n`, `\t` and `\r`) and 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 are measured per process, its threads and the images it exec'd included: time a worker spends waiting for the kernel's answer is system time (at most 20 ms of each call; the rest of a longer one, such as a write into a full pipe, counts as blocked), time it spends blocked (a read, a `poll` or `select` that waits, a sleep, `wait`, a futex, while stopped) is neither, from the moment the call actually starts to wait (a call that could wait but answers at once, such as a `select` with a zero timeout or a ready descriptor, a `waitpid` with `WNOHANG`, or a futex wait whose value has already changed, is system time), and the rest is user time; a process's times stop when it ends, a call still in flight then adds nothing, so a loop that never calls the kernel shows as user time while it runs; they show in `/proc/<pid>/stat`, `ps` and `ps()`, and the times of the children a process has waited for (theirs and their own waited-for children's, as POSIX counts them) show as its `cutime` and `cstime` in `/proc/<pid>/stat`; a stopped process is `T` there, in `/proc/<pid>/status` (`T (stopped)`) and in `ps()`; a WASI or WASIX program's `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` (`clock_time_get`) count only the time its thread runs, its own waits and blocked kernel calls left out (a sleep is not CPU time), per thread for now, so a threaded program's process clock is its calling thread's; and `/proc/stat` counts every process of the kernel, the reaped ones included, and `/proc/loadavg` averages the processes not blocked, sampled every 5 seconds. A JS program's time isn't measured yet. `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) is a placeholder. A process that isn't root sees only its own user's processes (see [Users](#users)). WASI and WASIX programs see the same `/proc`, read-only (opening a file in it for writing, or linking one of its files elsewhere, fails with `EROFS`), from every thread, also through a symlink that points into it (where every change, `mkdir`, `rename`, `unlink`, `chmod`, a hard link from it or into it, and so on, fails too: with `EROFS` when the root file system has a physical `/proc`, which stays hidden and untouched, and otherwise with the `ENOENT` of the missing directory, since that check is only paid for when a physical `/proc` exists; with one, a symlink whose target passes `..` after a file or a missing name, such as `/f/../proc`, fails with `ENOTDIR` or `ENOENT`, as on Linux), and `/` lists it, so a WASI program gets a preopen for it; `/proc/self` is the program's own pid there too (the pid an exec'd program took over). WASI and WASIX programs also have `/proc/<pid>/task/<tid>/stat` for each of a process's threads (the main thread is tid 1), whose state is `S` while the thread is blocked in the kernel (a `sem_wait`, a blocking read, a sleep, a futex) and `R` otherwise (`T` and `Z` as for the process). JS programs read the same `/proc` through `ctx.fs` and `ctx.open` (see [JS programs](#js-programs)). Neither has `/proc/<pid>/fd` yet.
|
|
@@ -1020,20 +1028,28 @@ A threaded WASIX C program for `test/integration/thread-asyncify.test.mjs` (#710
|
|
|
1020
1028
|
|
|
1021
1029
|
### `stopwait`
|
|
1022
1030
|
|
|
1023
|
-
A WASIX C program for `test/integration/stop-restart.test.mjs` (#738). `stopwait CALL` forks a child that waits 300 ms in `CALL` (`epoll`, `poll`, `ppoll`, `select`, `pselect`, `nanosleep` or `clock_nanosleep`) on a pipe that never gets data. The parent stops the child
|
|
1031
|
+
A WASIX C program for `test/integration/stop-restart.test.mjs` (#738). `stopwait CALL` forks a child that waits 300 ms in `CALL` (`epoll`, `poll`, `ppoll`, `select`, `pselect`, `nanosleep` or `clock_nanosleep`) on a pipe that never gets data. The parent stops the child once `/proc/<pid>/task/1/stat` shows it blocked (`S`), or 100 ms after it's ready on a kernel without that state, continues it 200 ms later, and reports whether the stop found it blocked. The child prints the result, the error and whether the wait ended before the deadline, at it (300–450 ms) or late.
|
|
1032
|
+
|
|
1033
|
+
**Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions) and binaryen's `wasm-opt` (`--asyncify` for `proc_fork`) on `PATH`. `bin/stopwait.wasm` is 59,958 bytes, sha256 `ce49e04baed13b7ccf04d8231da4a29567fde0dac19d5cc81f36c185e0eb4555`.
|
|
1034
|
+
|
|
1035
|
+
### `longwait`
|
|
1036
|
+
|
|
1037
|
+
A WASIX C program for `test/integration/long-timers.test.mjs` (#767).
|
|
1038
|
+
- `longwait alarm` sets `alarm(3000000)` (about 35 days), sleeps 0.5 s, and prints how many alarms came and how many seconds were left.
|
|
1039
|
+
- `longwait select|ppoll|nanosleep` waits 3,000,000 s on a pipe that never gets data, until a 300 ms interval timer interrupts the wait.
|
|
1024
1040
|
|
|
1025
|
-
**Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7
|
|
1041
|
+
**Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` on `PATH`. `bin/longwait.wasm` is 44,010 bytes, sha256 `70ae2adfb46864fad3af0241fe26f401a62a5176ccd25ab82fe6588178cfb5c8`.
|
|
1026
1042
|
|
|
1027
1043
|
### `semtest`
|
|
1028
1044
|
|
|
1029
1045
|
A WASIX C program for `test/integration/semaphore.test.mjs` (#717). It calls the `slicc.sem_*` imports directly (`import_module("slicc")`), so the rows don't wait for the libc side. `semtest MODE` runs one of:
|
|
1030
1046
|
- `handoff`, `inherit`, `spawn` (a spawned and an exec'd image open the semaphore by name; exec drops the handles);
|
|
1031
|
-
- `try`, `timed`, `eintr`, `stopped` (a forked child's 300 ms timed wait is stopped for 200 ms and continued, and reports whether it ended at the deadline);
|
|
1047
|
+
- `try`, `timed`, `eintr`, `stopped` (a forked child's 300 ms timed wait is stopped for 200 ms and continued, and reports whether it ended at the deadline; the parent stops it only once `/proc/<pid>/task/1/stat` shows it blocked, `S`, or after 100 ms on a kernel without that state, and reports which);
|
|
1032
1048
|
- `threads` (FIFO, one thread blocked at a time);
|
|
1033
1049
|
- `errors`, `unlink`, `owners setup|user`;
|
|
1034
|
-
- `killed` (a waiter killed before posts loses no unit), `exited`, `cap
|
|
1050
|
+
- `killed` (a waiter killed before posts loses no unit), `exited`, `cap [N]` (creates up to N names, 10,000 by default), `handles [N]` (opens up to N, 300 by default).
|
|
1035
1051
|
|
|
1036
|
-
**Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions, `-pthread`) and binaryen's `wasm-opt` (version 132, `--asyncify` for `proc_fork`) on `PATH`. `bin/semtest.wasm` is
|
|
1052
|
+
**Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions, `-pthread`) and binaryen's `wasm-opt` (version 132, `--asyncify` for `proc_fork`) on `PATH`. `bin/semtest.wasm` is 69,366 bytes, sha256 `b3202708f57acad5f51e5be1b06e3b5a0ad02dd84b3791e052787117b828da4e`.
|
|
1037
1053
|
|
|
1038
1054
|
### `wlocktest`
|
|
1039
1055
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -8460,6 +8460,11 @@ function devRemoval(path, cred) {
|
|
|
8460
8460
|
if (cred && cred.euid !== 0) return "EACCES";
|
|
8461
8461
|
return path === "/dev" ? "EBUSY" : "EPERM";
|
|
8462
8462
|
}
|
|
8463
|
+
function devCreation(path, cred) {
|
|
8464
|
+
if (path === "/proc" || path.startsWith("/proc/")) return "EROFS";
|
|
8465
|
+
if (path === "/dev" || !inDevTree(path)) return void 0;
|
|
8466
|
+
return cred && cred.euid !== 0 ? "EACCES" : "EPERM";
|
|
8467
|
+
}
|
|
8463
8468
|
|
|
8464
8469
|
// src/fs/fresh-modules.ts
|
|
8465
8470
|
var MAGIC = [0, 97, 115, 109];
|
|
@@ -11266,12 +11271,29 @@ async function guardAndResolve(entry, path, req) {
|
|
|
11266
11271
|
};
|
|
11267
11272
|
}
|
|
11268
11273
|
var REMOVALS = /* @__PURE__ */ new Set(["unlink", "rmdir", "rm"]);
|
|
11274
|
+
var CREATIONS = /* @__PURE__ */ new Set(["write", "mkdir", "symlink", "create"]);
|
|
11275
|
+
var NAMING = /* @__PURE__ */ new Set(["link", "rename"]);
|
|
11276
|
+
async function newDevName(entry, path, req) {
|
|
11277
|
+
const named = NAMING.has(req.op);
|
|
11278
|
+
if (!named && !CREATIONS.has(req.op)) return void 0;
|
|
11279
|
+
const target = named ? entry.fs.resolvePath(entry.cwd, req.arg2 ?? "") : path;
|
|
11280
|
+
const refused2 = devCreation(target, entry.cred?.());
|
|
11281
|
+
if (!refused2) return void 0;
|
|
11282
|
+
const there = await entry.fs.lstat(target).then(
|
|
11283
|
+
() => true,
|
|
11284
|
+
() => false
|
|
11285
|
+
);
|
|
11286
|
+
if (there) return void 0;
|
|
11287
|
+
return { ok: false, errno: refused2, message: `${target}: no new names in /dev or /proc` };
|
|
11288
|
+
}
|
|
11269
11289
|
async function dispatchOne(entry, req) {
|
|
11270
11290
|
const { fs, cwd } = entry;
|
|
11271
11291
|
try {
|
|
11272
11292
|
const path = fs.resolvePath(cwd, req.path);
|
|
11273
11293
|
const device = REMOVALS.has(req.op) ? devRemoval(path, entry.cred?.()) : void 0;
|
|
11274
11294
|
if (device) return { ok: false, errno: device, message: `${path}: a device entry stays` };
|
|
11295
|
+
const made2 = await newDevName(entry, path, req);
|
|
11296
|
+
if (made2) return made2;
|
|
11275
11297
|
const held = await byHandle(entry, path, req);
|
|
11276
11298
|
if (held) return held;
|
|
11277
11299
|
if (req.op === "access") return await accessOp(entry, path, req);
|
|
@@ -12114,7 +12136,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
|
|
|
12114
12136
|
var EARLY_WHOLE_FILE = 64 * 1024;
|
|
12115
12137
|
|
|
12116
12138
|
// src/version.ts
|
|
12117
|
-
var KERNEL_VERSION = true ? "1.102.
|
|
12139
|
+
var KERNEL_VERSION = true ? "1.102.2" : "0.0.0-dev";
|
|
12118
12140
|
var OS_RELEASE = "6.0.0-slicc";
|
|
12119
12141
|
function procVersion() {
|
|
12120
12142
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -12628,6 +12650,41 @@ var EventFd = class _EventFd {
|
|
|
12628
12650
|
}
|
|
12629
12651
|
};
|
|
12630
12652
|
|
|
12653
|
+
// src/kernel/timer.ts
|
|
12654
|
+
var TIMER_MAX_MS = 2147483647;
|
|
12655
|
+
function timerAt(deadline, fire) {
|
|
12656
|
+
let timer;
|
|
12657
|
+
const arm = () => {
|
|
12658
|
+
const left = deadline - performance.now();
|
|
12659
|
+
if (left > 0) {
|
|
12660
|
+
timer = setTimeout(arm, Math.min(left, TIMER_MAX_MS));
|
|
12661
|
+
return;
|
|
12662
|
+
}
|
|
12663
|
+
timer = void 0;
|
|
12664
|
+
fire();
|
|
12665
|
+
};
|
|
12666
|
+
timer = setTimeout(arm, Math.min(Math.max(0, deadline - performance.now()), TIMER_MAX_MS));
|
|
12667
|
+
return () => clearTimeout(timer);
|
|
12668
|
+
}
|
|
12669
|
+
function timerAfter(ms, fire) {
|
|
12670
|
+
return timerAt(performance.now() + ms, fire);
|
|
12671
|
+
}
|
|
12672
|
+
function repeating(first, every, fire) {
|
|
12673
|
+
let next2 = performance.now() + first;
|
|
12674
|
+
let cancel = () => {
|
|
12675
|
+
};
|
|
12676
|
+
const tick = () => {
|
|
12677
|
+
fire();
|
|
12678
|
+
if (every <= 0) return;
|
|
12679
|
+
next2 += every;
|
|
12680
|
+
const now = performance.now();
|
|
12681
|
+
if (next2 <= now) next2 += (Math.floor((now - next2) / every) + 1) * every;
|
|
12682
|
+
cancel = timerAt(next2, tick);
|
|
12683
|
+
};
|
|
12684
|
+
cancel = timerAt(next2, tick);
|
|
12685
|
+
return () => cancel();
|
|
12686
|
+
}
|
|
12687
|
+
|
|
12631
12688
|
// src/kernel/epoll.ts
|
|
12632
12689
|
var EPOLL = {
|
|
12633
12690
|
IN: 1,
|
|
@@ -12679,11 +12736,11 @@ function lookup(fds, fd) {
|
|
|
12679
12736
|
}
|
|
12680
12737
|
function pause(ms, signal) {
|
|
12681
12738
|
return new Promise((resolve2) => {
|
|
12682
|
-
const
|
|
12739
|
+
const cancel = timerAfter(ms, resolve2);
|
|
12683
12740
|
signal.addEventListener(
|
|
12684
12741
|
"abort",
|
|
12685
12742
|
() => {
|
|
12686
|
-
|
|
12743
|
+
cancel();
|
|
12687
12744
|
resolve2();
|
|
12688
12745
|
},
|
|
12689
12746
|
{ once: true }
|
|
@@ -12749,17 +12806,14 @@ var EpollFile = class _EpollFile {
|
|
|
12749
12806
|
const round = new AbortController();
|
|
12750
12807
|
const woken = this.changes.wait(AbortSignal.any([round.signal, signal]));
|
|
12751
12808
|
woken.catch(() => void 0);
|
|
12752
|
-
let
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
(resolve2) => timer = setTimeout(resolve2, deadline - performance.now())
|
|
12756
|
-
)
|
|
12757
|
-
];
|
|
12809
|
+
let cancel = () => {
|
|
12810
|
+
};
|
|
12811
|
+
const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => cancel = timerAt(deadline, resolve2))];
|
|
12758
12812
|
try {
|
|
12759
12813
|
await Promise.race([woken, ...expiry]);
|
|
12760
12814
|
} finally {
|
|
12761
12815
|
round.abort();
|
|
12762
|
-
|
|
12816
|
+
cancel();
|
|
12763
12817
|
}
|
|
12764
12818
|
}
|
|
12765
12819
|
}
|
|
@@ -13104,9 +13158,10 @@ var AsyncOps = class {
|
|
|
13104
13158
|
}
|
|
13105
13159
|
sleep(timeoutMs, signal) {
|
|
13106
13160
|
return new Promise((resolve2, reject) => {
|
|
13107
|
-
let
|
|
13161
|
+
let cancel = () => {
|
|
13162
|
+
};
|
|
13108
13163
|
const cleanup = () => {
|
|
13109
|
-
|
|
13164
|
+
cancel();
|
|
13110
13165
|
this.waiters.delete(wake);
|
|
13111
13166
|
signal.removeEventListener("abort", abort);
|
|
13112
13167
|
};
|
|
@@ -13121,10 +13176,10 @@ var AsyncOps = class {
|
|
|
13121
13176
|
this.waiters.add(wake);
|
|
13122
13177
|
signal.addEventListener("abort", abort, { once: true });
|
|
13123
13178
|
if (timeoutMs !== void 0) {
|
|
13124
|
-
|
|
13179
|
+
cancel = timerAfter(timeoutMs, () => {
|
|
13125
13180
|
cleanup();
|
|
13126
13181
|
resolve2(false);
|
|
13127
|
-
}
|
|
13182
|
+
});
|
|
13128
13183
|
}
|
|
13129
13184
|
});
|
|
13130
13185
|
}
|
|
@@ -14511,14 +14566,14 @@ async function runChild(req, deps) {
|
|
|
14511
14566
|
return frame2(outcome2, new Uint8Array(0), new Uint8Array(0));
|
|
14512
14567
|
}
|
|
14513
14568
|
if (failure2 !== void 0) kill();
|
|
14514
|
-
const
|
|
14569
|
+
const cancel = req.timeoutMs !== void 0 && req.timeoutMs > 0 ? timerAfter(req.timeoutMs, () => stop("ETIMEDOUT")) : void 0;
|
|
14515
14570
|
let status;
|
|
14516
14571
|
try {
|
|
14517
14572
|
status = await reaped(deps, pid);
|
|
14518
14573
|
const open = controls.filter((c) => c.opened);
|
|
14519
14574
|
await Promise.race([Promise.all(open.map((c) => c.eof)), decided.promise]);
|
|
14520
14575
|
} finally {
|
|
14521
|
-
|
|
14576
|
+
cancel?.();
|
|
14522
14577
|
for (const control of controls) control.shut = true;
|
|
14523
14578
|
}
|
|
14524
14579
|
const out = deps.children.captured(pid, 1);
|
|
@@ -14574,27 +14629,23 @@ async function waitReady(fds, read, write, deadline, interrupted3) {
|
|
|
14574
14629
|
const file = fds.get(fd).file;
|
|
14575
14630
|
return file.changed === void 0 ? [] : [file.changed(round.signal).catch(() => void 0)];
|
|
14576
14631
|
});
|
|
14577
|
-
let
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
(resolve2) => timer = setTimeout(resolve2, deadline - performance.now())
|
|
14581
|
-
)
|
|
14582
|
-
];
|
|
14632
|
+
let cancel = () => {
|
|
14633
|
+
};
|
|
14634
|
+
const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => cancel = timerAt(deadline, resolve2))];
|
|
14583
14635
|
try {
|
|
14584
14636
|
await Promise.race([...changes, ...expiry, interrupted3]);
|
|
14585
14637
|
} finally {
|
|
14586
14638
|
round.abort();
|
|
14587
|
-
|
|
14639
|
+
cancel();
|
|
14588
14640
|
}
|
|
14589
14641
|
}
|
|
14590
14642
|
}
|
|
14591
14643
|
|
|
14592
14644
|
// src/kernel/sem.ts
|
|
14593
14645
|
var SEM_VALUE_MAX = 2147483647;
|
|
14594
|
-
var SEM_NAMES_MAX =
|
|
14595
|
-
var SEM_HANDLES_MAX =
|
|
14646
|
+
var SEM_NAMES_MAX = 65536;
|
|
14647
|
+
var SEM_HANDLES_MAX = 65536;
|
|
14596
14648
|
var NAME_MAX = 251;
|
|
14597
|
-
var TIMER_MAX_MS = 2147483647;
|
|
14598
14649
|
var SEM_OPS = [
|
|
14599
14650
|
"sem-open",
|
|
14600
14651
|
"sem-close",
|
|
@@ -14631,11 +14682,12 @@ var Semaphore = class {
|
|
|
14631
14682
|
if (signal.aborted) return Promise.reject(new KernelError("EINTR"));
|
|
14632
14683
|
return new Promise((resolve2, reject) => {
|
|
14633
14684
|
const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : performance.now() + timeoutMs;
|
|
14634
|
-
let
|
|
14685
|
+
let cancel = () => {
|
|
14686
|
+
};
|
|
14635
14687
|
const leave = () => {
|
|
14636
14688
|
const at = this.waiters.indexOf(waiter);
|
|
14637
14689
|
if (at >= 0) this.waiters.splice(at, 1);
|
|
14638
|
-
|
|
14690
|
+
cancel();
|
|
14639
14691
|
signal.removeEventListener("abort", abort);
|
|
14640
14692
|
};
|
|
14641
14693
|
const waiter = {
|
|
@@ -14649,18 +14701,13 @@ var Semaphore = class {
|
|
|
14649
14701
|
reject(new KernelError("EINTR"));
|
|
14650
14702
|
};
|
|
14651
14703
|
const expire = () => {
|
|
14652
|
-
const left = deadline - performance.now();
|
|
14653
|
-
if (left > 0) {
|
|
14654
|
-
timer = setTimeout(expire, Math.min(left, TIMER_MAX_MS));
|
|
14655
|
-
return;
|
|
14656
|
-
}
|
|
14657
14704
|
leave();
|
|
14658
14705
|
resolve2(false);
|
|
14659
14706
|
};
|
|
14660
14707
|
this.waiters.push(waiter);
|
|
14661
14708
|
signal.addEventListener("abort", abort, { once: true });
|
|
14662
14709
|
if (deadline !== Number.POSITIVE_INFINITY) {
|
|
14663
|
-
|
|
14710
|
+
cancel = timerAt(deadline, expire);
|
|
14664
14711
|
}
|
|
14665
14712
|
});
|
|
14666
14713
|
}
|
|
@@ -14695,6 +14742,10 @@ var SemTable = class {
|
|
|
14695
14742
|
var SemHandles = class _SemHandles {
|
|
14696
14743
|
byHandle = /* @__PURE__ */ new Map();
|
|
14697
14744
|
next = 1;
|
|
14745
|
+
limit;
|
|
14746
|
+
constructor(limit = SEM_HANDLES_MAX) {
|
|
14747
|
+
this.limit = limit;
|
|
14748
|
+
}
|
|
14698
14749
|
add(sem) {
|
|
14699
14750
|
for (const [handle2, entry] of this.byHandle) {
|
|
14700
14751
|
if (entry.sem === sem) {
|
|
@@ -14702,7 +14753,7 @@ var SemHandles = class _SemHandles {
|
|
|
14702
14753
|
return handle2;
|
|
14703
14754
|
}
|
|
14704
14755
|
}
|
|
14705
|
-
if (this.byHandle.size >=
|
|
14756
|
+
if (this.byHandle.size >= this.limit) throw new KernelError("EMFILE");
|
|
14706
14757
|
const handle = this.next++;
|
|
14707
14758
|
this.byHandle.set(handle, { sem, opens: 1 });
|
|
14708
14759
|
return handle;
|
|
@@ -14718,7 +14769,7 @@ var SemHandles = class _SemHandles {
|
|
|
14718
14769
|
if (--entry.opens === 0) this.byHandle.delete(handle);
|
|
14719
14770
|
}
|
|
14720
14771
|
fork() {
|
|
14721
|
-
const child = new _SemHandles();
|
|
14772
|
+
const child = new _SemHandles(this.limit);
|
|
14722
14773
|
child.next = this.next;
|
|
14723
14774
|
for (const [handle, entry] of this.byHandle) child.byHandle.set(handle, { ...entry });
|
|
14724
14775
|
return child;
|
|
@@ -17005,17 +17056,12 @@ var WasmProcess = class {
|
|
|
17005
17056
|
(req, cancelled) => this.syscall(req, cancelled),
|
|
17006
17057
|
() => this.options.onAsync?.()
|
|
17007
17058
|
);
|
|
17008
|
-
alarmEvery;
|
|
17009
17059
|
setAlarm(sig, first, every, timer) {
|
|
17010
17060
|
if (!isSignal(sig)) throw new KernelError("EINVAL");
|
|
17011
17061
|
this.clearAlarm();
|
|
17012
17062
|
if (first <= 0) return;
|
|
17013
17063
|
const fire = timer === void 0 ? () => this.options.raise?.(sig) : () => this.timerExpired(timer);
|
|
17014
|
-
this.alarm =
|
|
17015
|
-
this.alarm = void 0;
|
|
17016
|
-
fire();
|
|
17017
|
-
if (every > 0) this.alarmEvery = setInterval(fire, every);
|
|
17018
|
-
}, first);
|
|
17064
|
+
this.alarm = repeating(first, every, fire);
|
|
17019
17065
|
}
|
|
17020
17066
|
timerExpired(which) {
|
|
17021
17067
|
this.options.onTimer?.(which);
|
|
@@ -17024,10 +17070,8 @@ var WasmProcess = class {
|
|
|
17024
17070
|
blocked2.abort();
|
|
17025
17071
|
}
|
|
17026
17072
|
clearAlarm() {
|
|
17027
|
-
|
|
17028
|
-
clearInterval(this.alarmEvery);
|
|
17073
|
+
this.alarm?.();
|
|
17029
17074
|
this.alarm = void 0;
|
|
17030
|
-
this.alarmEvery = void 0;
|
|
17031
17075
|
}
|
|
17032
17076
|
async exit() {
|
|
17033
17077
|
if (this.exited) return;
|