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