fixture/sys/
linux.rs

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
5use std::ffi::CString;
6use std::fs::File;
7use std::fs::OpenOptions;
8use std::io;
9use std::io::BufReader;
10use std::os::unix::fs::OpenOptionsExt;
11use std::path::Path;
12use std::path::PathBuf;
13use std::process::Child;
14use std::process::Command;
15use std::process::Stdio;
16use std::sync::Arc;
17use std::sync::Mutex;
18use std::time::Duration;
19use std::time::Instant;
20
21use anyhow::anyhow;
22use anyhow::Context;
23use anyhow::Result;
24use delegate::wire_format::DelegateMessage;
25use libc::O_DIRECT;
26use serde_json::StreamDeserializer;
27use tempfile::TempDir;
28
29use crate::utils::find_crosvm_binary;
30use crate::utils::run_with_status_check;
31use crate::vm::local_path_from_url;
32use crate::vm::Config;
33
34const FROM_GUEST_PIPE: &str = "from_guest";
35const TO_GUEST_PIPE: &str = "to_guest";
36const CONTROL_PIPE: &str = "control";
37
38/// Timeout for communicating with the VM. If we do not hear back, panic so we
39/// do not block the tests.
40const VM_COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(10);
41
42pub(crate) type SerialArgs = Path;
43
44/// Returns the name of crosvm binary.
45pub fn binary_name() -> &'static str {
46    "crosvm"
47}
48
49/// Safe wrapper for libc::mkfifo
50pub(crate) fn mkfifo(path: &Path) -> io::Result<()> {
51    let cpath = CString::new(path.to_str().unwrap()).unwrap();
52    // SAFETY: no mutable pointer passed to function and the return value is checked.
53    let result = unsafe { libc::mkfifo(cpath.as_ptr(), 0o777) };
54    if result == 0 {
55        Ok(())
56    } else {
57        Err(io::Error::last_os_error())
58    }
59}
60
61pub struct TestVmSys {
62    /// Maintain ownership of test_dir until the vm is destroyed.
63    #[allow(dead_code)]
64    pub test_dir: TempDir,
65    pub from_guest_reader: Arc<
66        Mutex<
67            StreamDeserializer<
68                'static,
69                serde_json::de::IoRead<BufReader<std::fs::File>>,
70                DelegateMessage,
71            >,
72        >,
73    >,
74    pub to_guest: Arc<Mutex<File>>,
75    pub control_socket_path: PathBuf,
76    pub process: Option<Child>, // Use `Option` to allow taking the ownership in `Drop::drop()`.
77}
78
79impl TestVmSys {
80    // Check if the test file system is a known compatible one. Needs to support features
81    // like O_DIRECT.
82    pub fn check_rootfs_file(rootfs_path: &Path) {
83        if let Err(e) = OpenOptions::new()
84            .custom_flags(O_DIRECT)
85            .write(false)
86            .read(true)
87            .open(rootfs_path)
88        {
89            panic!("File open with O_DIRECT expected to work but did not: {e}");
90        }
91    }
92
93    // Adds 2 serial devices:
94    // - ttyS0: Console device which prints kernel log / debug output of the delegate binary.
95    // - ttyS1: Serial device attached to the named pipes.
96    fn configure_serial_devices(
97        command: &mut Command,
98        stdout_hardware_type: &str,
99        from_guest_pipe: &Path,
100        to_guest_pipe: &Path,
101    ) {
102        let stdout_serial_option = format!("type=stdout,hardware={stdout_hardware_type},console");
103        command.args(["--serial", &stdout_serial_option]);
104
105        // Setup channel for communication with the delegate.
106        let serial_params = format!(
107            "type=file,path={},input={},num=2",
108            from_guest_pipe.display(),
109            to_guest_pipe.display()
110        );
111        command.args(["--serial", &serial_params]);
112    }
113
114    /// Configures the VM rootfs to load from the guest_under_test assets.
115    fn configure_rootfs(command: &mut Command, o_direct: bool, rw: bool, path: &Path) {
116        let rootfs_and_option = format!(
117            "{}{}{},root",
118            path.as_os_str().to_str().unwrap(),
119            if o_direct { ",direct=true" } else { "" },
120            if rw { "" } else { ",ro" }
121        );
122        command
123            .args(["--block", &rootfs_and_option])
124            .args(["--params", "init=/bin/delegate"]);
125    }
126
127    pub fn new_generic<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVmSys>
128    where
129        F: FnOnce(&mut Command, &Path, &Config) -> Result<()>,
130    {
131        // Create two named pipes to communicate with the guest.
132        let test_dir = TempDir::new()?;
133        let from_guest_pipe = test_dir.path().join(FROM_GUEST_PIPE);
134        let to_guest_pipe = test_dir.path().join(TO_GUEST_PIPE);
135        mkfifo(&from_guest_pipe)?;
136        mkfifo(&to_guest_pipe)?;
137
138        let control_socket_path = test_dir.path().join(CONTROL_PIPE);
139
140        let mut command = match &cfg.wrapper_cmd {
141            Some(cmd) => {
142                let wrapper_splitted =
143                    shlex::split(cmd).context("Failed to parse wrapper command")?;
144                let mut command_tmp = if sudo {
145                    let mut command = Command::new("sudo");
146                    command.arg(&wrapper_splitted[0]);
147                    command
148                } else {
149                    Command::new(&wrapper_splitted[0])
150                };
151
152                command_tmp.args(&wrapper_splitted[1..]);
153                command_tmp.arg(find_crosvm_binary());
154                command_tmp
155            }
156            None => {
157                if sudo {
158                    let mut command = Command::new("sudo");
159                    command.arg(find_crosvm_binary());
160                    command
161                } else {
162                    Command::new(find_crosvm_binary())
163                }
164            }
165        };
166
167        command.env("RUST_BACKTRACE", "full");
168
169        if let Some(log_file_name) = &cfg.log_file {
170            let log_file_stdout = File::create(log_file_name)?;
171            let log_file_stderr = log_file_stdout.try_clone()?;
172            command.stdout(Stdio::from(log_file_stdout));
173            command.stderr(Stdio::from(log_file_stderr));
174        }
175
176        command.args(["--log-level", cfg.log_level.as_str()]);
177        command.args(["run"]);
178
179        f(&mut command, test_dir.path(), &cfg)?;
180
181        command.args(&cfg.extra_args);
182
183        println!("$ {command:?}");
184        let mut process = command.spawn()?;
185
186        // Open pipes. Apply timeout to `to_guest` and `from_guest` since it will block until crosvm
187        // opens the other end.
188        let start = Instant::now();
189        let run_result = run_with_status_check(
190            move || (File::create(to_guest_pipe), File::open(from_guest_pipe)),
191            Duration::from_millis(200),
192            || {
193                if start.elapsed() > VM_COMMUNICATION_TIMEOUT {
194                    return false;
195                }
196                if let Some(wait_result) = process.try_wait().unwrap() {
197                    println!("crosvm unexpectedly exited: {wait_result:?}");
198                    false
199                } else {
200                    true
201                }
202            },
203        );
204
205        let (to_guest, from_guest) = match run_result {
206            Ok((to_guest, from_guest)) => (
207                to_guest.context("Cannot open to_guest pipe")?,
208                from_guest.context("Cannot open from_guest pipe")?,
209            ),
210            Err(error) => {
211                // Kill the crosvm process if we cannot connect in time.
212                process.kill().unwrap();
213                process.wait().unwrap();
214                panic!("Cannot connect to VM: {error}");
215            }
216        };
217
218        Ok(TestVmSys {
219            test_dir,
220            from_guest_reader: Arc::new(Mutex::new(
221                serde_json::Deserializer::from_reader(BufReader::new(from_guest)).into_iter(),
222            )),
223            to_guest: Arc::new(Mutex::new(to_guest)),
224            control_socket_path,
225            process: Some(process),
226        })
227    }
228
229    // Generates a config file from cfg and appends the command to use the config file.
230    pub fn append_config_args(command: &mut Command, test_dir: &Path, cfg: &Config) -> Result<()> {
231        TestVmSys::configure_serial_devices(
232            command,
233            &cfg.console_hardware,
234            &test_dir.join(FROM_GUEST_PIPE),
235            &test_dir.join(TO_GUEST_PIPE),
236        );
237        command.args(["--socket", test_dir.join(CONTROL_PIPE).to_str().unwrap()]);
238
239        if let Some(rootfs_url) = &cfg.rootfs_url {
240            if cfg.rootfs_rw {
241                std::fs::copy(
242                    match cfg.rootfs_compressed {
243                        true => local_path_from_url(rootfs_url).with_extension("raw"),
244                        false => local_path_from_url(rootfs_url),
245                    },
246                    test_dir.join("rw_rootfs.img"),
247                )
248                .unwrap();
249                TestVmSys::configure_rootfs(
250                    command,
251                    cfg.o_direct,
252                    true,
253                    &test_dir.join("rw_rootfs.img"),
254                );
255            } else if cfg.rootfs_compressed {
256                TestVmSys::configure_rootfs(
257                    command,
258                    cfg.o_direct,
259                    false,
260                    &local_path_from_url(rootfs_url).with_extension("raw"),
261                );
262            } else {
263                TestVmSys::configure_rootfs(
264                    command,
265                    cfg.o_direct,
266                    false,
267                    &local_path_from_url(rootfs_url),
268                );
269            }
270        };
271
272        // Set initrd if being requested
273        if let Some(initrd_url) = &cfg.initrd_url {
274            command.arg("--initrd");
275            command.arg(local_path_from_url(initrd_url));
276        }
277
278        // Set kernel as the last argument.
279        command.arg(local_path_from_url(&cfg.kernel_url));
280        Ok(())
281    }
282
283    pub fn crosvm_command(
284        &self,
285        command: &str,
286        mut args: Vec<String>,
287        sudo: bool,
288    ) -> Result<Vec<u8>> {
289        args.push(self.control_socket_path.to_str().unwrap().to_string());
290
291        println!("$ crosvm {} {:?}", command, &args.join(" "));
292
293        let mut cmd = if sudo {
294            let mut cmd = Command::new("sudo");
295            cmd.arg(find_crosvm_binary());
296            cmd
297        } else {
298            Command::new(find_crosvm_binary())
299        };
300
301        cmd.arg(command).args(args);
302
303        let output = cmd.output()?;
304        if !output.status.success() {
305            Err(anyhow!("Command failed with exit code {}", output.status))
306        } else {
307            Ok(output.stdout)
308        }
309    }
310}