1use std::ffi::CString;
6use std::fs::File;
7use std::fs::OpenOptions;
8use std::io;
9use std::io::BufReader;
10use std::os::unix::fs::OpenOptionsExt;
11use std::path::Path;
12use std::path::PathBuf;
13use std::process::Child;
14use std::process::Command;
15use std::process::Stdio;
16use std::sync::Arc;
17use std::sync::Mutex;
18use std::time::Duration;
19use std::time::Instant;
20
21use anyhow::anyhow;
22use anyhow::Context;
23use anyhow::Result;
24use delegate::wire_format::DelegateMessage;
25use libc::O_DIRECT;
26use serde_json::StreamDeserializer;
27use tempfile::TempDir;
28
29use crate::utils::find_crosvm_binary;
30use crate::utils::run_with_status_check;
31use crate::vm::local_path_from_url;
32use crate::vm::Config;
33
34const FROM_GUEST_PIPE: &str = "from_guest";
35const TO_GUEST_PIPE: &str = "to_guest";
36const CONTROL_PIPE: &str = "control";
37
38const VM_COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(10);
41
42pub(crate) type SerialArgs = Path;
43
44pub fn binary_name() -> &'static str {
46 "crosvm"
47}
48
49pub(crate) fn mkfifo(path: &Path) -> io::Result<()> {
51 let cpath = CString::new(path.to_str().unwrap()).unwrap();
52 let result = unsafe { libc::mkfifo(cpath.as_ptr(), 0o777) };
54 if result == 0 {
55 Ok(())
56 } else {
57 Err(io::Error::last_os_error())
58 }
59}
60
61pub struct TestVmSys {
62 #[allow(dead_code)]
64 pub test_dir: TempDir,
65 pub from_guest_reader: Arc<
66 Mutex<
67 StreamDeserializer<
68 'static,
69 serde_json::de::IoRead<BufReader<std::fs::File>>,
70 DelegateMessage,
71 >,
72 >,
73 >,
74 pub to_guest: Arc<Mutex<File>>,
75 pub control_socket_path: PathBuf,
76 pub process: Option<Child>, }
78
79impl TestVmSys {
80 pub fn check_rootfs_file(rootfs_path: &Path) {
83 if let Err(e) = OpenOptions::new()
84 .custom_flags(O_DIRECT)
85 .write(false)
86 .read(true)
87 .open(rootfs_path)
88 {
89 panic!("File open with O_DIRECT expected to work but did not: {e}");
90 }
91 }
92
93 fn configure_serial_devices(
97 command: &mut Command,
98 stdout_hardware_type: &str,
99 from_guest_pipe: &Path,
100 to_guest_pipe: &Path,
101 ) {
102 let stdout_serial_option = format!("type=stdout,hardware={stdout_hardware_type},console");
103 command.args(["--serial", &stdout_serial_option]);
104
105 let serial_params = format!(
107 "type=file,path={},input={},num=2",
108 from_guest_pipe.display(),
109 to_guest_pipe.display()
110 );
111 command.args(["--serial", &serial_params]);
112 }
113
114 fn configure_rootfs(command: &mut Command, o_direct: bool, rw: bool, path: &Path) {
116 let rootfs_and_option = format!(
117 "{}{}{},root",
118 path.as_os_str().to_str().unwrap(),
119 if o_direct { ",direct=true" } else { "" },
120 if rw { "" } else { ",ro" }
121 );
122 command
123 .args(["--block", &rootfs_and_option])
124 .args(["--params", "init=/bin/delegate"]);
125 }
126
127 pub fn new_generic<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVmSys>
128 where
129 F: FnOnce(&mut Command, &Path, &Config) -> Result<()>,
130 {
131 let test_dir = TempDir::new()?;
133 let from_guest_pipe = test_dir.path().join(FROM_GUEST_PIPE);
134 let to_guest_pipe = test_dir.path().join(TO_GUEST_PIPE);
135 mkfifo(&from_guest_pipe)?;
136 mkfifo(&to_guest_pipe)?;
137
138 let control_socket_path = test_dir.path().join(CONTROL_PIPE);
139
140 let mut command = match &cfg.wrapper_cmd {
141 Some(cmd) => {
142 let wrapper_splitted =
143 shlex::split(cmd).context("Failed to parse wrapper command")?;
144 let mut command_tmp = if sudo {
145 let mut command = Command::new("sudo");
146 command.arg(&wrapper_splitted[0]);
147 command
148 } else {
149 Command::new(&wrapper_splitted[0])
150 };
151
152 command_tmp.args(&wrapper_splitted[1..]);
153 command_tmp.arg(find_crosvm_binary());
154 command_tmp
155 }
156 None => {
157 if sudo {
158 let mut command = Command::new("sudo");
159 command.arg(find_crosvm_binary());
160 command
161 } else {
162 Command::new(find_crosvm_binary())
163 }
164 }
165 };
166
167 command.env("RUST_BACKTRACE", "full");
168
169 if let Some(log_file_name) = &cfg.log_file {
170 let log_file_stdout = File::create(log_file_name)?;
171 let log_file_stderr = log_file_stdout.try_clone()?;
172 command.stdout(Stdio::from(log_file_stdout));
173 command.stderr(Stdio::from(log_file_stderr));
174 }
175
176 command.args(["--log-level", cfg.log_level.as_str()]);
177 command.args(["run"]);
178
179 f(&mut command, test_dir.path(), &cfg)?;
180
181 command.args(&cfg.extra_args);
182
183 println!("$ {command:?}");
184 let mut process = command.spawn()?;
185
186 let start = Instant::now();
189 let run_result = run_with_status_check(
190 move || (File::create(to_guest_pipe), File::open(from_guest_pipe)),
191 Duration::from_millis(200),
192 || {
193 if start.elapsed() > VM_COMMUNICATION_TIMEOUT {
194 return false;
195 }
196 if let Some(wait_result) = process.try_wait().unwrap() {
197 println!("crosvm unexpectedly exited: {wait_result:?}");
198 false
199 } else {
200 true
201 }
202 },
203 );
204
205 let (to_guest, from_guest) = match run_result {
206 Ok((to_guest, from_guest)) => (
207 to_guest.context("Cannot open to_guest pipe")?,
208 from_guest.context("Cannot open from_guest pipe")?,
209 ),
210 Err(error) => {
211 process.kill().unwrap();
213 process.wait().unwrap();
214 panic!("Cannot connect to VM: {error}");
215 }
216 };
217
218 Ok(TestVmSys {
219 test_dir,
220 from_guest_reader: Arc::new(Mutex::new(
221 serde_json::Deserializer::from_reader(BufReader::new(from_guest)).into_iter(),
222 )),
223 to_guest: Arc::new(Mutex::new(to_guest)),
224 control_socket_path,
225 process: Some(process),
226 })
227 }
228
229 pub fn append_config_args(command: &mut Command, test_dir: &Path, cfg: &Config) -> Result<()> {
231 TestVmSys::configure_serial_devices(
232 command,
233 &cfg.console_hardware,
234 &test_dir.join(FROM_GUEST_PIPE),
235 &test_dir.join(TO_GUEST_PIPE),
236 );
237 command.args(["--socket", test_dir.join(CONTROL_PIPE).to_str().unwrap()]);
238
239 if let Some(rootfs_url) = &cfg.rootfs_url {
240 if cfg.rootfs_rw {
241 std::fs::copy(
242 match cfg.rootfs_compressed {
243 true => local_path_from_url(rootfs_url).with_extension("raw"),
244 false => local_path_from_url(rootfs_url),
245 },
246 test_dir.join("rw_rootfs.img"),
247 )
248 .unwrap();
249 TestVmSys::configure_rootfs(
250 command,
251 cfg.o_direct,
252 true,
253 &test_dir.join("rw_rootfs.img"),
254 );
255 } else if cfg.rootfs_compressed {
256 TestVmSys::configure_rootfs(
257 command,
258 cfg.o_direct,
259 false,
260 &local_path_from_url(rootfs_url).with_extension("raw"),
261 );
262 } else {
263 TestVmSys::configure_rootfs(
264 command,
265 cfg.o_direct,
266 false,
267 &local_path_from_url(rootfs_url),
268 );
269 }
270 };
271
272 if let Some(initrd_url) = &cfg.initrd_url {
274 command.arg("--initrd");
275 command.arg(local_path_from_url(initrd_url));
276 }
277
278 command.arg(local_path_from_url(&cfg.kernel_url));
280 Ok(())
281 }
282
283 pub fn crosvm_command(
284 &self,
285 command: &str,
286 mut args: Vec<String>,
287 sudo: bool,
288 ) -> Result<Vec<u8>> {
289 args.push(self.control_socket_path.to_str().unwrap().to_string());
290
291 println!("$ crosvm {} {:?}", command, &args.join(" "));
292
293 let mut cmd = if sudo {
294 let mut cmd = Command::new("sudo");
295 cmd.arg(find_crosvm_binary());
296 cmd
297 } else {
298 Command::new(find_crosvm_binary())
299 };
300
301 cmd.arg(command).args(args);
302
303 let output = cmd.output()?;
304 if !output.status.success() {
305 Err(anyhow!("Command failed with exit code {}", output.status))
306 } else {
307 Ok(output.stdout)
308 }
309 }
310}