opentitanlib/transport/verilator/
subprocess.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5use anyhow::{Context, Result, anyhow, ensure};
6use regex::Regex;
7use std::io::ErrorKind;
8use std::process::{Child, Command, Stdio};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11
12use crate::util::printer;
13
14/// Verilator startup options.
15pub struct Options {
16    /// The verilator executable.
17    pub executable: String,
18    /// The ROM images used to boot the CPU.
19    /// Format: "file[:slot]" where file is the image path and slot is an optional
20    /// slot number (defaults to 0).
21    /// Example: ["rom.bin", "rom.bin:1", "second_rom.bin:2"]
22    pub rom_images: Vec<String>,
23    /// The flash images stored in internal flash memory.
24    /// Format: "file[:slot]" where file is the image path and slot is an optional
25    /// slot number (defaults to 0).
26    /// Example: ["app.bin", "app.bin:1", "data.bin:2"]
27    pub flash_images: Vec<String>,
28    /// The image to load into CTN RAM.
29    pub ctn_ram_image: String,
30    /// The OTP settings.
31    pub otp_image: String,
32    /// Any extra arguments to verilator.
33    pub extra_args: Vec<String>,
34    /// Timeout for starting verilator.
35    pub timeout: Duration,
36}
37
38pub struct Subprocess {
39    child: Child,
40    accumulated_output: Arc<Mutex<String>>,
41}
42
43impl Subprocess {
44    /// Starts a verilator [`Subprocess`] based on [`Options`].
45    pub fn from_options(options: Options) -> Result<Self> {
46        let mut command = Command::new(&options.executable);
47        let mut args = Vec::new();
48
49        if !options.rom_images.is_empty() {
50            // Parse ROM image format: "file[:slot]" where slot defaults to 0
51            let re = Regex::new(r"^(?P<fname>.*?)(:(?P<slot>\d+))?$").unwrap();
52            let images = &options.rom_images;
53            for image in images {
54                let groups = re.captures(image).unwrap();
55                let image_file = groups.name("fname").unwrap().as_str();
56                let slot = match groups.name("slot") {
57                    Some(x) => x
58                        .as_str()
59                        .parse::<u8>()
60                        .with_context(|| format!("Invalid ROM slot '{}'", x.as_str()))?,
61                    None => 0,
62                };
63                args.push(format!("--meminit=rom{},{}", slot, image_file));
64            }
65        }
66        if !options.flash_images.is_empty() {
67            // Parse flash image format: "file[:slot]" where slot defaults to 0
68            let re = Regex::new(r"^(?P<fname>.*?)(:(?P<slot>\d+))?$").unwrap();
69            let images = &options.flash_images;
70            for image in images {
71                let groups = re.captures(image).unwrap();
72                let image_file = groups.name("fname").unwrap().as_str();
73                let slot = match groups.name("slot") {
74                    Some(x) => x
75                        .as_str()
76                        .parse::<u8>()
77                        .with_context(|| format!("Invalid FLASH slot '{}'", x.as_str()))?,
78                    None => 0,
79                };
80                args.push(format!("--meminit=flash{},{}", slot, image_file));
81            }
82        }
83        if !options.ctn_ram_image.is_empty() {
84            args.push(format!("--meminit=ctn_ram,{}", options.ctn_ram_image));
85        }
86        if !options.otp_image.is_empty() {
87            args.push(format!("--meminit=otp,{}", options.otp_image));
88        }
89        args.extend_from_slice(&options.extra_args);
90        command.args(&args[..]);
91
92        log::info!("CWD: {:?}", std::env::current_dir());
93        log::info!(
94            "Spawning verilator: {:?} {:?}",
95            options.executable,
96            args.join(" ")
97        );
98
99        let mut child = command
100            .stdin(Stdio::null())
101            .stdout(Stdio::piped())
102            .stderr(Stdio::inherit())
103            .spawn()?;
104        let accumulator: Arc<Mutex<String>> = Default::default();
105        let stdout = child.stdout.take().unwrap();
106        let a = Arc::clone(&accumulator);
107        std::thread::spawn(move || {
108            printer::accumulate(stdout, concat!(module_path!(), "::stdout"), a)
109        });
110
111        Ok(Subprocess {
112            child,
113            accumulated_output: accumulator,
114        })
115    }
116
117    /// Finds a string within the verilator output.
118    /// It is assumed that the [`Regex`] `re` has zero or one capture groups.
119    pub fn find(&self, re: &Regex, deadline: Instant) -> Result<String> {
120        // Regex captures_len: Capture group 0 is the full match.  Subsequent
121        // capture groups are the individual capture groups in the regex.
122        // We expect at most one user-specified capture group in the regex,
123        // and thus expect a capture length of at most two.
124        let len = re.captures_len();
125        ensure!(
126            len <= 2,
127            "Expected zero or one capture groups, found {}",
128            len
129        );
130        while deadline > Instant::now() {
131            {
132                let a = self.accumulated_output.lock().unwrap();
133                if let Some(captures) = re.captures(a.as_str()) {
134                    let val = captures.get(len - 1).expect("expected a capture");
135                    return Ok(val.as_str().to_owned());
136                }
137            }
138            std::thread::sleep(Duration::from_millis(50));
139        }
140        Err(anyhow!("Timed out"))
141    }
142
143    /// Kill the verilator subprocess.
144    pub fn kill(&mut self) -> Result<()> {
145        match self.child.kill() {
146            Err(error) if error.kind() != ErrorKind::InvalidInput => Err(error.into()),
147            _ => Ok(()),
148        }
149    }
150}
151
152#[cfg(test)]
153mod test {
154    use super::*;
155
156    fn echo_subprocess() -> Result<Subprocess> {
157        let options = Options {
158            executable: "/bin/echo".to_owned(),
159            rom_images: vec!["/dev/null:1".to_owned()],
160            flash_images: vec!["/dev/null:1".to_owned()],
161            otp_image: "".to_owned(),
162            ctn_ram_image: "".to_owned(),
163            extra_args: vec!["abc 123 def 456".to_owned()],
164            timeout: Duration::from_secs(5),
165        };
166        Subprocess::from_options(options)
167    }
168
169    #[test]
170    fn test_find_regex() -> Result<()> {
171        let subprocess = echo_subprocess()?;
172        let regex = Regex::new("abc (.*) def")?;
173        let deadline = Instant::now() + Duration::from_secs(5);
174        let found = subprocess.find(&regex, deadline)?;
175        assert_eq!(found, "123");
176        Ok(())
177    }
178
179    #[test]
180    fn test_kill() -> Result<()> {
181        let mut subprocess = echo_subprocess()?;
182        subprocess.kill()?;
183        Ok(())
184    }
185}