Reference · reference

wscript API: Term, types and the vmlab module

10 min read · 2026-09-02 · vmlab 0.9

This chapter covers the rest of the wscript surface: Term, the send/expect handle Machine.terminal returns; the six record types methods return or handlers receive; and the two functions on the vmlab module itself. The scripting guide explains where scripts run. The handles are in wscript API: Lab and Segment and wscript API: Machine.

The record types are plain structs: a script reads their fields directly, as r.exit_code or login.password. They are never constructed by a script. The signatures use the parameter names the host registers; the generated interface file spells them a0, a1 and so on.

Term.send

Send raw text to the shell.

wscript
fn send(self, text: string) -> Result[unit, string]
ParameterTypeMeaning
textstringThe bytes to write to the PTY, exactly as given. No newline is added.

Use this for control characters and partial input; the example below sends Ctrl-C as the byte 0x03. It fails with "<machine>: terminal session is closed" after Term.close or after the shell has exited, and with the agent's error if the write fails.

wscript
fn main(lab: Lab) {
    let Ok(vm) = lab.vm("box") else { return }
    let Ok(t) = vm.terminal() else { return }
    let s = t.send("\u{3}")
    t.close()
}

Term.send_line

Send a line of input followed by Enter.

wscript
fn send_line(self, text: string) -> Result[unit, string]
ParameterTypeMeaning
textstringThe line to type.

Writes text and a carriage return, which is what a PTY expects from Enter and works for POSIX shells and PowerShell alike. The errors are those of Term.send.

wscript
fn main(lab: Lab) {
    let Ok(vm) = lab.vm("box") else { return }
    let Ok(t) = vm.terminal() else { return }
    let s = t.send_line("systemctl restart app")
    t.close()
}

Term.read

Take whatever output the shell has produced so far.

wscript
fn read(self) -> string

Drains output already queued, waiting at most about 50 milliseconds for more, and returns it, clearing the buffer. It does not wait for a prompt; use Term.expect for that. It never fails: a closed session returns what was left in the buffer, or an empty string.

wscript
fn main(lab: Lab) {
    let Ok(vm) = lab.vm("box") else { return }
    let Ok(t) = vm.terminal() else { return }
    vmlab::sleep_ms(500)
    lab.log(t.read())
    t.close()
}

Term.expect

Wait until the output matches a regular expression and return the text up to the end of the match.

wscript
fn expect(self, pattern: string, timeout_secs: int) -> Result[string, string]
ParameterTypeMeaning
patternstringA regular expression, matched anywhere in the accumulated output.
timeout_secsintHow long to wait, in seconds.

Output accumulates in a buffer. On a match the text through the end of the match is consumed and returned, and what follows stays for the next call, so successive expects walk the stream. The buffer holds the raw PTY stream, prompts and escape sequences included, so match on something stable. It fails with "bad pattern: <reason>" for an invalid expression, with "<machine>: timed out after <n>s waiting for /<pattern>/; tail: <text>" at the deadline, and with "<machine>: terminal ended (<why>) before /<pattern>/ matched; tail: <text>" when the shell exits or the agent channel closes first. The tail is the last few hundred bytes of unmatched output, for debugging.

wscript
fn main(lab: Lab) {
    let Ok(vm) = lab.vm("box") else { return }
    let Ok(t) = vm.terminal() else { return }
    let s = t.send_line("hostname")
    match t.expect("box", 10) {
        Ok(out) => lab.log("saw: " + out),
        Err(e) => lab.log(e),
    }
    t.close()
}

Term.resize

Resize the session's PTY.

wscript
fn resize(self, cols: int, rows: int) -> Result[unit, string]
ParameterTypeMeaning
colsintThe new width in columns. Clamped to at least 2.
rowsintThe new height in rows. Clamped to at least 2.

A session opens at 120 by 32, which is wide enough that prompts rarely wrap mid-pattern. It fails on a closed session and with the agent's error otherwise.

wscript
fn main(lab: Lab) {
    let Ok(vm) = lab.vm("box") else { return }
    let Ok(t) = vm.terminal() else { return }
    let rz = t.resize(200, 50)
    t.close()
}

Term.close

End the session and kill the shell.

wscript
fn close(self)

Closing is deterministic; a handle that goes out of scope without it is closed when the script's VM collects it. Closing twice is harmless. It never fails. After it, Term.send, Term.send_line and Term.resize fail with "terminal session is closed" and Term.expect reports the session ended with reason "closed".

wscript
fn main(lab: Lab) {
    let Ok(vm) = lab.vm("box") else { return }
    let Ok(t) = vm.terminal() else { return }
    t.close()
}

Match

Where an image or text was found on the display, returned by the screen-matching methods.

wscript
struct Match {
    x: int,
    y: int,
    w: int,
    h: int,
    score: float,
    cx: int,
    cy: int,
    text: string,
}
FieldTypeDefaultMeaning
xint0Left edge of the match, in framebuffer pixels.
yint0Top edge of the match.
wint0Width of the matched region; the reference image's width.
hint0Height of the matched region.
scorefloat1.0The similarity score, 0 to 1.
cxint0Horizontal centre, for Machine.mouse_move.
cyint0Vertical centre.
textstringemptyThe matched text, set only by Machine.wait_for_text.

From wait_for_image, wait_for_image_opts, wait_for_any and find_image the position fields describe the match and text is empty. From wait_for_text only text is meaningful: the position fields are 0 and the score is 1, because OCR does not report a location. The defaults column shows those filler values.

ExecResult

What a guest command produced, returned by Machine.exec and Machine.exec_timeout.

wscript
struct ExecResult {
    exit_code: int,
    stdout: string,
    stderr: string,
}
FieldTypeDefaultMeaning
exit_codeintnoneThe process's exit code. Non-zero is not an error to the call.
stdoutstringnoneCaptured standard output, decoded as UTF-8 with invalid bytes replaced.
stderrstringnoneCaptured standard error, decoded the same way.

Every field is always set. Branch on exit_code for the command's own verdict; the call's Result reports only whether the command could be run at all.

Login

One identity the lab file declares for a machine, returned by Machine.logins.

wscript
struct Login {
    label: string,
    user: string,
    password: Option[string],
    elevated: bool,
    default: bool,
}
FieldTypeDefaultMeaning
labelstringnoneThe name --user and as_login select this identity by.
userstringnoneThe guest account, for example PROBE\dev.
passwordOption[string]NoneThe declared secret exactly as written, or None where the author declared none. Never an empty string.
elevatedbooltrue on Windows, false on LinuxWhether sessions as this login run elevated. Resolved: an undeclared value takes the guest family's default. Elevation is Windows-only, so a Linux login that never declared it reports false.
defaultboolresolvedWhether this is the machine's default identity, with the lone-login rule applied: a machine's only login is the default without saying so.

password is None rather than empty so that a script never passes an empty secret to an account-creation command by accident. elevated and default cross resolved rather than as written, so a script asking "is this the default" gets the answer vmlab acts on. See Logins and the login {} block in vm and its children.

GuestStats

One sample of guest metrics, returned by Machine.stats.

wscript
struct GuestStats {
    cpu_pct: float,
    mem_used: int,
    mem_total: int,
    disks: List[DiskStat],
}
FieldTypeDefaultMeaning
cpu_pctfloatnoneCPU use across the guest, in percent.
mem_usedintnoneMemory in use, in bytes.
mem_totalintnoneMemory the guest sees, in bytes.
disksList[DiskStat]emptyOne entry per mounted filesystem.

The sample comes from the agent's metrics feature; `vmlab machine capabilities` says whether a machine's agent offers it.

DiskStat

One mounted filesystem inside GuestStats.

wscript
struct DiskStat {
    mount: string,
    used: int,
    total: int,
}
FieldTypeDefaultMeaning
mountstringnoneThe mount point, such as / or C:\.
usedintnoneBytes in use.
totalintnoneThe filesystem's size in bytes.

Event

The payload an event handler receives as its first argument.

wscript
struct Event {
    name: string,
    vm: string,
    data: string,
}

fn handle(event: Event, lab: Lab) { }
FieldTypeDefaultMeaning
namestringnoneThe event name, such as vm.crashed. See Events.
vmstringemptyThe machine the event concerns: the payload's vm key, else its container key, else empty.
datastring{}The whole payload as JSON text.

A handler is bound with an on {} block in the lab file, see Events and handlers, and runs `fn handle(event: Event, lab: Lab)` on a lab handle with no owning machine. Handler failures are logged and never fatal. vm is filled from the vm or container key only, so an event that names its subject as machine alone leaves it empty; the daemons set both keys on such events for that reason. data carries every other field for a handler to parse.

wscript
fn handle(event: Event, lab: Lab) {
    lab.log("event " + event.name + " on " + event.vm)
    if event.name == "vm.crashed" {
        let Ok(m) = lab.machine(event.vm) else { return }
        let shot = m.screenshot("")
    }
}

vmlab::sleep_ms

Pause the script.

wscript
fn sleep_ms(ms: int)
ParameterTypeMeaning
msintHow long to sleep, in milliseconds. A negative value returns at once.

Blocks the script's thread; the lab daemon keeps running. Prefer the waiting methods, Machine.wait_ready, Machine.wait_for_image, Term.expect, where one fits, and use this for polling loops around Machine.exec.

wscript
fn main(lab: Lab) {
    let vm = lab.this_vm().expect("no target vm")
    for i in 0..10 {
        match vm.exec("cmd.exe", ["/c", "if exist C:\\done (exit 0) else (exit 1)"]) {
            Ok(r) => { if r.exit_code == 0 { return } }
            Err(e) => lab.log("waiting: " + e),
        }
        vmlab::sleep_ms(1000)
    }
}

vmlab::templeos_agent_script

The vmlab agent for TempleOS, as text to type at its shell.

wscript
fn templeos_agent_script() -> Result[string, string]

TempleOS reads no ISO 9660 and has no network, so the bootstrap ISO cannot carry the agent in and the screen is the only way. This returns the HolyC source as A("…") statements that accumulate in a buffer, then the FileWrite to ~/VmlabAgt.HC, the #include, and VmlabAgentInstall — which appends the include and the spawn to ~/MakeHome.HC.Z, so the agent starts at every boot, and starts it now so a build verifies the handshake without a reboot. It fails when the agent asset is missing, naming the searched paths.

Roughly twelve thousand keystrokes, about eight minutes at 40 ms, once per template build. The machine must use the QMP input transport, the default: over VNC, TempleOS sees each shift a keystroke late and shifted characters land on the wrong key.

wscript
fn main(lab: Lab) {
    let Ok(vm) = lab.vm("temple") else { return }
    let Ok(script) = vmlab::templeos_agent_script() else { return }
    vm.type_text_paced(script, 40).expect("typing the agent")
}

vmlab::env

Read an environment variable of the lab daemon's process.

wscript
fn env(name: string) -> string
ParameterTypeMeaning
namestringThe variable's name.

Returns the value, or an empty string when it is unset, so a script cannot tell an unset variable from an empty one. The environment is the daemon's, which inherits from the vmlab invocation that started it, not necessarily from the shell running vmlab up now. It exists so a build or provision script can carry an operator toggle without a schema change, such as a VMLAB_SKIP_UPDATES=1 that lets a template build skip its update pass.

wscript
fn main(lab: Lab) {
    if vmlab::env("VMLAB_SKIP_UPDATES") == "1" {
        lab.log("skipping updates")
        return
    }
}