petri/
linux_direct_serial_agent.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use futures::AsyncReadExt;
5use futures::AsyncWriteExt;
6use pal_async::socket::ReadHalf;
7use pal_async::socket::WriteHalf;
8use unix_socket::UnixStream;
9
10const BUSYBOX_INIT: &str = "/bin/busybox --install /bin && mount none /dev -t devtmpfs && mount none /proc -t proc && mount none /sys -t sysfs";
11
12pub(crate) struct LinuxDirectSerialAgent {
13    /// Writer to serial 0, the console we define in our kernel commandline
14    write: WriteHalf<UnixStream>,
15    /// Reader on serial 1, not serial 0, to avoid reading the commands we just sent
16    read: ReadHalf<UnixStream>,
17    /// Delayed initialization so new can be synchronous
18    init: bool,
19}
20
21impl LinuxDirectSerialAgent {
22    pub(crate) fn new(
23        serial1_read: ReadHalf<UnixStream>,
24        serial0_write: WriteHalf<UnixStream>,
25    ) -> Self {
26        Self {
27            read: serial1_read,
28            write: serial0_write,
29            init: false,
30        }
31    }
32}
33
34impl LinuxDirectSerialAgent {
35    // Inform the agent that it should reinitialize itself on the next command.
36    pub(crate) fn reset(&mut self) {
37        self.init = false;
38    }
39
40    pub(crate) async fn run_command(&mut self, command: &str) -> anyhow::Result<String> {
41        self.init_busybox_if_necessary().await?;
42        let bytes = self.run_command_core(command).await?;
43        Ok(String::from_utf8_lossy(&bytes).into_owned())
44    }
45
46    async fn run_command_core(&mut self, command: &str) -> anyhow::Result<Vec<u8>> {
47        // We need a signal that the current command has finished executing so that we can stop reading
48        // and return to the caller. The pipe will remain open, so we can't just read until we get 0 bytes.
49        // Instead we send this special text sequence to signal the end of the command, since it's unlikely
50        // that a normal command will ever output it.
51        const COMMAND_END_SIGNAL: &str = "== Petri Command Complete ==";
52        let command = format!("({command}) > /dev/ttyS1\necho {COMMAND_END_SIGNAL} > /dev/ttyS1\n");
53
54        // When reading the output there will be a trailing newline.
55        const COMMAND_END_SIGNAL_READ: &str = "== Petri Command Complete ==\r\n";
56
57        self.write.write_all(command.as_bytes()).await?;
58
59        let mut output = Vec::new();
60        let mut buf = [0u8; 1024];
61        loop {
62            let n = self.read.read(&mut buf).await?;
63            tracing::debug!(buf = ?&buf[..n], "read serial bytes from guest");
64            output.extend_from_slice(&buf[..n]);
65            if output.ends_with(COMMAND_END_SIGNAL_READ.as_bytes()) {
66                output.truncate(output.len() - COMMAND_END_SIGNAL_READ.len());
67                break;
68            }
69        }
70
71        Ok(output)
72    }
73
74    async fn init_busybox_if_necessary(&mut self) -> anyhow::Result<()> {
75        if !self.init {
76            self.run_command_core(BUSYBOX_INIT).await?;
77            self.init = true;
78        };
79        Ok(())
80    }
81}