tudelft_arm_qemu_runner/
lib.rs

1use std::{
2    fs::File,
3    io::{Read, Write},
4    net::{TcpListener, TcpStream},
5    path::Path,
6    process::{Child, Command},
7};
8use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
9use std::process::Stdio;
10use tracing::info;
11use error::{PrereqError, RunnerError};
12
13pub mod error;
14
15/// Run a qemu instance, and get a handle to it's UART input and output.
16///
17/// When the runner is dropped the qemu instance is killed. This can be used
18/// to reset the runner to a known initial state.
19///
20/// The only way to interact with the runner is via the Read and Write traits.
21/// These traits provide a way to write bytes to and read from the UART driver.
22pub struct Runner {
23    pub qemu: Child,
24    #[allow(unused)]
25    pub listener: TcpListener,
26    pub stream: TcpStream,
27    /// Used to determine if the runner is running tests or not.
28    ///
29    /// It it's set to true, you probably want to call `wait_for_tests` instead
30    /// of using the Read and Write traits.
31    ///
32    /// At least for the example, the tests are not using the UART driver.
33    pub testing: bool,
34}
35
36impl Drop for Runner {
37    fn drop(&mut self) {
38        if !self.testing {
39            info!("runner dropped: killing qemu");
40            self.qemu.kill().unwrap();
41        }
42    }
43}
44
45impl Read for Runner {
46    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
47        self.stream.read(buf)
48    }
49}
50
51impl Write for Runner {
52    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
53        self.stream.write(buf)
54    }
55
56    fn flush(&mut self) -> std::io::Result<()> {
57        self.stream.flush()
58    }
59}
60
61fn check_prereqs() -> Result<(), PrereqError> {
62    info!("checking for `rust-objcopy` in $PATH");
63    if Command::new("rust-objcopy").output().is_err() {
64        return Err(PrereqError::MissingRustObjcopy);
65    }
66
67    info!("checking for `qemu-system-arm` in $PATH");
68    if Command::new("qemu-system-arm").output().is_err() {
69        return Err(PrereqError::MissingQemu);
70    }
71
72    Ok(())
73}
74
75impl Runner {
76    /// Creates a new runner instance.
77    ///
78    /// This spins up qemu with the provided executable, and starts
79    /// a tcp listener to allow qemu to use tcp as a replacement for
80    /// the normal UART pins.
81    pub fn new(executable_path: &str, wait_for_debugger: bool) -> Result<Self, RunnerError> {
82        check_prereqs()?;
83        let original_file = Path::new(&executable_path).to_path_buf();
84        let testing = {
85            let mut v = vec![];
86            File::open(&original_file)?.read_to_end(&mut v)?;
87            const TEST_RUNNER: &[u8] = b"test_runner";
88            v.windows(TEST_RUNNER.len()).any(|p| p == TEST_RUNNER)
89        };
90
91        let mut qemu_cmd = create_qemu(&executable_path);
92        const SERIAL_ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 4321));
93        qemu_cmd.args(["-serial", &format!("tcp:{SERIAL_ADDRESS}")]);
94
95        if wait_for_debugger {
96            qemu_cmd.args(["-s", "-S"]);
97        }
98
99        info!("starting qemu: {qemu_cmd:?}");
100        let qemu = qemu_cmd.spawn()?;
101        info!("qemu piping serial i/o to tcp:{SERIAL_ADDRESS}");
102
103        let socket_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), SERIAL_ADDRESS.port());
104        info!("binding tcp socket to {socket_addr}");
105
106        if wait_for_debugger {
107            info!("waiting for debugger to attach...");
108        }
109
110        let listener = TcpListener::bind(socket_addr).unwrap();
111        let stream = listener.incoming().next().unwrap()?;
112        Ok(Self {
113            qemu,
114            listener,
115            stream,
116            testing,
117        })
118    }
119
120    /// Wait for the tests to finish.
121    pub fn wait_for_tests(mut self) {
122        self.qemu.wait().unwrap();
123    }
124}
125
126fn create_qemu(bin_file: &str) -> Command {
127    let mut cmd = Command::new("qemu-system-arm");
128    cmd.stdout(Stdio::inherit());
129    cmd.args(["-machine", "lm3s6965evb"])
130        .args(["-cpu", "cortex-m3"])
131        .args(["-semihosting-config", "enable=on,target=native"])
132        .arg("-kernel")
133        .arg(bin_file)
134        .args(["-monitor", "none"]);
135    cmd
136}