• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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