1 // Copyright 2020 The Chromium OS Authors. All rights reserved.
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 libc::O_DIRECT;
6 use std::ffi::CString;
7 use std::fs::File;
8 use std::fs::OpenOptions;
9 use std::io::{self, BufRead, BufReader, Write};
10 use std::os::unix::fs::OpenOptionsExt;
11 use std::path::{Path, PathBuf};
12 use std::process::Command;
13 use std::sync::mpsc::sync_channel;
14 use std::sync::Once;
15 use std::thread;
16 use std::time::Duration;
17 use std::{env, process::Child};
18
19 use anyhow::{anyhow, Result};
20 use base::syslog;
21 use tempfile::TempDir;
22
23 const PREBUILT_URL: &str = "https://storage.googleapis.com/chromeos-localmirror/distfiles";
24
25 #[cfg(target_arch = "x86_64")]
26 const ARCH: &str = "x86_64";
27 #[cfg(target_arch = "arm")]
28 const ARCH: &str = "arm";
29 #[cfg(target_arch = "aarch64")]
30 const ARCH: &str = "aarch64";
31
32 /// Timeout for communicating with the VM. If we do not hear back, panic so we
33 /// do not block the tests.
34 const VM_COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(10);
35
prebuilt_version() -> &'static str36 fn prebuilt_version() -> &'static str {
37 include_str!("../guest_under_test/PREBUILT_VERSION").trim()
38 }
39
kernel_prebuilt_url() -> String40 fn kernel_prebuilt_url() -> String {
41 format!(
42 "{}/crosvm-testing-bzimage-{}-{}",
43 PREBUILT_URL,
44 ARCH,
45 prebuilt_version()
46 )
47 }
48
rootfs_prebuilt_url() -> String49 fn rootfs_prebuilt_url() -> String {
50 format!(
51 "{}/crosvm-testing-rootfs-{}-{}",
52 PREBUILT_URL,
53 ARCH,
54 prebuilt_version()
55 )
56 }
57
58 /// The kernel bzImage is stored next to the test executable, unless overridden by
59 /// CROSVM_CARGO_TEST_KERNEL_BINARY
kernel_path() -> PathBuf60 fn kernel_path() -> PathBuf {
61 match env::var("CROSVM_CARGO_TEST_KERNEL_BINARY") {
62 Ok(value) => PathBuf::from(value),
63 Err(_) => env::current_exe()
64 .unwrap()
65 .parent()
66 .unwrap()
67 .join("bzImage"),
68 }
69 }
70
71 /// The rootfs image is stored next to the test executable, unless overridden by
72 /// CROSVM_CARGO_TEST_ROOTFS_IMAGE
rootfs_path() -> PathBuf73 fn rootfs_path() -> PathBuf {
74 match env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE") {
75 Ok(value) => PathBuf::from(value),
76 Err(_) => env::current_exe().unwrap().parent().unwrap().join("rootfs"),
77 }
78 }
79
80 /// The crosvm binary is expected to be alongside to the integration tests
81 /// binary. Alternatively in the parent directory (cargo will put the
82 /// test binary in target/debug/deps/ but the crosvm binary in target/debug).
find_crosvm_binary() -> PathBuf83 fn find_crosvm_binary() -> PathBuf {
84 let exe_dir = env::current_exe().unwrap().parent().unwrap().to_path_buf();
85 let first = exe_dir.join("crosvm");
86 if first.exists() {
87 return first;
88 }
89 let second = exe_dir.parent().unwrap().join("crosvm");
90 if second.exists() {
91 return second;
92 }
93 panic!("Cannot find ./crosvm or ../crosvm alongside test binary.");
94 }
95
96 /// Safe wrapper for libc::mkfifo
mkfifo(path: &Path) -> io::Result<()>97 fn mkfifo(path: &Path) -> io::Result<()> {
98 let cpath = CString::new(path.to_str().unwrap()).unwrap();
99 let result = unsafe { libc::mkfifo(cpath.as_ptr(), 0o777) };
100 if result == 0 {
101 Ok(())
102 } else {
103 Err(io::Error::last_os_error())
104 }
105 }
106
107 /// Run the provided closure, but panic if it does not complete until the timeout has passed.
108 /// We should panic here, as we cannot gracefully stop the closure from running.
panic_on_timeout<F, U>(closure: F, timeout: Duration) -> U where F: FnOnce() -> U + Send + 'static, U: Send + 'static,109 fn panic_on_timeout<F, U>(closure: F, timeout: Duration) -> U
110 where
111 F: FnOnce() -> U + Send + 'static,
112 U: Send + 'static,
113 {
114 let (tx, rx) = sync_channel::<()>(1);
115 let handle = thread::spawn(move || {
116 let result = closure();
117 tx.send(()).unwrap();
118 result
119 });
120 rx.recv_timeout(timeout)
121 .expect("Operation timed out or closure paniced.");
122 handle.join().unwrap()
123 }
124
download_file(url: &str, destination: &Path) -> Result<()>125 fn download_file(url: &str, destination: &Path) -> Result<()> {
126 let status = Command::new("curl")
127 .arg("--fail")
128 .arg("--location")
129 .args(&["--output", destination.to_str().unwrap()])
130 .arg(url)
131 .status();
132 match status {
133 Ok(exit_code) => {
134 if !exit_code.success() {
135 Err(anyhow!("Cannot download {}", url))
136 } else {
137 Ok(())
138 }
139 }
140 Err(error) => Err(anyhow!(error)),
141 }
142 }
143
crosvm_command(command: &str, args: &[&str]) -> Result<()>144 fn crosvm_command(command: &str, args: &[&str]) -> Result<()> {
145 println!("$ crosvm {} {:?}", command, &args.join(" "));
146 let status = Command::new(find_crosvm_binary())
147 .arg(command)
148 .args(args)
149 .status()?;
150
151 if !status.success() {
152 Err(anyhow!("Command failed with exit code {}", status))
153 } else {
154 Ok(())
155 }
156 }
157
158 /// Test fixture to spin up a VM running a guest that can be communicated with.
159 ///
160 /// After creation, commands can be sent via exec_in_guest. The VM is stopped
161 /// when this instance is dropped.
162 pub struct TestVm {
163 /// Maintain ownership of test_dir until the vm is destroyed.
164 #[allow(dead_code)]
165 test_dir: TempDir,
166 from_guest_reader: BufReader<File>,
167 to_guest: File,
168 control_socket_path: PathBuf,
169 process: Child,
170 debug: bool,
171 }
172
173 impl TestVm {
174 /// Magic line sent by the delegate binary when the guest is ready.
175 const MAGIC_LINE: &'static str = "\x05Ready";
176
177 /// Downloads prebuilts if needed.
initialize_once()178 fn initialize_once() {
179 if let Err(e) = syslog::init() {
180 panic!("failed to initiailize syslog: {}", e);
181 }
182
183 // It's possible the prebuilts downloaded by crosvm-9999.ebuild differ
184 // from the version that crosvm was compiled for.
185 if let Ok(value) = env::var("CROSVM_CARGO_TEST_PREBUILT_VERSION") {
186 if value != prebuilt_version() {
187 panic!(
188 "Environment provided prebuilts are version {}, but crosvm was compiled \
189 for prebuilt version {}. Did you update PREBUILT_VERSION everywhere?",
190 value,
191 prebuilt_version()
192 );
193 }
194 }
195
196 let kernel_path = kernel_path();
197 if env::var("CROSVM_CARGO_TEST_KERNEL_BINARY").is_err() {
198 if !kernel_path.exists() {
199 println!("Downloading kernel prebuilt:");
200 download_file(&kernel_prebuilt_url(), &kernel_path).unwrap();
201 }
202 }
203 assert!(kernel_path.exists(), "{:?} does not exist", kernel_path);
204
205 let rootfs_path = rootfs_path();
206 if env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE").is_err() {
207 if !rootfs_path.exists() {
208 println!("Downloading rootfs prebuilt:");
209 download_file(&rootfs_prebuilt_url(), &rootfs_path).unwrap();
210 }
211 }
212 assert!(rootfs_path.exists(), "{:?} does not exist", rootfs_path);
213
214 // Check if the test file system is a known compatible one. Needs to support features like O_DIRECT.
215 if let Err(e) = OpenOptions::new()
216 .custom_flags(O_DIRECT)
217 .write(false)
218 .read(true)
219 .open(rootfs_path)
220 {
221 panic!(
222 "File open with O_DIRECT expected to work but did not: {}",
223 e
224 );
225 }
226 }
227
228 // Adds 2 serial devices:
229 // - ttyS0: Console device which prints kernel log / debug output of the
230 // delegate binary.
231 // - ttyS1: Serial device attached to the named pipes.
configure_serial_devices( command: &mut Command, from_guest_pipe: &Path, to_guest_pipe: &Path, )232 fn configure_serial_devices(
233 command: &mut Command,
234 from_guest_pipe: &Path,
235 to_guest_pipe: &Path,
236 ) {
237 command.args(&["--serial", "type=syslog"]);
238
239 // Setup channel for communication with the delegate.
240 let serial_params = format!(
241 "type=file,path={},input={},num=2",
242 from_guest_pipe.display(),
243 to_guest_pipe.display()
244 );
245 command.args(&["--serial", &serial_params]);
246 }
247
248 /// Configures the VM kernel and rootfs to load from the guest_under_test assets.
configure_kernel(command: &mut Command, o_direct: bool)249 fn configure_kernel(command: &mut Command, o_direct: bool) {
250 let rootfs_and_option = format!(
251 "{}{}",
252 rootfs_path().to_str().unwrap(),
253 if o_direct { ",o_direct=true" } else { "" }
254 );
255 command
256 .args(&["--root", &rootfs_and_option])
257 .args(&["--params", "init=/bin/delegate"])
258 .arg(kernel_path());
259 }
260
261 /// Instanciate a new crosvm instance. The first call will trigger the download of prebuilt
262 /// files if necessary.
new(additional_arguments: &[&str], debug: bool, o_direct: bool) -> Result<TestVm>263 pub fn new(additional_arguments: &[&str], debug: bool, o_direct: bool) -> Result<TestVm> {
264 static PREP_ONCE: Once = Once::new();
265 PREP_ONCE.call_once(TestVm::initialize_once);
266
267 // Create two named pipes to communicate with the guest.
268 let test_dir = TempDir::new()?;
269 let from_guest_pipe = test_dir.path().join("from_guest");
270 let to_guest_pipe = test_dir.path().join("to_guest");
271 mkfifo(&from_guest_pipe)?;
272 mkfifo(&to_guest_pipe)?;
273
274 let control_socket_path = test_dir.path().join("control");
275
276 let mut command = Command::new(find_crosvm_binary());
277 command.args(&["run", "--disable-sandbox"]);
278 TestVm::configure_serial_devices(&mut command, &from_guest_pipe, &to_guest_pipe);
279 command.args(&["--socket", control_socket_path.to_str().unwrap()]);
280 command.args(additional_arguments);
281
282 TestVm::configure_kernel(&mut command, o_direct);
283
284 println!("$ {:?}", command);
285
286 let process = command.spawn()?;
287
288 // Open pipes. Panic if we cannot connect after a timeout.
289 let (to_guest, from_guest) = panic_on_timeout(
290 move || (File::create(to_guest_pipe), File::open(from_guest_pipe)),
291 VM_COMMUNICATION_TIMEOUT,
292 );
293
294 // Wait for magic line to be received, indicating the delegate is ready.
295 let mut from_guest_reader = BufReader::new(from_guest?);
296 let mut magic_line = String::new();
297 from_guest_reader.read_line(&mut magic_line)?;
298 assert_eq!(magic_line.trim(), TestVm::MAGIC_LINE);
299
300 Ok(TestVm {
301 test_dir,
302 from_guest_reader,
303 to_guest: to_guest?,
304 control_socket_path,
305 process,
306 debug,
307 })
308 }
309
310 /// Executes the shell command `command` and returns the programs stdout.
exec_in_guest(&mut self, command: &str) -> Result<String>311 pub fn exec_in_guest(&mut self, command: &str) -> Result<String> {
312 // Write command to serial port.
313 writeln!(&mut self.to_guest, "{}", command)?;
314
315 // We will receive an echo of what we have written on the pipe.
316 let mut echo = String::new();
317 self.from_guest_reader.read_line(&mut echo)?;
318 assert_eq!(echo.trim(), command);
319
320 // Return all remaining lines until we receive the MAGIC_LINE
321 let mut output = String::new();
322 loop {
323 let mut line = String::new();
324 self.from_guest_reader.read_line(&mut line)?;
325 if line.trim() == TestVm::MAGIC_LINE {
326 break;
327 }
328 output.push_str(&line);
329 }
330 let trimmed = output.trim();
331 if self.debug {
332 println!("<- {:?}", trimmed);
333 }
334 Ok(trimmed.to_string())
335 }
336
stop(&self) -> Result<()>337 pub fn stop(&self) -> Result<()> {
338 crosvm_command("stop", &[self.control_socket_path.to_str().unwrap()])
339 }
340
suspend(&self) -> Result<()>341 pub fn suspend(&self) -> Result<()> {
342 crosvm_command("suspend", &[self.control_socket_path.to_str().unwrap()])
343 }
344
resume(&self) -> Result<()>345 pub fn resume(&self) -> Result<()> {
346 crosvm_command("resume", &[self.control_socket_path.to_str().unwrap()])
347 }
348 }
349
350 impl Drop for TestVm {
drop(&mut self)351 fn drop(&mut self) {
352 self.stop().unwrap();
353 self.process.wait().unwrap();
354 }
355 }
356