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