1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 //! Provides utility functions used by multiple fixture files.
6
7 use std::env;
8 use std::io::ErrorKind;
9 #[cfg(any(target_os = "android", target_os = "linux"))]
10 use std::os::unix::process::ExitStatusExt;
11 use std::path::Path;
12 use std::path::PathBuf;
13 use std::process::Command;
14 use std::process::ExitStatus;
15 use std::process::Output;
16 use std::sync::mpsc::sync_channel;
17 use std::sync::mpsc::RecvTimeoutError;
18 use std::thread;
19 use std::time::Duration;
20 use std::time::SystemTime;
21
22 use anyhow::bail;
23 use anyhow::Result;
24 use tempfile::NamedTempFile;
25
26 use crate::sys::binary_name;
27 use crate::vhost_user::CmdType;
28 use crate::vhost_user::Config as VuConfig;
29
30 pub const DEFAULT_BLOCK_SIZE: u64 = 1024 * 1024;
31
32 /// Returns the path to the crosvm binary to be tested.
33 ///
34 /// The crosvm binary is expected to be alongside to the integration tests
35 /// binary. Alternatively in the parent directory (cargo will put the
36 /// test binary in target/debug/deps/ but the crosvm binary in target/debug)
find_crosvm_binary() -> PathBuf37 pub fn find_crosvm_binary() -> PathBuf {
38 let binary_name = binary_name();
39 let exe_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
40 let first = exe_dir.join(binary_name);
41 if first.exists() {
42 return first;
43 }
44 let second = exe_dir.parent().unwrap().join(binary_name);
45 if second.exists() {
46 return second;
47 }
48 panic!(
49 "Cannot find {} in ./ or ../ alongside test binary.",
50 binary_name
51 );
52 }
53
54 /// Run the provided closure in a separate thread and return it's result. If the closure does not
55 /// finish before the timeout is reached, an Error is returned instead.
56 ///
57 /// WARNING: It is not possible to kill the closure if a timeout occurs. It is advised to panic
58 /// when an error is returned.
run_with_timeout<F, U>(closure: F, timeout: Duration) -> Result<U> where F: FnOnce() -> U + Send + 'static, U: Send + 'static,59 pub fn run_with_timeout<F, U>(closure: F, timeout: Duration) -> Result<U>
60 where
61 F: FnOnce() -> U + Send + 'static,
62 U: Send + 'static,
63 {
64 run_with_status_check(closure, timeout, || false)
65 }
66
67 /// Run the provided closure in a separate thread and return it's result. If the closure does not
68 /// finish, continue_fn is called periodically with interval while continue_fn return true. Once
69 /// continue_fn return false, an Error is returned instead.
70 ///
71 /// WARNING: It is not possible to kill the closure if a timeout occurs. It is advised to panic
72 /// when an error is returned.
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,73 pub fn run_with_status_check<F, U, C>(
74 closure: F,
75 interval: Duration,
76 mut continue_fn: C,
77 ) -> Result<U>
78 where
79 F: FnOnce() -> U + Send + 'static,
80 U: Send + 'static,
81 C: FnMut() -> bool,
82 {
83 let (tx, rx) = sync_channel::<()>(1);
84 let handle = thread::spawn(move || {
85 let result = closure();
86 // Notify main thread the closure is done. Fail silently if it's not listening anymore.
87 let _ = tx.send(());
88 result
89 });
90 loop {
91 match rx.recv_timeout(interval) {
92 Ok(_) => {
93 return Ok(handle.join().unwrap());
94 }
95 Err(RecvTimeoutError::Timeout) => {
96 if !continue_fn() {
97 bail!("closure timed out");
98 }
99 }
100 Err(RecvTimeoutError::Disconnected) => bail!("closure panicked"),
101 }
102 }
103 }
104
105 #[derive(Debug)]
106 pub enum CommandError {
107 IoError(std::io::Error),
108 ErrorCode(i32),
109 Signal(i32),
110 }
111
112 /// Extension trait for utilities on std::process::Command
113 pub trait CommandExt {
114 /// Same as Command::output() but will treat non-success status of the Command as an
115 /// error.
output_checked(&mut self) -> std::result::Result<Output, CommandError>116 fn output_checked(&mut self) -> std::result::Result<Output, CommandError>;
117
118 /// Print the command to be executed
log(&mut self) -> &mut Self119 fn log(&mut self) -> &mut Self;
120 }
121
122 impl CommandExt for Command {
output_checked(&mut self) -> std::result::Result<Output, CommandError>123 fn output_checked(&mut self) -> std::result::Result<Output, CommandError> {
124 let output = self.output().map_err(CommandError::IoError)?;
125 if !output.status.success() {
126 if let Some(code) = output.status.code() {
127 return Err(CommandError::ErrorCode(code));
128 } else {
129 #[cfg(any(target_os = "android", target_os = "linux"))]
130 if let Some(signal) = output.status.signal() {
131 return Err(CommandError::Signal(signal));
132 }
133 panic!("No error code and no signal should never happen.");
134 }
135 }
136 Ok(output)
137 }
138
log(&mut self) -> &mut Self139 fn log(&mut self) -> &mut Self {
140 println!("$ {:?}", self);
141 self
142 }
143 }
144
145 /// Extension trait for utilities on std::process::Child
146 pub trait ChildExt {
147 /// Same as Child.wait(), but will return with an error after the specified timeout.
wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>>148 fn wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>>;
149 }
150
151 impl ChildExt for std::process::Child {
wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>>152 fn wait_with_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<ExitStatus>> {
153 let start_time = SystemTime::now();
154 while SystemTime::now().duration_since(start_time).unwrap() < timeout {
155 if let Ok(status) = self.try_wait() {
156 return Ok(status);
157 }
158 thread::sleep(Duration::from_millis(10));
159 }
160 Err(std::io::Error::new(
161 ErrorKind::TimedOut,
162 "Timeout while waiting for child",
163 ))
164 }
165 }
166
167 /// Calls the `closure` until it returns a non-error Result.
168 /// If it has been re-tried `retries` times, the last result is returned.
retry<F, T, E>(mut closure: F, retries: usize) -> Result<T, E> where F: FnMut() -> Result<T, E>, E: std::fmt::Debug,169 pub fn retry<F, T, E>(mut closure: F, retries: usize) -> Result<T, E>
170 where
171 F: FnMut() -> Result<T, E>,
172 E: std::fmt::Debug,
173 {
174 let mut attempts_left = retries + 1;
175 loop {
176 let result = closure();
177 attempts_left -= 1;
178 if result.is_ok() || attempts_left == 0 {
179 break result;
180 } else {
181 println!("Attempt failed: {:?}", result.err());
182 }
183 }
184 }
185
186 /// Prepare a temporary ext4 disk file.
prepare_disk_img() -> NamedTempFile187 pub fn prepare_disk_img() -> NamedTempFile {
188 let mut disk = NamedTempFile::new().unwrap();
189 disk.as_file_mut().set_len(DEFAULT_BLOCK_SIZE).unwrap();
190
191 // Add /sbin and /usr/sbin to PATH since some distributions put mkfs.ext4 in one of those
192 // directories but don't add them to non-root PATH.
193 let path = env::var("PATH").unwrap();
194 let path = [&path, "/sbin", "/usr/sbin"].join(":");
195
196 // TODO(b/243127910): Use `mkfs.ext4 -d` to include test data.
197 Command::new("mkfs.ext4")
198 .arg(disk.path().to_str().unwrap())
199 .env("PATH", path)
200 .output()
201 .expect("failed to execute process");
202 disk
203 }
204
create_vu_block_config(cmd_type: CmdType, socket: &Path, disk: &Path) -> VuConfig205 pub fn create_vu_block_config(cmd_type: CmdType, socket: &Path, disk: &Path) -> VuConfig {
206 let socket_path = socket.to_str().unwrap();
207 let disk_path = disk.to_str().unwrap();
208 println!("disk={disk_path}, socket={socket_path}");
209 match cmd_type {
210 CmdType::Device => VuConfig::new(cmd_type, "block").extra_args(vec![
211 "block".to_string(),
212 "--socket-path".to_string(),
213 socket_path.to_string(),
214 "--file".to_string(),
215 disk_path.to_string(),
216 ]),
217 CmdType::Devices => VuConfig::new(cmd_type, "block").extra_args(vec![
218 "--block".to_string(),
219 format!("vhost={},path={}", socket_path, disk_path),
220 ]),
221 }
222 }
223
create_vu_console_multiport_config( socket: &Path, file_path: Vec<(PathBuf, PathBuf)>, ) -> VuConfig224 pub fn create_vu_console_multiport_config(
225 socket: &Path,
226 file_path: Vec<(PathBuf, PathBuf)>,
227 ) -> VuConfig {
228 let socket_path = socket.to_str().unwrap();
229
230 let mut args = vec![
231 "console".to_string(),
232 "--socket-path".to_string(),
233 socket_path.to_string(),
234 ];
235
236 for (i, (output_file, input_file)) in file_path.iter().enumerate() {
237 args.push("--port".to_string());
238 match input_file.file_name().is_some() {
239 true => {
240 args.push(format!(
241 "type=file,hardware=virtio-console,name=port{},path={},input={}",
242 i,
243 output_file.to_str().unwrap(),
244 input_file.to_str().unwrap(),
245 ));
246 }
247 false => {
248 args.push(format!(
249 "type=file,hardware=virtio-console,name=port{},path={}",
250 i,
251 output_file.to_str().unwrap(),
252 ));
253 }
254 };
255 }
256 VuConfig::new(CmdType::Device, "console").extra_args(args)
257 }
258