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