cargo-image-runner 0.5.0

A generic, customizable runner for building and booting kernel/embedded images with Limine, GRUB, QEMU, and more
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
use super::io::{IoAction, IoHandler};
use super::{RunResult, Runner};
use crate::config::SerialMode;
use crate::core::context::Context;
use crate::core::error::{Error, Result};
use crate::firmware::OvmfFirmware;
use std::io::Read as _;
use std::io::Write as _;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::time::Duration;

/// QEMU runner for executing bootable images.
pub struct QemuRunner;

impl QemuRunner {
    /// Create a new QEMU runner.
    pub fn new() -> Self {
        Self
    }

    /// Check if QEMU is available.
    fn check_available() -> bool {
        Command::new("qemu-system-x86_64")
            .arg("--version")
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .is_ok()
    }

    /// Build the base QEMU command with machine, memory, cores, KVM, UEFI,
    /// image, and extra arguments — but NOT serial or stdio config.
    fn build_command(&self, ctx: &Context, image_path: &Path) -> Result<Command> {
        let qemu_config = &ctx.config.runner.qemu;

        let mut cmd = Command::new(&qemu_config.binary);

        // Basic QEMU args
        cmd.arg("-machine").arg(&qemu_config.machine);
        cmd.arg("-m").arg(qemu_config.memory.to_string());

        // CPU cores
        if qemu_config.cores > 1 {
            cmd.arg("-smp").arg(qemu_config.cores.to_string());
        }

        // KVM acceleration if enabled and available
        #[cfg(target_os = "linux")]
        if qemu_config.kvm {
            cmd.arg("-enable-kvm");
        }

        // Handle UEFI boot
        if ctx.config.boot.boot_type.needs_uefi() {
            #[cfg(feature = "uefi")]
            {
                let ovmf = OvmfFirmware::new(ctx.cache_dir.join("ovmf"));
                let ovmf_files = ovmf.fetch()?;

                cmd.arg("-drive").arg(format!(
                    "if=pflash,format=raw,readonly=on,file={}",
                    ovmf_files.code().display()
                ));
                cmd.arg("-drive").arg(format!(
                    "if=pflash,format=raw,file={}",
                    ovmf_files.vars().display()
                ));
            }

            #[cfg(not(feature = "uefi"))]
            {
                return Err(Error::feature_not_enabled(
                    "uefi (required for UEFI boot)",
                ));
            }
        }

        // Attach the image
        if image_path.is_dir() {
            cmd.arg("-drive").arg(format!(
                "format=raw,file=fat:rw:{}",
                image_path.display()
            ));
        } else if image_path.extension().and_then(|s| s.to_str()) == Some("iso") {
            cmd.arg("-cdrom").arg(image_path);
        } else {
            cmd.arg("-drive")
                .arg(format!("format=raw,file={}", image_path.display()));
        }

        // Add extra arguments from config (test or run mode)
        for arg in ctx.get_extra_args() {
            cmd.arg(arg);
        }

        // Add extra QEMU arguments from config
        for arg in &qemu_config.extra_args {
            cmd.arg(arg);
        }

        // Add env var args (CARGO_IMAGE_RUNNER_QEMU_ARGS)
        for arg in &ctx.env_extra_args {
            cmd.arg(arg);
        }

        // Add CLI passthrough args — only in non-test mode.
        if !ctx.is_test {
            for arg in &ctx.cli_extra_args {
                cmd.arg(arg);
            }
        }

        Ok(cmd)
    }

    /// Apply serial and monitor flags to a command based on SerialConfig.
    fn apply_serial_config(cmd: &mut Command, mode: SerialMode, separate_monitor: Option<bool>) {
        let serial_arg = match mode {
            SerialMode::MonStdio => "mon:stdio",
            SerialMode::Stdio => "stdio",
            SerialMode::None => "none",
        };
        cmd.arg("-serial").arg(serial_arg);

        // If separate_monitor is explicitly requested and mode is not mon:stdio
        // (which already includes the monitor), add a separate monitor.
        if separate_monitor == Some(true) && mode != SerialMode::MonStdio {
            cmd.arg("-monitor").arg("none");
        }
    }

    /// Set up timeout watchdog thread. Returns the timed_out flag.
    fn setup_timeout(
        timeout_secs: Option<u64>,
        child_id: u32,
    ) -> (Arc<AtomicBool>, Option<std::thread::JoinHandle<()>>) {
        let timed_out = Arc::new(AtomicBool::new(false));
        let handle = if let Some(secs) = timeout_secs {
            let flag = timed_out.clone();
            Some(std::thread::spawn(move || {
                std::thread::sleep(Duration::from_secs(secs));
                if !flag.swap(true, Ordering::SeqCst) {
                    #[cfg(unix)]
                    {
                        unsafe {
                            libc::kill(child_id as i32, libc::SIGKILL);
                        }
                    }
                    #[cfg(not(unix))]
                    {
                        let _ = child_id;
                    }
                }
            }))
        } else {
            None
        };
        (timed_out, handle)
    }
}

impl Default for QemuRunner {
    fn default() -> Self {
        Self::new()
    }
}

/// Internal events sent from reader threads to the main I/O loop.
enum IoEvent {
    Stdout(Vec<u8>),
    Stderr(Vec<u8>),
    StdoutClosed,
    StderrClosed,
}

impl Runner for QemuRunner {
    fn run(&self, ctx: &Context, image_path: &Path) -> Result<RunResult> {
        let qemu_config = &ctx.config.runner.qemu;
        let mut cmd = self.build_command(ctx, image_path)?;

        // Apply serial config from settings
        Self::apply_serial_config(
            &mut cmd,
            qemu_config.serial.mode,
            qemu_config.serial.separate_monitor,
        );

        if ctx.config.verbose {
            println!("Executing: {:?}", cmd);
        }

        if ctx.is_test {
            // Test mode: inherit stdout/stderr, enforce timeout, check exit code.
            cmd.stdin(Stdio::null());
            cmd.stdout(Stdio::inherit());
            cmd.stderr(Stdio::inherit());

            let child = cmd.spawn().map_err(|e| {
                Error::runner(format!(
                    "failed to execute {}: {}",
                    qemu_config.binary, e
                ))
            })?;

            let (timed_out, _timeout_handle) =
                Self::setup_timeout(ctx.config.test.timeout, child.id());

            let status = child.wait_with_output().map_err(|e| {
                Error::runner(format!("failed to wait for {}: {}", qemu_config.binary, e))
            })?;

            let was_timed_out = timed_out.swap(true, Ordering::SeqCst);

            let exit_code = status.status.code().unwrap_or(-1);
            let success = if let Some(success_code) = ctx.test_success_exit_code() {
                exit_code == success_code
            } else {
                status.status.success()
            };

            let mut result = RunResult::new(exit_code, success);
            if was_timed_out {
                result = result.with_timeout();
            }
            Ok(result)
        } else {
            // Normal mode: inherit stdio
            cmd.stdin(Stdio::inherit());
            cmd.stdout(Stdio::inherit());
            cmd.stderr(Stdio::inherit());

            let status = cmd.status().map_err(|e| {
                Error::runner(format!(
                    "failed to execute {}: {}",
                    qemu_config.binary, e
                ))
            })?;

            let exit_code = status.code().unwrap_or(-1);
            Ok(RunResult::new(exit_code, status.success()))
        }
    }

    fn run_with_io(
        &self,
        ctx: &Context,
        image_path: &Path,
        handler: &mut dyn IoHandler,
    ) -> Result<RunResult> {
        let qemu_config = &ctx.config.runner.qemu;
        let mut cmd = self.build_command(ctx, image_path)?;

        // When using an I/O handler, force serial to stdio and disable the monitor
        // so stdout carries only serial data.
        cmd.arg("-serial").arg("stdio");
        cmd.arg("-monitor").arg("none");

        // Pipe all stdio for programmatic access
        cmd.stdin(Stdio::piped());
        cmd.stdout(Stdio::piped());
        cmd.stderr(Stdio::piped());

        if ctx.config.verbose {
            println!("Executing (with I/O handler): {:?}", cmd);
        }

        handler.on_start(&cmd);

        let mut child = cmd.spawn().map_err(|e| {
            Error::runner(format!(
                "failed to execute {}: {}",
                qemu_config.binary, e
            ))
        })?;

        let child_id = child.id();

        // Take ownership of piped streams
        let mut child_stdin = child.stdin.take();
        let child_stdout = child
            .stdout
            .take()
            .ok_or_else(|| Error::runner("failed to capture QEMU stdout"))?;
        let child_stderr = child
            .stderr
            .take()
            .ok_or_else(|| Error::runner("failed to capture QEMU stderr"))?;

        // Set up I/O event channel
        let (tx, rx) = mpsc::channel::<IoEvent>();

        // Spawn stdout reader thread
        let tx_stdout = tx.clone();
        let stdout_thread = std::thread::spawn(move || {
            let mut reader = child_stdout;
            let mut buf = [0u8; 4096];
            loop {
                match reader.read(&mut buf) {
                    Ok(0) => {
                        let _ = tx_stdout.send(IoEvent::StdoutClosed);
                        break;
                    }
                    Ok(n) => {
                        if tx_stdout.send(IoEvent::Stdout(buf[..n].to_vec())).is_err() {
                            break;
                        }
                    }
                    Err(_) => {
                        let _ = tx_stdout.send(IoEvent::StdoutClosed);
                        break;
                    }
                }
            }
        });

        // Spawn stderr reader thread
        let tx_stderr = tx;
        let stderr_thread = std::thread::spawn(move || {
            let mut reader = child_stderr;
            let mut buf = [0u8; 4096];
            loop {
                match reader.read(&mut buf) {
                    Ok(0) => {
                        let _ = tx_stderr.send(IoEvent::StderrClosed);
                        break;
                    }
                    Ok(n) => {
                        if tx_stderr.send(IoEvent::Stderr(buf[..n].to_vec())).is_err() {
                            break;
                        }
                    }
                    Err(_) => {
                        let _ = tx_stderr.send(IoEvent::StderrClosed);
                        break;
                    }
                }
            }
        });

        // Set up timeout watchdog
        let (timed_out, _timeout_handle) =
            Self::setup_timeout(ctx.config.test.timeout, child_id);

        // Main I/O event loop
        let mut stdout_closed = false;
        let mut stderr_closed = false;

        while !stdout_closed || !stderr_closed {
            let event = match rx.recv() {
                Ok(event) => event,
                Err(_) => break,
            };

            match event {
                IoEvent::Stdout(data) => {
                    let action = handler.on_output(&data);
                    match action {
                        IoAction::Continue => {}
                        IoAction::SendInput(input) => {
                            if let Some(ref mut stdin) = child_stdin {
                                let _ = stdin.write_all(&input);
                                let _ = stdin.flush();
                            }
                        }
                        IoAction::Shutdown => {
                            // Kill the child process
                            let _ = child.kill();
                            break;
                        }
                    }
                }
                IoEvent::Stderr(data) => {
                    handler.on_stderr(&data);
                }
                IoEvent::StdoutClosed => {
                    stdout_closed = true;
                }
                IoEvent::StderrClosed => {
                    stderr_closed = true;
                }
            }
        }

        // Drop stdin to unblock child if it's waiting for input
        drop(child_stdin);

        // Wait for child to exit
        let status = child.wait().map_err(|e| {
            Error::runner(format!("failed to wait for {}: {}", qemu_config.binary, e))
        })?;

        // Join reader threads
        let _ = stdout_thread.join();
        let _ = stderr_thread.join();

        // Signal the timeout thread that we're done
        let was_timed_out = timed_out.swap(true, Ordering::SeqCst);

        let exit_code = status.code().unwrap_or(-1);
        let success = if let Some(success_code) = ctx.test_success_exit_code() {
            exit_code == success_code
        } else {
            status.success()
        };

        handler.on_exit(exit_code, was_timed_out);

        let mut result = RunResult::new(exit_code, success);
        if was_timed_out {
            result = result.with_timeout();
        }

        Ok(result)
    }

    fn is_available(&self) -> bool {
        Self::check_available()
    }

    fn name(&self) -> &str {
        "QEMU"
    }
}