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 use std::ffi::CString;
6 use std::fs::File;
7 use std::fs::OpenOptions;
8 use std::io;
9 use std::io::BufReader;
10 use std::io::Write;
11 use std::os::unix::fs::OpenOptionsExt;
12 use std::path::Path;
13 use std::path::PathBuf;
14 use std::process::Child;
15 use std::process::Command;
16 use std::process::Stdio;
17 use std::sync::Arc;
18 use std::sync::Mutex;
19 use std::time::Duration;
20 use std::time::Instant;
21
22 use anyhow::anyhow;
23 use anyhow::Context;
24 use anyhow::Result;
25 use delegate::wire_format::DelegateMessage;
26 use libc::O_DIRECT;
27 use serde_json::StreamDeserializer;
28 use tempfile::TempDir;
29
30 use crate::utils::find_crosvm_binary;
31 use crate::utils::run_with_status_check;
32 use crate::vm::local_path_from_url;
33 use crate::vm::Config;
34
35 const FROM_GUEST_PIPE: &str = "from_guest";
36 const TO_GUEST_PIPE: &str = "to_guest";
37 const CONTROL_PIPE: &str = "control";
38 const VM_JSON_CONFIG_FILE: &str = "vm.json";
39
40 /// Timeout for communicating with the VM. If we do not hear back, panic so we
41 /// do not block the tests.
42 const VM_COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(10);
43
44 pub(crate) type SerialArgs = Path;
45
46 /// Returns the name of crosvm binary.
binary_name() -> &'static str47 pub fn binary_name() -> &'static str {
48 "crosvm"
49 }
50
51 /// Safe wrapper for libc::mkfifo
mkfifo(path: &Path) -> io::Result<()>52 pub(crate) fn mkfifo(path: &Path) -> io::Result<()> {
53 let cpath = CString::new(path.to_str().unwrap()).unwrap();
54 // SAFETY: no mutable pointer passed to function and the return value is checked.
55 let result = unsafe { libc::mkfifo(cpath.as_ptr(), 0o777) };
56 if result == 0 {
57 Ok(())
58 } else {
59 Err(io::Error::last_os_error())
60 }
61 }
62
63 pub struct TestVmSys {
64 /// Maintain ownership of test_dir until the vm is destroyed.
65 #[allow(dead_code)]
66 pub test_dir: TempDir,
67 pub from_guest_reader: Arc<
68 Mutex<
69 StreamDeserializer<
70 'static,
71 serde_json::de::IoRead<BufReader<std::fs::File>>,
72 DelegateMessage,
73 >,
74 >,
75 >,
76 pub to_guest: Arc<Mutex<File>>,
77 pub control_socket_path: PathBuf,
78 pub process: Option<Child>, // Use `Option` to allow taking the ownership in `Drop::drop()`.
79 }
80
81 impl TestVmSys {
82 // Check if the test file system is a known compatible one. Needs to support features
83 // like O_DIRECT.
check_rootfs_file(rootfs_path: &Path)84 pub fn check_rootfs_file(rootfs_path: &Path) {
85 if let Err(e) = OpenOptions::new()
86 .custom_flags(O_DIRECT)
87 .write(false)
88 .read(true)
89 .open(rootfs_path)
90 {
91 panic!(
92 "File open with O_DIRECT expected to work but did not: {}",
93 e
94 );
95 }
96 }
97
98 // Adds 2 serial devices:
99 // - ttyS0: Console device which prints kernel log / debug output of the delegate binary.
100 // - ttyS1: Serial device attached to the named pipes.
configure_serial_devices( command: &mut Command, stdout_hardware_type: &str, from_guest_pipe: &Path, to_guest_pipe: &Path, )101 fn configure_serial_devices(
102 command: &mut Command,
103 stdout_hardware_type: &str,
104 from_guest_pipe: &Path,
105 to_guest_pipe: &Path,
106 ) {
107 let stdout_serial_option = format!("type=stdout,hardware={},console", stdout_hardware_type);
108 command.args(["--serial", &stdout_serial_option]);
109
110 // Setup channel for communication with the delegate.
111 let serial_params = format!(
112 "type=file,path={},input={},num=2",
113 from_guest_pipe.display(),
114 to_guest_pipe.display()
115 );
116 command.args(["--serial", &serial_params]);
117 }
118
119 /// Configures the VM rootfs to load from the guest_under_test assets.
configure_rootfs(command: &mut Command, o_direct: bool, rw: bool, path: &Path)120 fn configure_rootfs(command: &mut Command, o_direct: bool, rw: bool, path: &Path) {
121 let rootfs_and_option = format!(
122 "{}{}{},root",
123 path.as_os_str().to_str().unwrap(),
124 if o_direct { ",direct=true" } else { "" },
125 if rw { "" } else { ",ro" }
126 );
127 command
128 .args(["--block", &rootfs_and_option])
129 .args(["--params", "init=/bin/delegate"]);
130 }
131
new_generic<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVmSys> where F: FnOnce(&mut Command, &Path, &Config) -> Result<()>,132 pub fn new_generic<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVmSys>
133 where
134 F: FnOnce(&mut Command, &Path, &Config) -> Result<()>,
135 {
136 // Create two named pipes to communicate with the guest.
137 let test_dir = TempDir::new()?;
138 let from_guest_pipe = test_dir.path().join(FROM_GUEST_PIPE);
139 let to_guest_pipe = test_dir.path().join(TO_GUEST_PIPE);
140 mkfifo(&from_guest_pipe)?;
141 mkfifo(&to_guest_pipe)?;
142
143 let control_socket_path = test_dir.path().join(CONTROL_PIPE);
144
145 let mut command = match &cfg.wrapper_cmd {
146 Some(cmd) => {
147 let wrapper_splitted =
148 shlex::split(cmd).context("Failed to parse wrapper command")?;
149 let mut command_tmp = if sudo {
150 let mut command = Command::new("sudo");
151 command.arg(&wrapper_splitted[0]);
152 command
153 } else {
154 Command::new(&wrapper_splitted[0])
155 };
156
157 command_tmp.args(&wrapper_splitted[1..]);
158 command_tmp.arg(find_crosvm_binary());
159 command_tmp
160 }
161 None => {
162 if sudo {
163 let mut command = Command::new("sudo");
164 command.arg(find_crosvm_binary());
165 command
166 } else {
167 Command::new(find_crosvm_binary())
168 }
169 }
170 };
171
172 if let Some(log_file_name) = &cfg.log_file {
173 let log_file_stdout = File::create(log_file_name)?;
174 let log_file_stderr = log_file_stdout.try_clone()?;
175 command.stdout(Stdio::from(log_file_stdout));
176 command.stderr(Stdio::from(log_file_stderr));
177 }
178
179 command.args(["--log-level", cfg.log_level.as_str()]);
180 command.args(["run"]);
181
182 f(&mut command, test_dir.path(), &cfg)?;
183
184 command.args(&cfg.extra_args);
185
186 println!("$ {:?}", command);
187 let mut process = command.spawn()?;
188
189 // Open pipes. Apply timeout to `to_guest` and `from_guest` since it will block until crosvm
190 // opens the other end.
191 let start = Instant::now();
192 let run_result = run_with_status_check(
193 move || (File::create(to_guest_pipe), File::open(from_guest_pipe)),
194 Duration::from_millis(200),
195 || {
196 if start.elapsed() > VM_COMMUNICATION_TIMEOUT {
197 return false;
198 }
199 if let Some(wait_result) = process.try_wait().unwrap() {
200 println!("crosvm unexpectedly exited: {:?}", wait_result);
201 false
202 } else {
203 true
204 }
205 },
206 );
207
208 let (to_guest, from_guest) = match run_result {
209 Ok((to_guest, from_guest)) => (
210 to_guest.context("Cannot open to_guest pipe")?,
211 from_guest.context("Cannot open from_guest pipe")?,
212 ),
213 Err(error) => {
214 // Kill the crosvm process if we cannot connect in time.
215 process.kill().unwrap();
216 process.wait().unwrap();
217 panic!("Cannot connect to VM: {}", error);
218 }
219 };
220
221 Ok(TestVmSys {
222 test_dir,
223 from_guest_reader: Arc::new(Mutex::new(
224 serde_json::Deserializer::from_reader(BufReader::new(from_guest)).into_iter(),
225 )),
226 to_guest: Arc::new(Mutex::new(to_guest)),
227 control_socket_path,
228 process: Some(process),
229 })
230 }
231
232 // Generates a config file from cfg and appends the command to use the config file.
append_config_args(command: &mut Command, test_dir: &Path, cfg: &Config) -> Result<()>233 pub fn append_config_args(command: &mut Command, test_dir: &Path, cfg: &Config) -> Result<()> {
234 TestVmSys::configure_serial_devices(
235 command,
236 &cfg.console_hardware,
237 &test_dir.join(FROM_GUEST_PIPE),
238 &test_dir.join(TO_GUEST_PIPE),
239 );
240 command.args(["--socket", test_dir.join(CONTROL_PIPE).to_str().unwrap()]);
241
242 if let Some(rootfs_url) = &cfg.rootfs_url {
243 if cfg.rootfs_rw {
244 std::fs::copy(
245 match cfg.rootfs_compressed {
246 true => local_path_from_url(rootfs_url).with_extension("raw"),
247 false => local_path_from_url(rootfs_url),
248 },
249 test_dir.join("rw_rootfs.img"),
250 )
251 .unwrap();
252 TestVmSys::configure_rootfs(
253 command,
254 cfg.o_direct,
255 true,
256 &test_dir.join("rw_rootfs.img"),
257 );
258 } else if cfg.rootfs_compressed {
259 TestVmSys::configure_rootfs(
260 command,
261 cfg.o_direct,
262 false,
263 &local_path_from_url(rootfs_url).with_extension("raw"),
264 );
265 } else {
266 TestVmSys::configure_rootfs(
267 command,
268 cfg.o_direct,
269 false,
270 &local_path_from_url(rootfs_url),
271 );
272 }
273 };
274
275 // Set initrd if being requested
276 if let Some(initrd_url) = &cfg.initrd_url {
277 command.arg("--initrd");
278 command.arg(local_path_from_url(initrd_url));
279 }
280
281 // Set kernel as the last argument.
282 command.arg(local_path_from_url(&cfg.kernel_url));
283 Ok(())
284 }
285
286 /// Generate a JSON configuration file for `cfg` and returns its path.
generate_json_config_file(test_dir: &Path, cfg: &Config) -> Result<PathBuf>287 fn generate_json_config_file(test_dir: &Path, cfg: &Config) -> Result<PathBuf> {
288 let config_file_path = test_dir.join(VM_JSON_CONFIG_FILE);
289 let mut config_file = File::create(&config_file_path)?;
290
291 writeln!(config_file, "{{")?;
292 writeln!(
293 config_file,
294 r#""kernel": "{}""#,
295 local_path_from_url(&cfg.kernel_url).display()
296 )?;
297 if let Some(initrd_url) = &cfg.initrd_url {
298 writeln!(
299 config_file,
300 r#"",initrd": "{}""#,
301 local_path_from_url(initrd_url)
302 .to_str()
303 .context("invalid initrd path")?
304 )?;
305 };
306 writeln!(
307 config_file,
308 r#"
309 ,"socket": "{}",
310 "params": [ "init=/bin/delegate" ],
311 "serial": [
312 {{
313 "type": "stdout"
314 }},
315 {{
316 "type": "file",
317 "path": "{}",
318 "input": "{}",
319 "num": 2
320 }}
321 ]
322 "#,
323 test_dir.join(CONTROL_PIPE).display(),
324 test_dir.join(FROM_GUEST_PIPE).display(),
325 test_dir.join(TO_GUEST_PIPE).display(),
326 )?;
327
328 if let Some(rootfs_url) = &cfg.rootfs_url {
329 writeln!(
330 config_file,
331 r#"
332 ,"block": [
333 {{
334 "path": "{}",
335 "ro": true,
336 "root": true,
337 "direct": {}
338 }}
339 ]
340 "#,
341 local_path_from_url(rootfs_url)
342 .to_str()
343 .context("invalid rootfs path")?,
344 cfg.o_direct,
345 )?;
346 };
347
348 writeln!(config_file, "}}")?;
349
350 Ok(config_file_path)
351 }
352
353 // Generates a config file from cfg and appends the command to use the config file.
append_config_file_arg( command: &mut Command, test_dir: &Path, cfg: &Config, ) -> Result<()>354 pub fn append_config_file_arg(
355 command: &mut Command,
356 test_dir: &Path,
357 cfg: &Config,
358 ) -> Result<()> {
359 let config_file_path = TestVmSys::generate_json_config_file(test_dir, cfg)?;
360 command.args(["--cfg", config_file_path.to_str().unwrap()]);
361
362 Ok(())
363 }
364
crosvm_command( &self, command: &str, mut args: Vec<String>, sudo: bool, ) -> Result<Vec<u8>>365 pub fn crosvm_command(
366 &self,
367 command: &str,
368 mut args: Vec<String>,
369 sudo: bool,
370 ) -> Result<Vec<u8>> {
371 args.push(self.control_socket_path.to_str().unwrap().to_string());
372
373 println!("$ crosvm {} {:?}", command, &args.join(" "));
374
375 let mut cmd = if sudo {
376 let mut cmd = Command::new("sudo");
377 cmd.arg(find_crosvm_binary());
378 cmd
379 } else {
380 Command::new(find_crosvm_binary())
381 };
382
383 cmd.arg(command).args(args);
384
385 let output = cmd.output()?;
386 if !output.status.success() {
387 Err(anyhow!("Command failed with exit code {}", output.status))
388 } else {
389 Ok(output.stdout)
390 }
391 }
392 }
393