use crate::{args::RunnerArgs, config::Config};
use std::{io, path::Path, process, time::Duration};
use thiserror::Error;
use wait_timeout::ChildExt;
pub fn run(
config: Config,
args: RunnerArgs,
image_path: &Path,
is_test: bool,
) -> Result<i32, RunError> {
let mut run_command: Vec<_> = config
.run_command
.iter()
.map(|arg| arg.replace("{}", &format!("{}", image_path.display())))
.collect();
if is_test {
if config.test_no_reboot {
run_command.push("-no-reboot".to_owned());
}
if let Some(args) = config.test_args {
run_command.extend(args);
}
} else if let Some(args) = config.run_args {
run_command.extend(args);
}
if let Some(args) = args.runner_args {
run_command.extend(args);
}
if !args.quiet {
println!("Running: `{}`", run_command.join(" "));
}
let mut command = process::Command::new(&run_command[0]);
command.args(&run_command[1..]);
let exit_code = if is_test {
let mut child = command.spawn().map_err(|error| RunError::Io {
context: IoErrorContext::QemuTestCommand {
command: format!("{:?}", command),
},
error,
})?;
let timeout = Duration::from_secs(config.test_timeout.into());
match child
.wait_timeout(timeout)
.map_err(context(IoErrorContext::WaitWithTimeout))?
{
None => {
child.kill().map_err(context(IoErrorContext::KillQemu))?;
child.wait().map_err(context(IoErrorContext::WaitForQemu))?;
return Err(RunError::TestTimedOut);
}
Some(exit_status) => {
#[cfg(unix)]
{
if exit_status.code().is_none() {
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = exit_status.signal() {
eprintln!("QEMU process was terminated by signal {}", signal);
}
}
}
let qemu_exit_code = exit_status.code().ok_or(RunError::NoQemuExitCode)?;
match config.test_success_exit_code {
Some(code) if qemu_exit_code == code => 0,
Some(_) if qemu_exit_code == 0 => 1,
_ => qemu_exit_code,
}
}
}
} else {
let status = command.status().map_err(|error| RunError::Io {
context: IoErrorContext::QemuRunCommand {
command: format!("{:?}", command),
},
error,
})?;
status.code().unwrap_or(1)
};
Ok(exit_code)
}
#[derive(Debug, Error)]
pub enum RunError {
#[error("Test timed out")]
TestTimedOut,
#[error("Failed to read QEMU exit code")]
NoQemuExitCode,
#[error("{context}: An I/O error occured: {error}")]
Io {
context: IoErrorContext,
error: io::Error,
},
}
#[derive(Debug, Error)]
pub enum IoErrorContext {
#[error("Failed to execute QEMU run command `{command}`")]
QemuRunCommand {
command: String,
},
#[error("Failed to execute QEMU test command `{command}`")]
QemuTestCommand {
command: String,
},
#[error("Failed to wait with timeout")]
WaitWithTimeout,
#[error("Failed to kill QEMU")]
KillQemu,
#[error("Failed to wait for QEMU process")]
WaitForQemu,
}
fn context(context: IoErrorContext) -> impl FnOnce(io::Error) -> RunError {
|error| RunError::Io { context, error }
}