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