Module host and handle
defineModule
Section titled “defineModule”defineModule<const M extends ModuleDecl>(module: { manifest: M; adapter: (host: HostOf<M>) => Hooks<M> | Promise<Hooks<M>> }): Module<M>;Types a module definition and returns it unchanged. The manifest literal is inferred first, and it fixes the adapter’s host and the hooks it must return. Import it from @oxidevm/sdk/wasm.
The host
Section titled “The host”The adapter’s host has these members whatever the manifest says, plus the members of the capabilities it lists.
| Member | Meaning |
|---|---|
signal |
An AbortSignal that aborts when the module is stopping. |
options |
The declared options, with defaults applied. |
artifacts.compile(name) |
The declared .wasm artifact, compiled once per VM from the copy the VM checked against its pin. |
artifacts.bytes(name) |
The checked bytes of a declared artifact. |
artifacts.url(name) |
A blob: URL of the checked copy, for runtimes that fetch their own files. It is revoked when the module ends. |
stdio |
The process’s stdin, stdout and stderr descriptors. |
log(line) |
Writes one line to the module’s standard error. |
fail(status, message) |
Writes a message to standard error and returns status, from 1 to 119, for use as an exit status. |
kit |
Tested helpers: Linux to Emscripten error numbers, length-prefixed framing, a session gate, running a native command with bounded output, atomic directory creation, the network helpers kit.emscripten.network, kit.tls and kit.net, and kit.wasi, which runs wasm32-wasip1 programs as the module’s Linux process. See The kit’s network helpers and host.kit.wasi. |
host.net
Section titled “host.net”With net.vm or net.internet in requires.host, the module gets host.net, and the runner installs the fetch, Request, Response, Headers and WebSocket globals. All of them run on the module process’s own VM sockets, which the engine holds to the declared reach. See Module networking.
| Member | Meaning |
|---|---|
resolve(name, { signal? }) |
Resolves a name through the VM’s resolver: /etc/hosts, and for internet reach the nameservers in /etc/resolv.conf. Resolves with the addresses. |
connect({ host, port, tls?, alpn?, signal? }) |
Opens a TCP connection, with end-to-end TLS when tls is true, verified against the VM’s /etc/ssl/cert.pem. Resolves with a connection that has a readable and a writable stream, the negotiated alpn and close(). |
resolveSync(name), connectSync(options) |
The same, blocking the module’s worker, for interpreters that cannot await. The connection has blocking read(max), write(bytes), closeWrite(), close() and alpn. |
connectUnixSync(path) |
Opens a Unix-domain socket in the VM, blocking like connectSync, held to the module’s reach as TCP is. A path that names nothing fails with ENOENT. Needs SDK 0.0.14 or later (Userspace API 147). |
tls({ serverName, alpn? }) |
A sans-IO TLS session for a program that moves the ciphertext itself: receive, takeTls, send, takePlaintext, state, alpn, close and free. |
A failure is a NetError with a Linux error code and, where the VM decided, a reason: vm-local-only (EPERM, the VM is not connected) or module-network-reach (EACCES, beyond the declared reach). A revoke resets open connections with ECONNRESET. fetch rejects with TypeError("fetch failed") whose cause is that NetError. A certificate failure is a TlsError with Node’s codes. NetError and TlsError are exported from @oxidevm/sdk/wasm.
The kit’s network helpers
Section titled “The kit’s network helpers”| Helper | Meaning |
|---|---|
kit.emscripten.network(host, Module) |
Connects an Emscripten program’s SOCKFS stream sockets and getaddrinfo to host.net. The program exports SOCKFS, DNS and FS. |
kit.tls.session, kit.tls.handshakeSync |
The engine’s sans-IO TLS client, verified against the VM’s trust store. |
kit.net.describe(error, argv0?) |
One line for a network failure, such as rates: network unavailable: this VM is not connected to the internet. |
kit.net.errno(error) |
The Linux errno a network failure carries, or the closest one. |
A command prints the describe line and returns 69, MODULE_EXIT.unavailable.
host.process.signals
Section titled “host.process.signals”With the process.signals capability, the module can trap SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2 and the real-time signals SIGRTMIN to SIGRTMAX (35 to 64), so that they reach it instead of their default action, and change its signal mask. SIGKILL and SIGSTOP can never be trapped or blocked. See Signals. Masks and real-time signals need SDK 0.0.10 or later (Userspace API 146).
| Member | Meaning |
|---|---|
trap(signals) |
Traps the listed signals, by name (SIGRTMIN+n, SIGRTMAX-n for real-time signals) or by number, and resolves with a SignalTrap. |
block(signals), unblock(signals) |
Change the signal mask, as sigprocmask does. Resolve with the previous mask, as a set of names. Unblocking a pending signal delivers it. |
pending() |
The signals that arrived while blocked. |
raise(signal) |
kill(getpid(), signal). A trapped signal reaches its trap, a blocked one stays pending, an ignored one does nothing, and any other ends this process killed by it. Its promise then never settles. |
A SignalTrap is an AsyncIterable of the trapped signals as they arrive. A standard signal that arrives again before the module has seen it is delivered once. A real-time signal arrives as { signal, value, code, pid, uid }, and every sigqueue value queues in order up to the process’s queue limit. While a signal is trapped, its arrival makes a host operation that is waiting fail with EINTR. A process starts with the signal mask it inherited.
SignalTrap member |
Meaning |
|---|---|
signals |
The trapped signals. |
interruptBuffer |
An Int32Array in the format that Pyodide’s setInterruptBuffer reads: element 0 holds the number of a trapped signal that arrived, and the reader resets it to 0. |
untrap() |
Gives the signals back their default action. |
A command’s adapter returns main(io) and may add close(). io has args, env, cwd and the same descriptors as host.stdio. main returns an exit status from 0 to 119.
When the manifest declares commands, io also has these members. Without commands they do not exist, and reading them does not type-check.
| Member | Meaning |
|---|---|
invocation |
How the module started: { kind: "host" } from the application’s spawnModule, or { kind: "command", name, path, argv0 } when a program in the VM ran one of its commands. name is the declared name, path the stub’s path, /usr/local/bin/<name>, and argv0 the caller’s argv[0], which may be a relative path or a link. args never contains argv[0]. |
terminals |
{ stdin, stdout, stderr }, each true when that descriptor is a terminal. |
A signal that the module has not trapped with process.signals keeps its default action. A signal whose default action ends a process, such as SIGINT or SIGTERM, ends it at once: close() does not run, and the parent sees the process killed by that signal.
A service’s adapter returns protocols, one handler per key of expose, and may add info() and close(). A handler receives a connection with fd, signal, read(maxBytes) (an empty array at end of stream) and write(bytes) (writes every byte or rejects; a client that has hung up rejects the write and never ends the module). It runs for the life of that connection. info() returns extra fields for the ready report, which spawnModule returns as info.
spawnModule and stopModule
Section titled “spawnModule and stopModule”spawnModule(vm: UserspaceVM, handle: ModuleHandle, options?: SpawnOptions): Promise<ModuleProcess>;stopModule(process: ModuleProcess): Promise<ProcessExit>;The handle comes from src/oxidevm.modules.generated.ts. options is derived from the manifest:
| Field | Applies to | Meaning |
|---|---|---|
args, cwd, env |
commands | Arguments, working directory (default /workspace) and environment. |
workspaceId |
services with exclusive: "workspace" |
Required. Names the workspace whose lease the service holds. The VM takes the lease before the process starts. |
options |
modules that declare options | The declared options. Those without a default are required. |
signal |
all | Cancels startup. |
A ModuleProcess is an ordinary process handle (stdin, stdout, stderr, exit, kill). For a service it also has endpoints, the declared endpoints, and info, the fields its adapter reported when it became ready.
The VM checks each pinned file once, the first time a start or an enable needs it, before any of the module’s code runs. Later starts use the checked copy and fetch nothing. A file that does not match makes spawnModule reject with wasm-module-integrity before any process exists.
enableModuleCommands
Section titled “enableModuleCommands”enableModuleCommands(vm: UserspaceVM, handle: ModuleHandle, options?: { commands?: readonly string[]; options?: Record<string, unknown>; concurrency?: number; signal?: AbortSignal;}): Promise<ModuleCommands>;Turns on the declared commands of a command module for one Userspace VM. It hands the VM the pinned files it does not hold yet, which the VM checks against their pins, and the commands hold the checked copies, with the WebAssembly artifacts compiled, for as long as they stay on. It then places a stub at /usr/local/bin/<name> for each command. When a program in the VM runs a stub, the module runs as that process, from the checked copies. Import it from @oxidevm/sdk/wasm. See Run a module’s commands from a shell.
| Field | Meaning |
|---|---|
commands |
The names to turn on, some or all of the module’s commands. Default: all of them. |
options |
The module’s declared options, as for spawnModule. Those without a default are required. They apply to every run, because a program in the VM cannot pass any. |
concurrency |
How many runs of these commands may be active at once, an integer from 1 to 32. Default 4. A run beyond it runs the stub as an ordinary program, which exits with status 126. |
signal |
Cancels the call. |
With a generated handle, commands accepts only the names the module declares and options only its declared options, and a service handle, args, cwd and env do not type-check. A System VM does not type-check either.
The ModuleCommands it resolves with has:
| Member | Meaning |
|---|---|
commands |
The names this call turned on. |
paths |
The path of each command’s stub, /usr/local/bin/<name>. |
status() |
Resolves with { enabled, skipped }: the names that are on, and { name, reason } for each name left off, with reason: "occupied" when another file holds its path. That file is left as it is. |
disable() |
Turns the commands off: removes their stubs and releases the commands’ checked copies. Calling it again, or after the VM was disposed, does nothing. |
The commands stay on until disable() or until the VM is disposed. They live in memory only: they are never saved, restore points do not capture them, and no other VM shares them. Turn them on again after every createVM, including one that resumes from a restore point. A restore in the same VM keeps them on and places their stubs again.
When the call fails, nothing is turned on, with the one exception described after the table:
| Code | detail.reason |
When |
|---|---|---|
INVALID_ARGUMENT |
wasm-module-commands |
The module is a service or declares no commands, commands is empty, repeats a name or names one the module does not declare, concurrency is not an integer from 1 to 32, or the options carry another field, such as args. Nothing has been fetched. |
INVALID_ARGUMENT |
wasm-module-options |
An option is unknown, has the wrong type or is out of range, or a required one is missing. |
INVALID_ARGUMENT |
wasm-module-command-conflict |
Another enableModuleCommands call already turned on one of these names in this VM. |
FEATURE_NOT_AVAILABLE |
wasm-module-command-unavailable |
The VM cannot run module commands: it was not created by this copy of the SDK, is a System VM, has a root initial user or no image, or the page is not cross-origin isolated. Nothing has been fetched. |
FEATURE_NOT_AVAILABLE |
wasm-module-isolation-unavailable |
This browser cannot isolate module workers. detail.capability names the check that failed. |
QUOTA_EXCEEDED |
wasm-module-command-limit |
The VM already has 16 sets of commands on, or would have more than 64 command names. |
QUOTA_EXCEEDED |
wasm-module-copy-limit |
The VM would hold more than 512 MiB of checked module files. |
GUEST_CRASH |
wasm-module-integrity |
A pinned file did not match its sha256. |
CORE_LOAD_FAILED |
wasm-module-artifact |
A pinned file could not be downloaded. |
It also fails with STORAGE_ERROR when the VM’s files cannot be changed (detail.outcome is busy, directory-unavailable or filesystem, the last with errno), and with ABORTED when signal aborts. The one exception to “nothing is turned on” is a cancellation that reaches the VM after it turned the commands on: the call turns them off again, and if that fails too, ABORTED says so in detail.cleanupFailed and the commands stay on until the VM is disposed.
Exit statuses
Section titled “Exit statuses”| Status | Meaning | Error reason |
|---|---|---|
| 0 to 119 | The adapter’s own status | none |
| 120 | The launch description was missing, too large or invalid | wasm-module-launch |
| 121 | Reserved for a file or adapter that did not match its pinned sha256. Files are checked before the process exists, so the call rejects with this reason instead. | wasm-module-integrity |
| 122 | A declared browser feature or Linux program is missing | wasm-module-prerequisite |
| 123 | The adapter threw, had the wrong shape, or returned an invalid status | wasm-module-adapter |
| 124 | Another process holds the workspace lease or an endpoint of a service | wasm-module-busy |
| 125 | The engine reports a failed worker, the module frame was lost, or the frame ended a process that sent it a malformed message | none |
| 126 | A command’s stub ran as an ordinary program and printed why on standard error: the command is not on in this VM, the file is a copy of the stub at another path, the caller does not run as uid and gid 1000 outside a container, concurrency runs are already active, or the VM’s 32 host commands are in use |
none |
| 128 + n | Signal n ended the process, for example 130 for SIGINT. A shell reports this status; the process handle’s exit is { kind: "signaled" }. |
none |
See Reasons for how each maps to a VMErrorCode.