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