blob: 762c379fb5890e8c4d2b89696e2a56a4048e1a39 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Provides utility functions used by multiple fixture files.
use std::env;
use std::io::ErrorKind;
#[cfg(any(target_os = "android", target_os = "linux"))]
use std::os::unix::process::ExitStatusExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::ExitStatus;
use std::process::Output;
use std::sync::mpsc::sync_channel;
use std::sync::mpsc::RecvTimeoutError;
use std::thread;
use std::time::Duration;
use std::time::SystemTime;
use anyhow::bail;
use anyhow::Result;
use tempfile::NamedTempFile;
use crate::sys::binary_name;
use crate::vhost_user::CmdType;
use crate::vhost_user::Config as VuConfig;
pub const DEFAULT_BLOCK_SIZE: u64 = 1024 * 1024;
/// Returns the path to the crosvm binary to be tested.
///
/// The crosvm binary is expected to be alongside to the integration tests
/// binary. Alternatively in the parent directory (cargo will put the
/// test binary in target/debug/deps/ but the crosvm binary in target/debug)
pub fn find_crosvm_binary() -> PathBuf {
let binary_name = binary_name();
let exe_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
let first = exe_dir.join(binary_name);
if first.exists() {
return first;
}
let second = exe_dir.parent().unwrap().join(binary_name);
if second.exists() {
return second;
}
panic!(
"Cannot find {} in ./ or ../ alongside test binary.",
binary_name
);
}
/// Run the provided closure in a separate thread and return it's result. If the closure does not
/// finish before the timeout is reached, an Error is returned instead.
///
/// WARNING: It is not possible to kill the closure if a timeout occurs. It is advised to panic
/// when an error is returned.
pub fn run_with_timeout<F, U>(closure: F, timeout: Duration) -> Result<U>
where
F: FnOnce() -> U + Send + 'static,
U: Send + 'static,
{
run_with_status_check(closure, timeout, || false)
}
/// Run the provided closure in a separate thread and return it's result. If the closure does not
/// finish, continue_fn is called periodically with interval while continue_fn return true. Once
/// continue_fn return false, an Error is returned instead.
///
/// WARNING: It is not possible to kill the closure if a timeout occurs. It is advised to panic
/// when an error is returned.
pub fn run_with_status_check<F, U, C>(
closure: F,
interval: Duration,
mut continue_fn: C,
) -> Result<U>
where
F: FnOnce() -> U + Send + 'static,
U: Send + 'static,
C: FnMut() -> bool,
{
let (tx, rx) = sync_channel::<()>(1);
let handle = thread::spawn(move || {
let result = closure();
// Notify main thread the closure is done. Fail silently if it's not listening anymore.
let _ = tx.send(());
result
});
loop {
match rx.recv_timeout(interval) {
Ok(_) => {
return Ok(handle.join().unwrap());
}
Err(RecvTimeoutError::Timeout) => {
if !continue_fn() {
bail!("closure timed out");
}
}
Err(RecvTimeoutError::Disconnected) => bail!("closure panicked"),
}
}
}
#[derive(Debug)]
pub enum CommandError {
IoError(std::io::Error),
ErrorCode(i32),
Signal(i32),
}
/// Extension trait for utilities on std::process::Command
pub trait CommandExt {
/// Same as Command::output() but will treat non-success status of the Command as an
/// error.
fn output_checked(&mut self) -> std::result::Result<Output, CommandError>;
/// Print the command to be executed
fn log(&mut self) -> &mut Self;
}
impl CommandExt for Command {
fn output_checked(&mut self) -> std::result::Result<Output, CommandError> {
let output = self.output().map_err(CommandError::IoError)?;
if !output.status.success() {
if let Some(code) = output.status.code() {
return Err(CommandError::ErrorCode(code));
} else {
#[cfg(any(target_os = "android", target_os = "linux"))]
if let Some(signal) = output.status.signal() {
return Err(CommandError::Signal(signal));
}
panic!("No error code and no signal should never happen.");
}
}
Ok(output)
}
fn log(&mut self) -> &mut Self {
println!("$ {:?}", self);
self
}
}
/// Extension trait for utilities on std::process::Child
pub trait ChildExt {
/// Same as Child.wait(), but will return with an error after the specified timeout.
fn wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>>;
}
impl ChildExt for std::process::Child {
fn wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>> {
let start_time = SystemTime::now();
while SystemTime::now().duration_since(start_time).unwrap() < timeout {
if let Ok(status) = self.try_wait() {
return Ok(status);
}
thread::sleep(Duration::from_millis(10));
}
Err(std::io::Error::new(
ErrorKind::TimedOut,
"Timeout while waiting for child",
))
}
}
/// Calls the `closure` until it returns a non-error Result.
/// If it has been re-tried `retries` times, the last result is returned.
pub fn retry<F, T, E>(mut closure: F, retries: usize) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
E: std::fmt::Debug,
{
let mut attempts_left = retries + 1;
loop {
let result = closure();
attempts_left -= 1;
if result.is_ok() || attempts_left == 0 {
break result;
} else {
println!("Attempt failed: {:?}", result.err());
}
}
}
/// Prepare a temporary ext4 disk file.
pub fn prepare_disk_img() -> NamedTempFile {
let mut disk = NamedTempFile::new().unwrap();
disk.as_file_mut().set_len(DEFAULT_BLOCK_SIZE).unwrap();
// Add /sbin and /usr/sbin to PATH since some distributions put mkfs.ext4 in one of those
// directories but don't add them to non-root PATH.
let path = env::var("PATH").unwrap();
let path = [&path, "/sbin", "/usr/sbin"].join(":");
// TODO(b/243127910): Use `mkfs.ext4 -d` to include test data.
Command::new("mkfs.ext4")
.arg(disk.path().to_str().unwrap())
.env("PATH", path)
.output()
.expect("failed to execute process");
disk
}
pub fn create_vu_block_config(cmd_type: CmdType, socket: &Path, disk: &Path) -> VuConfig {
let socket_path = socket.to_str().unwrap();
let disk_path = disk.to_str().unwrap();
println!("disk={disk_path}, socket={socket_path}");
match cmd_type {
CmdType::Device => VuConfig::new(cmd_type, "block").extra_args(vec![
"block".to_string(),
"--socket".to_string(),
socket_path.to_string(),
"--file".to_string(),
disk_path.to_string(),
]),
CmdType::Devices => VuConfig::new(cmd_type, "block").extra_args(vec![
"--block".to_string(),
format!("vhost={},path={}", socket_path, disk_path),
]),
}
}