Skip to main content

cargo_image_runner/runner/
io.rs

1//! I/O handler trait and built-in implementations for capturing and streaming
2//! serial output from running QEMU instances.
3//!
4//! The [`IoHandler`] trait enables:
5//! - **Real-time output streaming** — process serial output as it arrives
6//! - **Programmatic/reactive input** — send serial input in response to output patterns
7//! - **Post-run capture** — full output available after run for test assertions
8//!
9//! # Built-in Handlers
10//!
11//! - [`CaptureHandler`] — accumulates all serial + stderr bytes, returns them via `finish()`
12//! - [`TeeHandler`] — captures AND echoes to real terminal
13//! - [`PatternResponder`] — matches string patterns in serial output and sends responses
14
15/// Actions a handler can return to control the runner.
16#[derive(Debug)]
17pub enum IoAction {
18    /// Continue running normally.
19    Continue,
20    /// Send the given bytes to the QEMU serial input (stdin).
21    SendInput(Vec<u8>),
22    /// Shut down the QEMU process.
23    Shutdown,
24}
25
26/// Data captured during a run, returned by [`IoHandler::finish()`].
27#[derive(Debug, Clone)]
28pub struct CapturedIo {
29    /// Captured serial output bytes.
30    pub serial: Vec<u8>,
31    /// Captured stderr bytes.
32    pub stderr: Vec<u8>,
33}
34
35/// Trait for handling I/O from a running QEMU instance.
36///
37/// Implementors receive callbacks for serial output, stderr, process start/exit,
38/// and can return [`IoAction`]s to send input or shut down the process.
39pub trait IoHandler: Send {
40    /// Called when serial output bytes arrive. Return an [`IoAction`].
41    fn on_output(&mut self, data: &[u8]) -> IoAction {
42        let _ = data;
43        IoAction::Continue
44    }
45
46    /// Called when QEMU stderr produces output.
47    fn on_stderr(&mut self, data: &[u8]) {
48        let _ = data;
49    }
50
51    /// Called when QEMU exits.
52    fn on_exit(&mut self, exit_code: i32, timed_out: bool) {
53        let _ = (exit_code, timed_out);
54    }
55
56    /// Called before QEMU starts with the command being executed.
57    fn on_start(&mut self, command: &std::process::Command) {
58        let _ = command;
59    }
60
61    /// Called after run completes to extract captured data.
62    fn finish(self: Box<Self>) -> Option<CapturedIo> {
63        None
64    }
65}
66
67/// Handler that accumulates all serial and stderr bytes for post-run inspection.
68///
69/// # Example
70///
71/// ```no_run
72/// use cargo_image_runner::runner::io::CaptureHandler;
73///
74/// let handler = CaptureHandler::new();
75/// // Pass to builder via .io_handler(handler)
76/// // After run, CapturedIo will contain all serial/stderr output.
77/// ```
78#[derive(Debug, Default)]
79pub struct CaptureHandler {
80    serial: Vec<u8>,
81    stderr: Vec<u8>,
82}
83
84impl CaptureHandler {
85    /// Create a new capture handler.
86    pub fn new() -> Self {
87        Self::default()
88    }
89}
90
91impl IoHandler for CaptureHandler {
92    fn on_output(&mut self, data: &[u8]) -> IoAction {
93        self.serial.extend_from_slice(data);
94        IoAction::Continue
95    }
96
97    fn on_stderr(&mut self, data: &[u8]) {
98        self.stderr.extend_from_slice(data);
99    }
100
101    fn finish(self: Box<Self>) -> Option<CapturedIo> {
102        Some(CapturedIo {
103            serial: self.serial,
104            stderr: self.stderr,
105        })
106    }
107}
108
109/// Handler that captures output AND echoes it to the real terminal.
110///
111/// Wraps a [`CaptureHandler`] internally so all data is available via `finish()`.
112#[derive(Debug, Default)]
113pub struct TeeHandler {
114    capture: CaptureHandler,
115}
116
117impl TeeHandler {
118    /// Create a new tee handler.
119    pub fn new() -> Self {
120        Self::default()
121    }
122}
123
124impl IoHandler for TeeHandler {
125    fn on_output(&mut self, data: &[u8]) -> IoAction {
126        use std::io::Write;
127        let _ = std::io::stdout().write_all(data);
128        self.capture.on_output(data)
129    }
130
131    fn on_stderr(&mut self, data: &[u8]) {
132        use std::io::Write;
133        let _ = std::io::stderr().write_all(data);
134        self.capture.on_stderr(data);
135    }
136
137    fn finish(self: Box<Self>) -> Option<CapturedIo> {
138        Box::new(self.capture).finish()
139    }
140}
141
142/// A pattern/response pair for [`PatternResponder`].
143#[derive(Debug, Clone)]
144struct PatternRule {
145    pattern: String,
146    response: Vec<u8>,
147}
148
149/// Handler that watches serial output for string patterns and sends responses.
150///
151/// Also captures all output for post-run inspection.
152///
153/// # Example
154///
155/// ```no_run
156/// use cargo_image_runner::runner::io::PatternResponder;
157///
158/// let handler = PatternResponder::new()
159///     .on_pattern("login:", b"root\n")
160///     .on_pattern("$ ", b"run-tests\n");
161/// ```
162#[derive(Debug, Default)]
163pub struct PatternResponder {
164    rules: Vec<PatternRule>,
165    /// Rolling buffer of recent serial output for pattern matching.
166    buffer: Vec<u8>,
167    capture: CaptureHandler,
168}
169
170impl PatternResponder {
171    /// Create a new pattern responder with no rules.
172    pub fn new() -> Self {
173        Self::default()
174    }
175
176    /// Add a pattern/response rule. When `pattern` is found in serial output,
177    /// `response` bytes will be sent to QEMU stdin.
178    pub fn on_pattern(mut self, pattern: &str, response: &[u8]) -> Self {
179        self.rules.push(PatternRule {
180            pattern: pattern.to_string(),
181            response: response.to_vec(),
182        });
183        self
184    }
185}
186
187impl IoHandler for PatternResponder {
188    fn on_output(&mut self, data: &[u8]) -> IoAction {
189        self.capture.on_output(data);
190        self.buffer.extend_from_slice(data);
191
192        // Keep buffer size bounded — retain enough for longest pattern match
193        let max_pattern_len = self.rules.iter().map(|r| r.pattern.len()).max().unwrap_or(0);
194        let max_buf = max_pattern_len.max(4096);
195        if self.buffer.len() > max_buf * 2 {
196            let drain = self.buffer.len() - max_buf;
197            self.buffer.drain(..drain);
198        }
199
200        // Check patterns against buffer
201        let buf_str = String::from_utf8_lossy(&self.buffer);
202        for rule in &self.rules {
203            if buf_str.contains(&rule.pattern) {
204                // Clear buffer past the match to avoid re-triggering
205                self.buffer.clear();
206                return IoAction::SendInput(rule.response.clone());
207            }
208        }
209
210        IoAction::Continue
211    }
212
213    fn on_stderr(&mut self, data: &[u8]) {
214        self.capture.on_stderr(data);
215    }
216
217    fn finish(self: Box<Self>) -> Option<CapturedIo> {
218        Box::new(self.capture).finish()
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_capture_handler_accumulates() {
228        let mut handler = CaptureHandler::new();
229        handler.on_output(b"hello ");
230        handler.on_output(b"world");
231        handler.on_stderr(b"err1");
232        handler.on_stderr(b"err2");
233
234        let captured = Box::new(handler).finish().unwrap();
235        assert_eq!(captured.serial, b"hello world");
236        assert_eq!(captured.stderr, b"err1err2");
237    }
238
239    #[test]
240    fn test_capture_handler_empty() {
241        let handler = CaptureHandler::new();
242        let captured = Box::new(handler).finish().unwrap();
243        assert!(captured.serial.is_empty());
244        assert!(captured.stderr.is_empty());
245    }
246
247    #[test]
248    fn test_tee_handler_captures() {
249        let mut handler = TeeHandler::new();
250        handler.on_output(b"data");
251        handler.on_stderr(b"err");
252
253        let captured = Box::new(handler).finish().unwrap();
254        assert_eq!(captured.serial, b"data");
255        assert_eq!(captured.stderr, b"err");
256    }
257
258    #[test]
259    fn test_pattern_responder_matches() {
260        let mut handler = PatternResponder::new()
261            .on_pattern("login:", b"root\n")
262            .on_pattern("$ ", b"ls\n");
263
264        // No match yet
265        let action = handler.on_output(b"booting...\n");
266        assert!(matches!(action, IoAction::Continue));
267
268        // Match login:
269        let action = handler.on_output(b"login:");
270        match action {
271            IoAction::SendInput(data) => assert_eq!(data, b"root\n"),
272            other => panic!("expected SendInput, got {:?}", other),
273        }
274
275        // Match shell prompt
276        let action = handler.on_output(b"root@host:~$ ");
277        match action {
278            IoAction::SendInput(data) => assert_eq!(data, b"ls\n"),
279            other => panic!("expected SendInput, got {:?}", other),
280        }
281    }
282
283    #[test]
284    fn test_pattern_responder_captures() {
285        let mut handler = PatternResponder::new().on_pattern("x", b"y");
286        handler.on_output(b"abc");
287        handler.on_stderr(b"err");
288
289        let captured = Box::new(handler).finish().unwrap();
290        assert_eq!(captured.serial, b"abc");
291        assert_eq!(captured.stderr, b"err");
292    }
293
294    #[test]
295    fn test_pattern_responder_no_rules() {
296        let mut handler = PatternResponder::new();
297        let action = handler.on_output(b"anything");
298        assert!(matches!(action, IoAction::Continue));
299    }
300
301    #[test]
302    fn test_default_io_handler_noop() {
303        struct Noop;
304        impl IoHandler for Noop {}
305
306        let mut handler = Noop;
307        let action = handler.on_output(b"data");
308        assert!(matches!(action, IoAction::Continue));
309        handler.on_stderr(b"err");
310        handler.on_exit(0, false);
311        assert!(Box::new(handler).finish().is_none());
312    }
313}