xref: /aosp_15_r20/external/crosvm/e2e_tests/fixture/src/sys/linux.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
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