Skip to content

Console & Command Execution

Interact with VMs programmatically through the serial console using pattern matching and command execution.

Overview

The VmConsole provides a high-level interface for VM automation:

  • Pattern matching: Wait for specific output before proceeding
  • Command execution: Run commands and capture output reliably
  • Control signals: Send Ctrl+C, Ctrl+D, and other signals
  • Concurrent access: Split into reader/writer for parallel operations

Enabling the Console

The console must be explicitly enabled when building the VM:

rust
use capsa::{Capsa, LinuxDirectBootConfig};

let vm = Capsa::vm(LinuxDirectBootConfig::new("kernel", "initrd"))
    .console_enabled()  // Required for console access
    .build()
    .await?;

Then obtain the console handle:

rust
let console = vm.console().await?;

Console Not Enabled

Calling vm.console() without .console_enabled() returns Error::ConsoleNotEnabled. This is a common source of confusion when first using the API.

Waiting for Boot

Before executing commands, wait for the VM to reach a usable state.

Basic Pattern Matching

Use wait_for() to block until a pattern appears in the console output:

rust
// Wait indefinitely for the pattern
console.wait_for("login:").await?;

With Timeout

Always prefer wait_for_timeout() in production code to avoid hanging indefinitely:

rust
use std::time::Duration;

console.wait_for_timeout("Boot successful", Duration::from_secs(30)).await?;

If the pattern is not found within the timeout, returns Error::Timeout.

Multiple Patterns

Use wait_for_any() when the VM might output different things:

rust
let (index, output) = console.wait_for_any(&["login:", "# ", "$ "]).await?;

match index {
    0 => println!("At login prompt"),
    1 => println!("Root shell ready"),
    2 => println!("User shell ready"),
    _ => unreachable!(),
}

This is useful for handling VMs that may boot to different states (login prompt vs auto-logged-in shell).

Executing Commands

The exec() method is the recommended way to run commands. It uses unique markers to reliably detect when a command completes:

rust
use std::time::Duration;

let output = console.exec("ls -la /", Duration::from_secs(5)).await?;
println!("Directory listing:\n{}", output);

exec() works by:

  1. Appending a unique marker (e.g., X=__DONE_42__) to your command
  2. Waiting for that marker in the output
  3. Returning all output up to the marker

This approach avoids false matches that can occur when your expected pattern appears in the command echo rather than the actual output.

Alternative: Manual Write and Wait

For more control, use write_line() followed by wait_for():

rust
console.write_line("echo hello").await?;
let output = console.wait_for("# ").await?;  // Wait for prompt to return

WARNING

Manual write/wait is error-prone. If the pattern appears in the command echo (due to terminal wrapping or other factors), you may capture incomplete output. Prefer exec() for reliability.

Running Commands with Prompt Detection

For simpler cases where you know the shell prompt:

rust
let output = console.run_command("whoami", "# ").await?;
// Or with timeout:
let output = console.run_command_timeout("whoami", "# ", Duration::from_secs(5)).await?;

Reading Output

Non-Blocking Read

read_available() returns whatever output is currently buffered without blocking:

rust
let current_output = console.read_available().await?;
if !current_output.is_empty() {
    println!("Buffered output: {}", current_output);
}

This is useful for:

  • Checking if there is pending output before a command
  • Draining output between commands
  • Debugging console state

Pattern Matching Returns Content

When wait_for() succeeds, it returns all output up to and including the matched pattern:

rust
let output = console.wait_for_timeout("complete", Duration::from_secs(10)).await?;
// output contains everything from the last wait_for call up to "complete"

Control Signals

Interrupt (Ctrl+C)

Stop a running command:

rust
// Start a long-running command
console.write_line("sleep 100").await?;

// Cancel it
console.send_interrupt().await?;

EOF (Ctrl+D)

Send end-of-file signal:

rust
console.send_eof().await?;

Automated Login

The login() helper handles the common login sequence:

rust
// Login with password
console.login("root", Some("password123")).await?;

// Login without password (common for root in test VMs)
console.login("root", None).await?;

This method:

  1. Waits for login: prompt
  2. Sends the username
  3. If password provided, waits for Password: and sends it
  4. Waits for a shell prompt (#, $, or >)

Splitting the Console

For concurrent reading and writing, split the console into separate halves:

rust
let (reader, writer) = console.split().await?;

Both halves implement standard Tokio traits:

  • ConsoleReader implements AsyncRead
  • ConsoleWriter implements AsyncWrite

This is useful for advanced scenarios like:

  • Background output monitoring while sending commands
  • Implementing custom console multiplexing
  • Integration with other async I/O libraries

WARNING

Splitting consumes the VmConsole. You cannot use the high-level methods (exec(), wait_for(), etc.) after splitting. Use this only when you need low-level control.

Best Practices

Always Use Timeouts

Production code should never wait indefinitely:

rust
// Good: Bounded wait time
console.wait_for_timeout("# ", Duration::from_secs(30)).await?;

// Risky: May hang forever if pattern never appears
console.wait_for("# ").await?;

Prefer exec() Over Manual Patterns

The exec() method handles edge cases that manual pattern matching misses:

rust
// Good: Reliable completion detection
let output = console.exec("echo test", Duration::from_secs(5)).await?;

// Risky: "test" might match in command echo, not output
console.write_line("echo test").await?;
console.wait_for("test").await?;

Handle Errors Gracefully

Pattern not found and timeout errors are common. Handle them appropriately:

rust
use capsa::Error;

match console.wait_for_timeout("ready", Duration::from_secs(10)).await {
    Ok(output) => println!("Found pattern: {}", output),
    Err(Error::Timeout(msg)) => {
        eprintln!("VM did not become ready: {}", msg);
        // Consider: retry, log diagnostics, or fail the operation
    }
    Err(Error::PatternNotFound { pattern }) => {
        eprintln!("Pattern '{}' not found (console closed?)", pattern);
    }
    Err(e) => return Err(e),
}

Clean Up After Interrupted Commands

If you send an interrupt, give the shell time to process it:

rust
console.send_interrupt().await?;
tokio::time::sleep(Duration::from_millis(100)).await;
let output = console.read_available().await?;  // Drain any remaining output

Complete Example

A full example demonstrating common console operations:

rust
use capsa::{Capsa, LinuxDirectBootConfig};
use std::time::Duration;

#[tokio::main]
async fn main() -> capsa::Result<()> {
    // Build VM with console enabled
    let vm = Capsa::vm(LinuxDirectBootConfig::new("kernel", "initrd"))
        .cpus(2)
        .memory_mb(512)
        .console_enabled()
        .build()
        .await?;

    // Get console handle
    let console = vm.console().await?;

    // Wait for VM to boot
    console.wait_for_timeout("Boot successful", Duration::from_secs(30)).await?;

    // Execute commands
    let uname = console.exec("uname -a", Duration::from_secs(5)).await?;
    println!("System: {}", uname.trim());

    let files = console.exec("ls /", Duration::from_secs(5)).await?;
    println!("Root contents:\n{}", files);

    // Run a command that takes time
    console.exec("sleep 1 && echo 'done sleeping'", Duration::from_secs(10)).await?;

    // Clean shutdown
    vm.stop().await?;

    Ok(())
}

Released under the MIT License.