VM
createVM resolves with a UserspaceVM (mode: "userspace") or a SystemVM (mode: "system"). Both extend VMBase, which has every member on this page except spawnJS, which only UserspaceVM has. VM is the union of the two. All types are exported from @oxidevm/sdk.
Members
Section titled “Members”| Member | Type | Meaning |
|---|---|---|
mode |
"userspace" | "system" |
The mode the VM runs in. |
image |
ImageInfo |
The image actually running. See mode and image. |
capabilities |
VMCapabilities |
Optional features this VM supports. See capabilities. |
workspace |
Workspace |
Files under /workspace. See Workspace. |
exec(spec) |
Promise<ExecResult> |
Runs a command to completion and collects its output. See exec. |
spawn(spec) |
Promise<ProcessHandle> or Promise<TerminalHandle> |
Starts a process with pipes, or on a terminal when spec.terminal is set. See spawn. |
exposePort(port, options?) |
Promise<PreviewHandle> |
Waits for a guest server on port and opens a preview route to it. See Previews. |
listeningPorts(options?) |
Promise<readonly ListeningPort[]> |
The ports guest programs listen on now. |
watchListeningPorts(listener, options) |
Promise<void> |
Reports listening ports now and after every change. |
network |
VMNetwork |
Guest internet access through a relay. See network. |
start |
VMStart |
How this VM started: cold, or resumed from a restore point. See start. |
snapshots |
VMSnapshots |
Restore points and backups. See snapshots. |
suspend(options) |
Promise<SnapshotInfo> |
Saves a restore point, then disposes the VM. See suspend. |
checkForUpdate(options?) |
Promise<ImageInfo | null> |
Checks whether the configured image differs from the running one. |
prepareImage(options?) |
Promise<void> |
Downloads and verifies missing or damaged parts of the running image. |
allowWorkspaceHandoff(listener, options) |
Promise<void> |
Lets another tab take over this workspace. See allowWorkspaceHandoff. |
dispose() |
Promise<void> |
Stops the VM and releases everything it holds. See dispose. |
spawnJS(spec) |
Promise<ProcessHandle> or Promise<TerminalHandle> |
Userspace only: runs a trusted JavaScript worker as a Linux process. See spawnJS. |
After dispose(), or once suspend() has resolved, the VM is gone: its workspace, process, preview, image and handoff methods fail with PROTOCOL_ERROR. Calling dispose() again is safe.
mode and image
Section titled “mode and image”mode is the mode you passed to createVM. image describes the image actually running:
| Field | Type | Meaning |
|---|---|---|
id |
string |
The image ID from its recipe. |
version |
string |
The image version from its recipe. |
mode |
"userspace" | "system" |
Always equal to vm.mode. |
manifestSHA256 |
SHA256 |
SHA-256 of the exact signed manifest, as 64 lowercase hex digits. |
cpuProfile |
"x86-64-v2" | "x86-64-v3" |
The x86-64 CPU profile the image declares. |
signatureVerified |
true |
The manifest signature was verified with the trusted key. |
keyId |
string |
The ID of that key. |
vm.image follows a restore that changes the running image, including a restore to an older image.
capabilities
Section titled “capabilities”| Field | Type | Meaning |
|---|---|---|
previews |
FeatureSupport |
{ available: true } when createVM received preview, otherwise { available: false, reason: "not-configured" }. |
snapshots |
FeatureSupport |
Whether restore points work in this VM. Check it before you show restore controls. |
maximumVcpus |
1 | 2 | 3 | 4 |
The most virtual CPUs this VM can use; currently 1. |
crossOriginIsolated |
boolean |
Whether the page is cross-origin isolated. |
type FeatureSupport = | { available: true } | { available: false; reason: "mode-unsupported" | "image-unsupported" | "browser-unsupported" | "release-gate-open" | "not-configured"; };exec(spec)
Section titled “exec(spec)”Runs a command, waits for it to exit and returns everything it printed.
| Field | Type | Default | Meaning |
|---|---|---|---|
command |
string |
required | An absolute path, a command name or a relative path. Not empty, no NUL. See command lookup. |
args |
readonly string[] |
[] |
At most 4,096 arguments, each passed as its own string. No shell evaluates them. |
cwd |
string |
"/workspace" |
The working directory, an absolute guest path. |
env |
Readonly<Record<string, string>> |
{} |
At most 4,096 variables, names matching [A-Za-z_][A-Za-z0-9_]*, laid over the defaults. |
stdin |
Uint8Array |
none | Bytes written to standard input before it is closed. |
maxOutputBytes |
number |
1048576 (1 MiB) |
Limit for stdout and stderr together, an integer from 1 to 67,108,864 (64 MiB). |
signal |
AbortSignal |
none | Stops the command. |
It resolves with ExecResult:
interface ExecResult { stdout: Uint8Array; stderr: Uint8Array; exit: ProcessExit;}- Standard input is closed right after
stdinis written, or at once withoutstdin. Both output streams are read concurrently. - A non-zero exit code or a signal is an ordinary result in
exit, not a rejection. - When the output passes
maxOutputBytes, the process group is killed andexecrejects withQUOTA_EXCEEDEDanddetail.maxOutputBytes. - When
signalaborts, the process group is killed, its cleanup is awaited andexecrejects withABORTED. - If the process can no longer be reached during that cleanup, the rejection carries
detail.cleanupFailed: truerather than a made-up exit.
Use spawn and stream the output for installs, builds, servers and other long or verbose commands.
import type { VM } from "@oxidevm/sdk";
export async function nodeVersion(vm: VM): Promise<string> { const result = await vm.exec({ command: "node", args: ["--version"] }); if (result.exit.kind !== "exited" || result.exit.code !== 0) { throw new Error(new TextDecoder().decode(result.stderr)); } return new TextDecoder().decode(result.stdout).trim();}Environment
Section titled “Environment”Every process starts with PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin and HOME=/workspace. A value in env replaces the default, including an explicit empty string.
Command lookup
Section titled “Command lookup”An absolute command runs that executable directly. A command name or a relative path is resolved by the image’s own /bin/sh with the PATH in effect, so these forms need /bin/sh in the image; the arguments still pass through unchanged. In Userspace, a name that is not found exits with code 127. To run shell syntax, start the shell yourself: { command: "/bin/sh", args: ["-c", "ls -la | head"] }.
spawn(spec)
Section titled “spawn(spec)”spawn(spec: TerminalSpawnSpec): Promise<TerminalHandle>;spawn(spec: PipeSpawnSpec): Promise<ProcessHandle>;PipeSpawnSpec takes command, args, cwd and env as for exec, plus signal. TerminalSpawnSpec adds terminal: { columns: number; rows: number }, integers from 1 to 1,000, and runs the process on a pseudo-terminal.
spawnresolves once the process runs; it does not wait for the exit.signalcancels creation only. If it aborts while the process starts, the process group is killed andspawnrejects withABORTED. Afterspawnresolved, stop the process withkill.- Read the output (both
stdoutandstderrof a pipe process) and closestdinwhen there is no more input. A process whose output nobody reads can stall.
import type { ProcessExit, VM } from "@oxidevm/sdk";
export async function runStreaming( vm: VM, command: string, args: readonly string[], log: (text: string) => void,): Promise<ProcessExit> { const child = await vm.spawn({ command, args }); const sink = () => { const decoder = new TextDecoder(); return new WritableStream<Uint8Array>({ write: (bytes) => log(decoder.decode(bytes, { stream: true })) }); }; const [exit] = await Promise.all([ child.exit, child.stdout.pipeTo(sink()), child.stderr.pipeTo(sink()), child.stdin.close(), ]); return exit;}ProcessHandle
Section titled “ProcessHandle”| Member | Type | Meaning |
|---|---|---|
kind |
"process" |
Discriminant. |
pid |
ProcessId |
The guest process ID, a positive integer. |
stdin |
WritableStream<Uint8Array> |
Standard input. |
stdout |
ReadableStream<Uint8Array> |
Standard output. |
stderr |
ReadableStream<Uint8Array> |
Standard error. |
exit |
Promise<ProcessExit> |
Resolves with the actual exit. |
kill(signal?, options?) |
Promise<void> |
Sends a Linux signal. See kill. |
snapshotKey |
string | undefined |
Equals the key of the matching ResumedTerminal after a restore. Keep it in app data to restore tab names and order. |
TerminalHandle
Section titled “TerminalHandle”| Member | Type | Meaning |
|---|---|---|
kind |
"terminal" |
Discriminant. |
pid, stdin, exit, kill, snapshotKey |
see ProcessHandle |
The same members as on a pipe process. |
output |
ReadableStream<Uint8Array> |
Everything the terminal prints, standard output and standard error together. |
resize(size) |
Promise<void> |
Sets { columns, rows }, integers from 1 to 1,000. |
interrupt() |
Promise<void> |
Sends Ctrl+C to the terminal’s foreground process group. The shell stays usable when the guest program allows it. |
A terminal handle has no stdout or stderr. Write key presses to stdin as bytes, one write at a time. After the process exited, resize and interrupt fail with PROTOCOL_ERROR; an image whose guest integration lacks them fails with FEATURE_NOT_AVAILABLE and detail.reason: "image-unsupported". Closing your terminal view does not stop the shell; kill it when the tab closes. See Terminals.
kill(signal?: LinuxSignal, options?: { scope?: "process" | "process-group" }): Promise<void>;- The default signal is
"SIGTERM"and the default scope is"process"."process-group"signals the process’s whole process group. - A signal sent after the exit was confirmed does nothing and can never reach another process that reused the PID.
- An unknown signal name or scope fails with
INVALID_ARGUMENT.
type LinuxSignal = | "SIGHUP" | "SIGINT" | "SIGQUIT" | "SIGILL" | "SIGTRAP" | "SIGABRT" | "SIGBUS" | "SIGFPE" | "SIGKILL" | "SIGUSR1" | "SIGSEGV" | "SIGUSR2" | "SIGPIPE" | "SIGALRM" | "SIGTERM" | "SIGSTKFLT" | "SIGCHLD" | "SIGCONT" | "SIGSTOP" | "SIGTSTP" | "SIGTTIN" | "SIGTTOU" | "SIGURG" | "SIGXCPU" | "SIGXFSZ" | "SIGVTALRM" | "SIGPROF" | "SIGWINCH" | "SIGIO" | "SIGPWR" | "SIGSYS";ProcessExit
Section titled “ProcessExit”type ProcessExit = | { kind: "exited"; code: number } | { kind: "signaled"; signal: LinuxSignal | null; signalNumber: number };signal is null for a signal without a name in LinuxSignal, such as a real-time signal; signalNumber always holds the exact number.
Previews
Section titled “Previews”exposePort(port, options?)
Section titled “exposePort(port, options?)”exposePort(port: number, options?: { signal?: AbortSignal }): Promise<PreviewHandle>;portis an integer from 1 to 65535; anything else fails withINVALID_ARGUMENT.- It needs
previewin thecreateVMoptions, otherwise it fails withFEATURE_NOT_AVAILABLEanddetail.reason: "not-configured". - It waits until a guest program listens on
portwhere a preview can reach it, then opens the route. It does not start a server. - Calls for the same port share one route until the last of their handles is closed.
- Aborting
signalstops the wait withABORTED.
PreviewHandle member |
Type | Meaning |
|---|---|---|
port |
number |
The guest port. |
url |
URL |
The preview address. Load it in an iframe or open it in a tab. |
isolation |
"per-session" | "shared" |
The preview.isolation of the VM. |
close() |
Promise<void> |
Closes the route. The guest server keeps running. |
dispose() closes every route the VM opened. See Previews.
listeningPorts(options?)
Section titled “listeningPorts(options?)”Resolves with at most 256 ListeningPort entries, lowest port first. options.signal cancels the read.
ListeningPort field |
Type | Meaning |
|---|---|---|
port |
number |
1 to 65535. |
address |
string |
The address as bound, for example "0.0.0.0", "::", "127.0.0.1" or "::1". |
program |
string | null |
The command that started the listener, such as the command typed in a terminal, at most 256 bytes of UTF-8; null when unknown. |
previewable |
boolean |
Whether a preview can reach this listener. |
watchListeningPorts(listener, options)
Section titled “watchListeningPorts(listener, options)”Calls listener with the current list, then again each time the list changes. The returned promise resolves when options.signal aborts and rejects when reading the ports fails.
import type { VM } from "@oxidevm/sdk";
export function watchPorts(vm: VM, signal: AbortSignal): Promise<void> { return vm.watchListeningPorts((ports) => { console.log(ports.filter((entry) => entry.previewable).map((entry) => entry.port)); }, { signal });}network
Section titled “network”vm.network is a VMNetwork, the same object for the VM’s whole lifetime.
| Member | Type | Meaning |
|---|---|---|
state |
NetworkState |
The current relay state. |
connect(connection, options?) |
Promise<void> |
Connects through a relay with RelayConnectionOptions; options.signal cancels. Resolves once the relay session and the transport are ready. On failure, internet access stays off. |
disconnect() |
Promise<void> |
Revokes internet access. Safe to call repeatedly. Files, processes, terminals and previews keep working. |
watch(onChange, options) |
Promise<void> |
Reports the current state at once, then every change. Resolves when options.signal aborts or the VM is disposed; rejects if onChange throws. |
connect fails with RELAY_ERROR, PERMISSION_DENIED, QUOTA_EXCEEDED or PROTOCOL_ERROR, and detail.reason names the relay failure. Invalid connection options fail with INVALID_ARGUMENT.
NetworkState kind |
Other fields | Meaning |
|---|---|---|
"off" |
none | No internet access. |
"discovering" |
none | Looking for the user’s personal relay. |
"awaiting-approval" |
code, origin, workspaceId, workspaceName, expiresAt |
Waiting for the user to approve the six-digit code in their personal relay. |
"connecting" |
relay: "personal" | "server" |
Connecting. |
"connected" |
relay, expiresAt |
Internet access is on. |
"failed" |
reason: RelayFailureReason |
The last connection attempt failed. |
Restoring a restore point, suspend and dispose revoke relay access, and restore points and backups never keep it. See Networking.
type VMStart = | { kind: "booted"; reason: ManagedBootReason; skipped?: SnapshotInfo; shipped?: { reason: ShippedSkipReason }; update?: { id: string; version: string }; workspaceDetails?: UserspaceWorkspaceStart; } | { kind: "resumed"; snapshot: SnapshotInfo; appData: Uint8Array | null; terminals: readonly ResumedTerminal[]; newerFilesDropped: boolean; workspace: "in-sync" | "reconciled" | "conflict-preserved"; skipped?: SnapshotInfo; update?: { id: string; version: string }; workspaceDetails?: UserspaceWorkspaceStart; };| Field | Meaning |
|---|---|
kind |
"booted": Linux started cold. "resumed": it continued from a restore point. |
reason |
Why the start was cold. See the table below. |
skipped |
A restore point that was tried and could not resume. |
shipped |
Why the requested shippedSnapshot prepared start did not open. |
update |
The configured image, when the workspace keeps running another image. Start with restorePoint: "update-image" to move to it. |
snapshot |
The restore point that resumed. |
appData |
The bytes stored with snapshots.setAppData for that save, or null. |
terminals |
Processes the restore point brought back. See ResumedTerminal below. |
newerFilesDropped |
The workspace held files newer than the restore point. restorePoint: "pre-resume" keeps them. |
workspace |
How /workspace relates to file changes made since the save. |
workspaceDetails |
Userspace: how the saved /workspace met this image. |
ManagedBootReason |
Meaning |
|---|---|
"first-start" |
No restore point exists yet. |
"restart" |
A cold start was requested: restorePoint: "none" or "pre-resume". |
"fresh-image" |
restorePoint: "fresh-image" was requested. |
"update-image" |
restorePoint: "update-image" was requested. |
"no-save" |
Restore points exist, but none matches where the VM left off: it last booted cold. |
"incompatible" |
The restore point is from another format, image or machine shape, or its image is gone. |
"damaged" |
Every candidate restore point was damaged. |
"no-space" |
The browser had no room to resume. |
"restore-failed" |
Restoring failed for another reason. |
"agent-unavailable" |
The restored guest agent did not answer. |
ShippedSkipReason |
Meaning |
|---|---|
"untrusted" |
No trusted key was given, or the manifest is signed by another key. |
"signature-invalid" |
The signature does not verify. |
"newer", "older" |
It needs a newer SDK, or comes from an older one. |
"incompatible" |
It belongs to another image or does not fit this VM. |
"damaged" |
Its data is damaged. |
"unavailable" |
It could not be downloaded. |
"no-space" |
The browser had no room for it. |
"restore-failed" |
Restoring it failed. |
"agent-unavailable" |
Its guest agent did not answer. |
ResumedTerminal
Section titled “ResumedTerminal”type ResumedTerminal = | { source: "console"; key: "console"; screenTail: Uint8Array } | { source: "process"; key: string; command: string; screenTail: Uint8Array; attach(options?: { signal?: AbortSignal }): Promise<ProcessHandle | TerminalHandle>; };screenTail holds the newest output saved with the process, at most 64 KiB. attach takes over a restored process: it replays the saved screen once, then streams what the process prints from then on. Handles from before a restore are retired; attach again.
UserspaceWorkspaceStart
Section titled “UserspaceWorkspaceStart”| Field | Type | Meaning |
|---|---|---|
state |
"in-sync" | "reconciled" | "conflict-preserved" |
in-sync: nothing was saved, or it was saved on this image. reconciled: it was saved on another image and carried over cleanly. conflict-preserved: see conflicts and preserved. |
conflicts |
readonly { path: string; reason: string }[] |
At most 256 /workspace paths that could not be carried over cleanly. reason is "changed-in-both" (the user’s version stayed), "base-unknown" (the earlier image is unknown here; the user’s version stayed) or "content-unavailable" (the user’s version named a file this browser no longer holds; the image’s version stayed). |
omittedConflicts |
number |
Conflicts beyond the first 256. |
preserved |
{ id: string; reason: string } or absent |
The earlier save was kept byte for byte under preserved/<id>/ in the workspace’s overlay store. reason is "unreadable", "unreplayable", "base-unavailable" or "content-unavailable". |
A damaged save never stops the VM from starting.
snapshots
Section titled “snapshots”| Member | Returns | Meaning |
|---|---|---|
list(options?) |
Promise<readonly SnapshotInfo[]> |
The workspace’s restore points. |
capture(options) |
Promise<SnapshotInfo> |
Saves a restore point. options: label (not blank, at most 256 UTF-8 bytes), kind? ("named" by default, "safety" or "automatic"), signal?. |
restore(ref, options?) |
Promise<SnapshotRestoreResult> |
Returns to a restore point. By default it saves the current state as a safety point first (saveCurrentFirst: true). Resolves with { start, undo? }, where undo is that safety point. options also take onProgress and signal. |
rename(ref, label, options?) |
Promise<SnapshotInfo> |
Renames a restore point. |
delete(ref, options?) |
Promise<void> |
Deletes a restore point. |
usage(options?) |
Promise<SnapshotUsage> |
{ referencedBytes, reclaimableBytes, estimate, persisted }, with the browser’s storage estimate ({ usage, quota } or null). |
setAppData(data) |
Promise<void> |
Stores up to 1 MiB of your own bytes (or null) with later restore points. They return as start.appData. |
observe(listener, options) |
void |
Delivers SnapshotEvents, such as save-started, save-finished, save-failed, auto-save-paused and restore-progress, until options.signal aborts. |
autoSave.configure(policy, options?) |
Promise<void> |
{ enabled: boolean }. Automatic saves start disabled. |
autoSave.hint(hint) |
void |
Tells automatic saving about the page: "hidden", "visible", "activity" or "files-changed". |
export(ref, options?) |
Promise<BackupExportResult> |
Writes a backup to options.destination, such as a file handle from showSaveFilePicker(), and resolves with { kind: "written", bytes }; without a destination it resolves with { kind: "file", file, suggestedName }. options also take onProgress and signal. |
inspect(file, options?) |
Promise<BackupInspection> |
Reads a backup Blob without importing it. |
import(file, options?) |
Promise<SnapshotInfo> |
Adds a backup Blob as a restore point. |
ref is a SnapshotRef, { id: string; createdAt: string }. A restore point from the other mode fails with FEATURE_NOT_AVAILABLE and detail.reason: "runtime-mismatch". While a JavaScript process from spawnJS or acceleration runs, capture fails with FEATURE_NOT_AVAILABLE and detail.reason: "js-process-active".
SnapshotInfo field |
Type | Meaning |
|---|---|---|
mode |
"system" | "userspace" |
The mode it belongs to. |
ref |
SnapshotRef |
Its identity. |
kind |
"automatic" | "named" | "safety" | "imported" | "shipped" |
An automatic save, a named point, a safety point taken before a restore, reset or update, an imported backup, or a prepared start. |
label |
string | undefined |
Its name. |
capturedAtUnixMs |
number |
When it was captured. |
profile |
{ id: string; version: string } | null |
The managed image it runs, or null. |
bytes |
{ ram: number; disk: number; unique: number } |
Its memory, its changed disk content, and the bytes only it keeps (about what deleting it frees). |
resumable |
true or a reason |
true, or why it cannot resume here: "older-format", "newer-format", "profile-mismatch", "machine-mismatch", "runtime-mismatch", "base-missing" or "corrupt". |
See Restore points.
suspend(options)
Section titled “suspend(options)”suspend(options: { label: string; signal?: AbortSignal }): Promise<SnapshotInfo>;Captures a consistent restore point, releases the VM’s resources and its workspace lease, and disposes the VM before it resolves. Open the workspace again with restorePoint: "latest" to continue. If suspending fails, the VM stays usable whenever it can be kept. Like capture, it fails with js-process-active while a JavaScript process runs.
checkForUpdate and prepareImage
Section titled “checkForUpdate and prepareImage”| Member | Behavior |
|---|---|
checkForUpdate(options?) |
Verifies the image configured for this VM (the image option, or the default image). Resolves with its ImageInfo when it differs from the running image, otherwise null. It never installs anything; start with restorePoint: "update-image" to move to the offered image. vm.start.update keeps the offer known at startup. |
prepareImage(options?) |
Downloads and verifies whatever is missing or damaged in the running image, without restarting the guest or changing the image version. Processes and terminals keep running. Verified progress is kept when options.signal aborts. |
allowWorkspaceHandoff
Section titled “allowWorkspaceHandoff”allowWorkspaceHandoff( listener: (moved: WorkspaceMovedAway) => void, options: { signal: AbortSignal },): Promise<void>;Opts in to handing the workspace to another tab. When another tab calls requestWorkspaceHandoff for this VM’s workspace ID, this VM saves its state when it can, is disposed, and then calls listener with { saved: boolean }. The returned promise resolves when options.signal aborts or the VM is disposed; aborting stops accepting requests.
The requesting tab calls requestWorkspaceHandoff from the package root before it creates its own VM:
import { createVM, requestWorkspaceHandoff } from "@oxidevm/sdk";
const outcome = await requestWorkspaceHandoff("my-project", { timeoutMs: 10_000 });if (outcome === "no-response") throw new Error("Another tab still holds this workspace");const vm = await createVM({ mode: "system", workspaceId: "my-project", restorePoint: "latest" });requestWorkspaceHandoff option |
Type | Meaning |
|---|---|---|
timeoutMs |
number |
How long to wait for the holding tab to answer, from 1 to 600,000 ms; default 10,000. |
signal |
AbortSignal |
Cancels the request. |
It resolves with "released" once the workspace is free, at once when no VM holds it, or with "no-response" when no tab answered in time. If the holder’s save fails, the request rejects with the holder’s error and the holder keeps running.
dispose()
Section titled “dispose()”Stops the VM’s processes, closes its preview routes, revokes relay access, saves a durable workspace’s files, releases the workspace lease and frees the VM’s workers and assets. Calling it again returns the same promise. Always await it, and await it before you create another VM with the same workspaceId.
spawnJS(spec)
Section titled “spawnJS(spec)”Userspace only. Runs a trusted JavaScript module worker as a Linux process: it gets a PID, file descriptors, credentials and signals like any other process, and reaches files, sockets and child processes through the Linux side.
spawnJS(spec: TerminalJSProcessSpec): Promise<TerminalHandle>;spawnJS(spec: PipeJSProcessSpec): Promise<ProcessHandle>;| Field | Type | Default | Meaning |
|---|---|---|---|
worker |
URL |
required | The worker module. It must be on the page’s origin, and the page must be cross-origin isolated; otherwise FEATURE_NOT_AVAILABLE. |
args |
readonly string[] |
[] |
Arguments, available to the worker. |
cwd |
string |
"/workspace" |
Working directory. |
env |
Readonly<Record<string, string>> |
{} |
Environment, handled as for spawn. |
terminal |
{ columns: number; rows: number } |
none | Runs it on a pseudo-terminal and returns a TerminalHandle. |
signal |
AbortSignal |
none | Cancels creation only, as for spawn. |
The VM must use the default initialUser: "user"; other credential profiles fail. The worker calls serveProcess(handler) from @oxidevm/sdk/js-process. The handler returns an exit code from 0 to 255, or nothing for 0. A failure to construct or run the worker, or of its transport, exits with code 125, and the process is never retried as a Linux program. At most four JavaScript processes run at once. While one runs, snapshots.capture and suspend fail with js-process-active.
import type { UserspaceVM } from "@oxidevm/sdk";
export async function runWorker(vm: UserspaceVM, worker: URL): Promise<string> { const child = await vm.spawnJS({ worker, args: ["hello"] }); const [stdout, stderr, exit] = await Promise.all([ new Response(child.stdout).text(), new Response(child.stderr).text(), child.exit, child.stdin.close(), ]); if (exit.kind !== "exited" || exit.code !== 0) throw new Error(`worker failed: ${stderr}`); return stdout;}