Running commands inside the guest
Provium gives you several ways to run commands inside a guest. This page is the practical guide; the canonical method reference is on VM.
Sync exec: vm:run #
The default. Returns a RunResult with exit_code, stdout, stderr, status, signal, timed_out.
local r = vm:
r:
t:
Two call shapes:
Shell form: vm:run(string) #
vm:
vm:
The string is run through /bin/sh -c "<string>" so shell metacharacters work. Convenient for one-liners; quote-handling is the shell's problem.
Direct exec: vm:run(cmd, {args, ...}) or vm:run(cmd, {arr}) #
vm: -- canonical
vm: -- legacy bare-array form
No shell. The first arg is the executable; positional args go in args (or as a bare array when no opts keys are present). Use this when:
- The args contain shell metacharacters you don't want interpreted.
- You don't want a shell process in your tree (PID, signal handling, etc.).
- You're passing user-controlled data that you'd otherwise have to quote.
The detection between the two table forms is by recognised keys: the presence of any of env, env_clear, cwd, stdin, timeout, timeout_ms, or args selects the opts form; a table without any of them is treated as the legacy direct-args array.
Environment, cwd, stdin #
vm:
vm:
vm:
Combine freely:
local r = vm:
t:
t:
env_clear = true makes the guest see ONLY the keys you supplied:
vm:
Without env_clear, your env merges with the agent's environment.
Timeouts #
Two equivalent keys: timeout_ms (int, milliseconds) and timeout (number seconds, or string with suffix).
vm: -- TimedOut
vm: -- Same
vm: -- Same
vm: -- 5s
vm: -- 1 minute
When the timeout fires, the agent kills the process and returns a RunResult with status = "timed_out", timed_out = true, and exit_code = -2. Use r.timed_out to disambiguate from a clean exit with code -2.
local r = vm:
if r. not r:
RunResult fields and helpers #
Most tests only need three things from a RunResult:
local r = vm:
r: -- raise if not ok; message includes status, stdout, stderr
r. -- captured stdout (bytes)
r: -- shorthand for exit_code == 0
When a command can fail in more than one way, check r.status ("exited" / "signalled" / "timed_out") rather than pattern-matching exit_code — the sentinel exit codes and the signal field are documented in the RunResult reference.
Async exec: vm:run_async #
Returns a Process userdata immediately. The agent does NOT auto-kill; you control the lifetime.
local proc = vm:
-- … do other things …
proc:
local r = proc:
The opts shape mirrors vm:run's, except that passing timeout / timeout_ms is rejected — use proc:wait(timeout) instead. (Silently honouring timeout here would be a footgun: the agent doesn't auto-kill, so the timeout would do nothing.)
Stdin pipe #
local proc = vm:
proc:
proc:
proc:
local r = proc:
t:
Streaming stdout / stderr #
local proc = vm:
local out = proc:
out:
-- now exercise the server
See streams and tails for the full stream API.
Signals #
proc: -- defaults to SIGTERM
proc: -- SIGKILL by number
proc: -- by name
proc: -- SIGUSR1
proc: -- alias for kill(); reads better for non-fatal signals
Signals are accepted by friendly name (term, kill, usr1, …), with a sig prefix (sigterm), or as a bare integer. The full recognised-name list is in the Process reference.
Inspecting the process #
proc: -- live kernel PID inside the guest
proc: -- opaque agent-side handle id
proc: -- non-blocking poll
pid() calls into the agent every time. handle() is a stable in-memory id that never changes.
Waiting #
proc: -- wait forever
proc: -- wait at most 5 seconds
proc: -- 500 ms
Don't pass 0 — the harness rejects it and points you at proc:status() for non-blocking polling. The Process reference explains why a literal-zero timeout is a footgun.
Workers: parallel commands in the same VM #
vm:spawn_worker() returns a Worker — a sub-agent connection. Useful for driving the same VM from multiple threads of control:
local w1 = vm:
local w2 = vm:
-- Two writers in parallel.
local p1 = w1:
local p2 = w2:
p1::
p2::
Workers expose the same surface as the VM (run, run_async, open_file, syscall, kill, join, close). Files and processes allocated under a worker live in the worker's namespace; cleanup is per-worker.
For coordination between workers' guest processes, use a guest-side primitive (file on a shared mount, fifo, network message). lab:barrier(name, count, timeout?) is a host-side rendezvous and can't be reached from inside a guest — see Labs and scope — Barriers.
Common patterns #
"Did the fixture build correctly?" #
test
"Run a server, hit it, tear down" #
test
"Compose multiple ops in one round trip" #
local results = vm:
-- results[3].ok == "1"
-- results[4].err contains "No such file"
vm:batch is one wire round-trip for N ops. Useful when latency dominates (many small ops back-to-back) or when you want to inspect the ordered outcome.
See also #
- VM reference — every method.
- Process reference — async-process surface.
- Worker reference — concurrent agent connections.
- Streams and tails —
proc:stdout_stream/proc:stderr_stream.