xref: /aosp_15_r20/external/crosvm/e2e_tests/fixture/src/sys/windows.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 // TODO(b/262270352): This file is build-only upstream as crosvm.exe cannot yet
6 // start a VM on windows. Enable e2e tests on windows and remove this comment.
7 
8 use std::env;
9 use std::fs::File;
10 use std::fs::OpenOptions;
11 use std::io::BufReader;
12 use std::io::Write;
13 use std::path::Path;
14 use std::path::PathBuf;
15 use std::process::Child;
16 use std::process::Command;
17 use std::sync::Arc;
18 use std::sync::Mutex;
19 use std::time::Duration;
20 
21 use anyhow::Context;
22 use anyhow::Result;
23 use base::named_pipes;
24 use base::PipeConnection;
25 use delegate::wire_format::DelegateMessage;
26 use rand::Rng;
27 use serde_json::StreamDeserializer;
28 
29 use crate::utils::find_crosvm_binary;
30 use crate::vm::local_path_from_url;
31 use crate::vm::Config;
32 
33 const GUEST_EARLYCON: &str = "guest_earlycon.log";
34 const GUEST_CONSOLE: &str = "guest_latecon.log";
35 const HYPERVISOR_LOG: &str = "hypervisor.log";
36 const VM_JSON_CONFIG_FILE: &str = "vm.json";
37 // SLEEP_TIMEOUT is somewhat arbitrarily chosen by looking at a few downstream
38 // presubmit runs.
39 const SLEEP_TIMEOUT: Duration = Duration::from_millis(500);
40 // RETRY_COUNT is somewhat arbitrarily chosen by looking at a few downstream
41 // presubmit runs.
42 const RETRY_COUNT: u16 = 600;
43 
44 pub struct SerialArgs {
45     // This pipe is used to communicate to/from guest.
46     from_guest_pipe: PathBuf,
47     logs_dir: PathBuf,
48 }
49 
50 /// Returns the name of crosvm binary.
binary_name() -> &'static str51 pub fn binary_name() -> &'static str {
52     "crosvm.exe"
53 }
54 
55 // Generates random pipe name in device folder.
generate_pipe_name() -> String56 fn generate_pipe_name() -> String {
57     format!(
58         r"\\.\pipe\test-ipc-pipe-name.rand{}",
59         rand::thread_rng().gen::<u64>(),
60     )
61 }
62 
63 // Gets custom hypervisor from `CROSVM_TEST_HYPERVISOR` environment variable or
64 // return `whpx` as default.
get_hypervisor() -> String65 fn get_hypervisor() -> String {
66     env::var("CROSVM_TEST_HYPERVISOR").unwrap_or("whpx".to_string())
67 }
68 
69 // If the hypervisor is haxm derivative, then returns `userspace` else returns
70 // None.
get_irqchip(hypervisor: &str) -> Option<String>71 fn get_irqchip(hypervisor: &str) -> Option<String> {
72     if hypervisor == "haxm" || hypervisor == "ghaxm" {
73         Some("userspace".to_string())
74     } else {
75         None
76     }
77 }
78 
79 // Ruturns hypervisor related args.
get_hypervisor_args() -> Vec<String>80 fn get_hypervisor_args() -> Vec<String> {
81     let hypervisor = get_hypervisor();
82     let mut args = if let Some(irqchip) = get_irqchip(&hypervisor) {
83         vec!["--irqchip".to_owned(), irqchip]
84     } else {
85         vec![]
86     };
87     args.extend_from_slice(&["--hypervisor".to_owned(), hypervisor]);
88     args
89 }
90 
91 // Dumps logs found in `logs_dir` created by crosvm run.
dump_logs(logs_dir: &str)92 fn dump_logs(logs_dir: &str) {
93     let dir = Path::new(logs_dir);
94     if dir.is_dir() {
95         for entry in std::fs::read_dir(dir).unwrap() {
96             let entry = entry.unwrap();
97             let path = entry.path();
98             if !path.is_dir() {
99                 let data = std::fs::read_to_string(&path)
100                     .unwrap_or_else(|e| panic!("Unable to read file {:?}: {:?}", &path, e));
101                 eprintln!("---------- {:?}", &path);
102                 eprintln!("{}", &data);
103                 eprintln!("---------- {:?}", &path);
104             }
105         }
106     }
107 }
108 
create_client_pipe_helper(from_guest_pipe: &str, logs_dir: &str) -> PipeConnection109 fn create_client_pipe_helper(from_guest_pipe: &str, logs_dir: &str) -> PipeConnection {
110     for _ in 0..RETRY_COUNT {
111         std::thread::sleep(SLEEP_TIMEOUT);
112         // Open pipes. Panic if we cannot connect after a timeout.
113         if let Ok(pipe) = named_pipes::create_client_pipe(
114             from_guest_pipe,
115             &named_pipes::FramingMode::Byte,
116             &named_pipes::BlockingMode::Wait,
117             false,
118         ) {
119             return pipe;
120         }
121     }
122 
123     dump_logs(logs_dir);
124     panic!("Failed to open pipe from guest");
125 }
126 
127 pub struct TestVmSys {
128     pub(crate) from_guest_reader: Arc<
129         Mutex<
130             StreamDeserializer<
131                 'static,
132                 serde_json::de::IoRead<BufReader<PipeConnection>>,
133                 DelegateMessage,
134             >,
135         >,
136     >,
137     pub(crate) to_guest: Arc<Mutex<PipeConnection>>,
138     pub(crate) process: Option<Child>, /* Use `Option` to allow taking the ownership in
139                                         * `Drop::drop()`. */
140 }
141 
142 impl TestVmSys {
143     // Check if the test file system is a known compatible one.
check_rootfs_file(rootfs_path: &Path)144     pub fn check_rootfs_file(rootfs_path: &Path) {
145         // Check if the test file system is a known compatible one.
146         if let Err(e) = OpenOptions::new().write(false).read(true).open(rootfs_path) {
147             panic!("File open expected to work but did not: {}", e);
148         }
149     }
150 
151     // Adds 2 serial devices:
152     // - ttyS0: Console device which prints kernel log / debug output of the delegate binary.
153     // - ttyS1: Serial device attached to the named pipes.
configure_serial_devices( command: &mut Command, stdout_hardware_type: &str, from_guest_pipe: &Path, logs_dir: &Path, )154     fn configure_serial_devices(
155         command: &mut Command,
156         stdout_hardware_type: &str,
157         from_guest_pipe: &Path,
158         logs_dir: &Path,
159     ) {
160         let earlycon_path = Path::new(logs_dir).join(GUEST_EARLYCON);
161         let earlycon_str = earlycon_path.to_str().unwrap();
162 
163         command.args([
164             r"--serial",
165             &format!("hardware=serial,num=1,type=file,path={earlycon_str},earlycon=true"),
166         ]);
167 
168         let console_path = Path::new(logs_dir).join(GUEST_CONSOLE);
169         let console_str = console_path.to_str().unwrap();
170         command.args([
171             r"--serial",
172             &format!(
173                 "hardware={stdout_hardware_type},num=1,type=file,path={console_str},console=true"
174             ),
175         ]);
176 
177         // Setup channel for communication with the delegate.
178         let serial_params = format!(
179             "hardware=serial,type=namedpipe,path={},num=2",
180             from_guest_pipe.display(),
181         );
182         command.args(["--serial", &serial_params]);
183     }
184 
185     /// Configures the VM rootfs to load from the guest_under_test assets.
configure_rootfs(command: &mut Command, _o_direct: bool, path: &Path)186     fn configure_rootfs(command: &mut Command, _o_direct: bool, path: &Path) {
187         let rootfs_and_option = format!(
188             "{},ro,root,sparse=false",
189             path.as_os_str().to_str().unwrap(),
190         );
191         command.args(["--root", &rootfs_and_option]).args([
192             "--params",
193             "init=/bin/delegate noxsaves noxsave nopat nopti tsc=reliable",
194         ]);
195     }
196 
new_generic<F>(f: F, cfg: Config, _sudo: bool) -> Result<TestVmSys> where F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,197     pub fn new_generic<F>(f: F, cfg: Config, _sudo: bool) -> Result<TestVmSys>
198     where
199         F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,
200     {
201         let logs_dir = "emulator_logs";
202         let mut logs_path = PathBuf::new();
203         logs_path.push(logs_dir);
204         std::fs::create_dir_all(logs_dir)?;
205         // Create named pipe to communicate with the guest.
206         let from_guest_path = generate_pipe_name();
207         let from_guest_pipe = Path::new(&from_guest_path);
208 
209         let mut command = Command::new(find_crosvm_binary());
210         command.args(["--log-level", "INFO", "run-mp"]);
211 
212         f(
213             &mut command,
214             &SerialArgs {
215                 from_guest_pipe: from_guest_pipe.to_path_buf(),
216                 logs_dir: logs_path,
217             },
218             &cfg,
219         )?;
220 
221         let hypervisor_log_path = Path::new(logs_dir).join(HYPERVISOR_LOG);
222         let hypervisor_log_str = hypervisor_log_path.to_str().unwrap();
223         command.args([
224             "--logs-directory",
225             logs_dir,
226             "--kernel-log-file",
227             hypervisor_log_str,
228         ]);
229         command.args(get_hypervisor_args());
230         command.args(cfg.extra_args);
231 
232         println!("Running command: {:?}", command);
233 
234         let process = Some(command.spawn().unwrap());
235 
236         let to_guest = create_client_pipe_helper(&from_guest_path, logs_dir);
237         let from_guest_reader = BufReader::new(to_guest.try_clone().unwrap());
238 
239         Ok(TestVmSys {
240             from_guest_reader: Arc::new(Mutex::new(
241                 serde_json::Deserializer::from_reader(from_guest_reader).into_iter(),
242             )),
243             to_guest: Arc::new(Mutex::new(to_guest)),
244             process,
245         })
246     }
247 
248     // Generates a config file from cfg and appends the command to use the config file.
append_config_args( command: &mut Command, serial_args: &SerialArgs, cfg: &Config, ) -> Result<()>249     pub fn append_config_args(
250         command: &mut Command,
251         serial_args: &SerialArgs,
252         cfg: &Config,
253     ) -> Result<()> {
254         TestVmSys::configure_serial_devices(
255             command,
256             &cfg.console_hardware,
257             &serial_args.from_guest_pipe,
258             &serial_args.logs_dir,
259         );
260         if let Some(rootfs_url) = &cfg.rootfs_url {
261             TestVmSys::configure_rootfs(command, cfg.o_direct, &local_path_from_url(rootfs_url));
262         };
263 
264         // Set initrd if being requested
265         if let Some(initrd_url) = &cfg.initrd_url {
266             command.arg("--initrd");
267             command.arg(local_path_from_url(initrd_url));
268         }
269 
270         // Set kernel as the last argument.
271         command.arg(local_path_from_url(&cfg.kernel_url));
272 
273         Ok(())
274     }
275 
276     /// Generate a JSON configuration file for `cfg` and returns its path.
generate_json_config_file( from_guest_pipe: &Path, logs_path: &Path, cfg: &Config, ) -> Result<PathBuf>277     fn generate_json_config_file(
278         from_guest_pipe: &Path,
279         logs_path: &Path,
280         cfg: &Config,
281     ) -> Result<PathBuf> {
282         let config_file_path = logs_path.join(VM_JSON_CONFIG_FILE);
283         let mut config_file = File::create(&config_file_path)?;
284 
285         writeln!(config_file, "{{")?;
286 
287         writeln!(
288             config_file,
289             r#"
290               "params": [ "init=/bin/delegate noxsaves noxsave nopat nopti tsc=reliable" ],
291               "serial": [
292                 {{
293                     "type": "file",
294                     "hardware": "serial",
295                     "num": "1",
296                     "path": "{}",
297                     "earlycon": "true"
298                 }},
299                 {{
300                     "type": "file",
301                     "path": "{}",
302                     "hardware": "serial",
303                     "num": "1",
304                     "console": "true"
305                    }},
306                 {{
307                     "hardware": "serial",
308                     "num": "2",
309                     "type": "namedpipe",
310                     "path": "{}",
311                 }},
312               ]
313             }}
314             "#,
315             logs_path.join(GUEST_EARLYCON).display(),
316             logs_path.join(GUEST_CONSOLE).display(),
317             from_guest_pipe.display()
318         )?;
319 
320         if let Some(rootfs_url) = &cfg.rootfs_url {
321             writeln!(
322                 config_file,
323                 r#"
324                 ,"root": [
325                 {{
326                   "path": "{}",
327                   "ro": true,
328                   "root": true,
329                   "sparse": false
330                 }}
331               ]
332                   "#,
333                 local_path_from_url(rootfs_url)
334                     .to_str()
335                     .context("invalid rootfs path")?,
336             )?;
337         };
338         if let Some(initrd_url) = &cfg.initrd_url {
339             writeln!(
340                 config_file,
341                 r#"",initrd": "{}""#,
342                 local_path_from_url(initrd_url)
343                     .to_str()
344                     .context("invalid initrd path")?
345             )?;
346         };
347 
348         writeln!(
349             config_file,
350             r#"
351         ,"logs-directory": "{}",
352         "kernel-log-file": "{},
353         "hypervisor": "{}"
354         {},
355         {}"#,
356             logs_path.display(),
357             logs_path.join(HYPERVISOR_LOG).display(),
358             get_hypervisor(),
359             local_path_from_url(&cfg.kernel_url).display(),
360             &get_irqchip(&get_hypervisor()).map_or("".to_owned(), |irqchip| format!(
361                 r#","irqchip": "{}""#,
362                 irqchip
363             ))
364         )?;
365 
366         writeln!(config_file, "}}")?;
367 
368         Ok(config_file_path)
369     }
370 
371     // Generates a config file from cfg and appends the command to use the config file.
append_config_file_arg( command: &mut Command, serial_args: &SerialArgs, cfg: &Config, ) -> Result<()>372     pub fn append_config_file_arg(
373         command: &mut Command,
374         serial_args: &SerialArgs,
375         cfg: &Config,
376     ) -> Result<()> {
377         let config_file_path = TestVmSys::generate_json_config_file(
378             &serial_args.from_guest_pipe,
379             &serial_args.logs_dir,
380             cfg,
381         )?;
382         command.args(["--cfg", config_file_path.to_str().unwrap()]);
383 
384         Ok(())
385     }
386 
crosvm_command( &mut self, _command: &str, mut _args: Vec<String>, _sudo: bool, ) -> Result<Vec<u8>>387     pub fn crosvm_command(
388         &mut self,
389         _command: &str,
390         mut _args: Vec<String>,
391         _sudo: bool,
392     ) -> Result<Vec<u8>> {
393         unimplemented!()
394     }
395 }
396