• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 (to_guest, from_guest) = match 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             Ok((to_guest, from_guest)) => (
208                 to_guest.context("Cannot open to_guest pipe")?,
209                 from_guest.context("Cannot open from_guest pipe")?,
210             ),
211             Err(error) => {
212                 // Kill the crosvm process if we cannot connect in time.
213                 process.kill().unwrap();
214                 process.wait().unwrap();
215                 panic!("Cannot connect to VM: {}", error);
216             }
217         };
218 
219         Ok(TestVmSys {
220             test_dir,
221             from_guest_reader: Arc::new(Mutex::new(
222                 serde_json::Deserializer::from_reader(BufReader::new(from_guest)).into_iter(),
223             )),
224             to_guest: Arc::new(Mutex::new(to_guest)),
225             control_socket_path,
226             process: Some(process),
227         })
228     }
229 
230     // 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<()>231     pub fn append_config_args(command: &mut Command, test_dir: &Path, cfg: &Config) -> Result<()> {
232         TestVmSys::configure_serial_devices(
233             command,
234             &cfg.console_hardware,
235             &test_dir.join(FROM_GUEST_PIPE),
236             &test_dir.join(TO_GUEST_PIPE),
237         );
238         command.args(["--socket", test_dir.join(CONTROL_PIPE).to_str().unwrap()]);
239 
240         if let Some(rootfs_url) = &cfg.rootfs_url {
241             if cfg.rootfs_rw {
242                 std::fs::copy(
243                     match cfg.rootfs_compressed {
244                         true => local_path_from_url(rootfs_url).with_extension("raw"),
245                         false => local_path_from_url(rootfs_url),
246                     },
247                     test_dir.join("rw_rootfs.img"),
248                 )
249                 .unwrap();
250                 TestVmSys::configure_rootfs(
251                     command,
252                     cfg.o_direct,
253                     true,
254                     &test_dir.join("rw_rootfs.img"),
255                 );
256             } else if cfg.rootfs_compressed {
257                 TestVmSys::configure_rootfs(
258                     command,
259                     cfg.o_direct,
260                     false,
261                     &local_path_from_url(rootfs_url).with_extension("raw"),
262                 );
263             } else {
264                 TestVmSys::configure_rootfs(
265                     command,
266                     cfg.o_direct,
267                     false,
268                     &local_path_from_url(rootfs_url),
269                 );
270             }
271         };
272 
273         // Set initrd if being requested
274         if let Some(initrd_url) = &cfg.initrd_url {
275             command.arg("--initrd");
276             command.arg(local_path_from_url(initrd_url));
277         }
278 
279         // Set kernel as the last argument.
280         command.arg(local_path_from_url(&cfg.kernel_url));
281         Ok(())
282     }
283 
284     /// Generate a JSON configuration file for `cfg` and returns its path.
generate_json_config_file(test_dir: &Path, cfg: &Config) -> Result<PathBuf>285     fn generate_json_config_file(test_dir: &Path, cfg: &Config) -> Result<PathBuf> {
286         let config_file_path = test_dir.join(VM_JSON_CONFIG_FILE);
287         let mut config_file = File::create(&config_file_path)?;
288 
289         writeln!(config_file, "{{")?;
290         writeln!(
291             config_file,
292             r#""kernel": "{}""#,
293             local_path_from_url(&cfg.kernel_url).display()
294         )?;
295         if let Some(initrd_url) = &cfg.initrd_url {
296             writeln!(
297                 config_file,
298                 r#"",initrd": "{}""#,
299                 local_path_from_url(initrd_url)
300                     .to_str()
301                     .context("invalid initrd path")?
302             )?;
303         };
304         writeln!(
305             config_file,
306             r#"
307         ,"socket": "{}",
308         "params": [ "init=/bin/delegate" ],
309         "serial": [
310           {{
311             "type": "stdout"
312           }},
313           {{
314             "type": "file",
315             "path": "{}",
316             "input": "{}",
317             "num": 2
318           }}
319         ]
320         "#,
321             test_dir.join(CONTROL_PIPE).display(),
322             test_dir.join(FROM_GUEST_PIPE).display(),
323             test_dir.join(TO_GUEST_PIPE).display(),
324         )?;
325 
326         if let Some(rootfs_url) = &cfg.rootfs_url {
327             writeln!(
328                 config_file,
329                 r#"
330                 ,"block": [
331                     {{
332                       "path": "{}",
333                       "ro": true,
334                       "root": true,
335                       "direct": {}
336                     }}
337                   ]
338                   "#,
339                 local_path_from_url(rootfs_url)
340                     .to_str()
341                     .context("invalid rootfs path")?,
342                 cfg.o_direct,
343             )?;
344         };
345 
346         writeln!(config_file, "}}")?;
347 
348         Ok(config_file_path)
349     }
350 
351     // 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<()>352     pub fn append_config_file_arg(
353         command: &mut Command,
354         test_dir: &Path,
355         cfg: &Config,
356     ) -> Result<()> {
357         let config_file_path = TestVmSys::generate_json_config_file(test_dir, cfg)?;
358         command.args(["--cfg", config_file_path.to_str().unwrap()]);
359 
360         Ok(())
361     }
362 
crosvm_command( &self, command: &str, mut args: Vec<String>, sudo: bool, ) -> Result<Vec<u8>>363     pub fn crosvm_command(
364         &self,
365         command: &str,
366         mut args: Vec<String>,
367         sudo: bool,
368     ) -> Result<Vec<u8>> {
369         args.push(self.control_socket_path.to_str().unwrap().to_string());
370 
371         println!("$ crosvm {} {:?}", command, &args.join(" "));
372 
373         let mut cmd = if sudo {
374             let mut cmd = Command::new("sudo");
375             cmd.arg(find_crosvm_binary());
376             cmd
377         } else {
378             Command::new(find_crosvm_binary())
379         };
380 
381         cmd.arg(command).args(args);
382 
383         let output = cmd.output()?;
384         if !output.status.success() {
385             Err(anyhow!("Command failed with exit code {}", output.status))
386         } else {
387             Ok(output.stdout)
388         }
389     }
390 }
391