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:
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:
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:
// Wait indefinitely for the pattern
console.wait_for("login:").await?;With Timeout
Always prefer wait_for_timeout() in production code to avoid hanging indefinitely:
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:
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
Recommended: exec()
The exec() method is the recommended way to run commands. It uses unique markers to reliably detect when a command completes:
use std::time::Duration;
let output = console.exec("ls -la /", Duration::from_secs(5)).await?;
println!("Directory listing:\n{}", output);exec() works by:
- Appending a unique marker (e.g.,
X=__DONE_42__) to your command - Waiting for that marker in the output
- 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():
console.write_line("echo hello").await?;
let output = console.wait_for("# ").await?; // Wait for prompt to returnWARNING
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:
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:
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:
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:
// 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:
console.send_eof().await?;Automated Login
The login() helper handles the common login sequence:
// 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:
- Waits for
login:prompt - Sends the username
- If password provided, waits for
Password:and sends it - Waits for a shell prompt (
#,$, or>)
Splitting the Console
For concurrent reading and writing, split the console into separate halves:
let (reader, writer) = console.split().await?;Both halves implement standard Tokio traits:
ConsoleReaderimplementsAsyncReadConsoleWriterimplementsAsyncWrite
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:
// 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:
// 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:
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:
console.send_interrupt().await?;
tokio::time::sleep(Duration::from_millis(100)).await;
let output = console.read_available().await?; // Drain any remaining outputComplete Example
A full example demonstrating common console operations:
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(())
}