Skip to main content

arcbox_virtio_console/
socket.rs

1//! Unix-socket console backend.
2//!
3//! Allows connecting to the console via a Unix socket — useful for automated
4//! testing and scripting.
5
6use std::io::{Read, Write};
7use std::os::unix::net::{UnixListener, UnixStream};
8use std::path::{Path, PathBuf};
9
10use crate::ConsoleIo;
11
12/// Unix socket-based console backend.
13///
14/// Allows connecting to the console via a Unix socket,
15/// useful for automated testing and scripting.
16pub struct SocketConsole {
17    /// Listening socket.
18    listener: Option<UnixListener>,
19    /// Connected client.
20    client: Option<UnixStream>,
21    /// Socket path.
22    path: PathBuf,
23}
24
25impl SocketConsole {
26    /// Creates a new socket console.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if the socket cannot be created.
31    pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
32        let path = path.into();
33
34        let _ = std::fs::remove_file(&path);
35
36        let listener = UnixListener::bind(&path)?;
37        listener.set_nonblocking(true)?;
38
39        tracing::info!("Created socket console: {}", path.display());
40
41        Ok(Self {
42            listener: Some(listener),
43            client: None,
44            path,
45        })
46    }
47
48    /// Returns the socket path.
49    #[must_use]
50    pub fn path(&self) -> &Path {
51        &self.path
52    }
53
54    /// Accepts a new connection if available.
55    pub fn accept(&mut self) -> std::io::Result<bool> {
56        if let Some(listener) = &self.listener {
57            match listener.accept() {
58                Ok((stream, _)) => {
59                    stream.set_nonblocking(true)?;
60                    self.client = Some(stream);
61                    tracing::info!("Console client connected");
62                    Ok(true)
63                }
64                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(false),
65                Err(e) => Err(e),
66            }
67        } else {
68            Ok(false)
69        }
70    }
71
72    /// Checks if a client is connected.
73    #[must_use]
74    pub const fn is_connected(&self) -> bool {
75        self.client.is_some()
76    }
77}
78
79impl Drop for SocketConsole {
80    fn drop(&mut self) {
81        self.client = None;
82        self.listener = None;
83        let _ = std::fs::remove_file(&self.path);
84    }
85}
86
87impl ConsoleIo for SocketConsole {
88    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
89        // Try to accept new connections
90        let _ = self.accept();
91
92        if let Some(client) = &mut self.client {
93            match client.read(buf) {
94                Ok(0) => {
95                    // Client disconnected
96                    self.client = None;
97                    Ok(0)
98                }
99                Ok(n) => Ok(n),
100                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(0),
101                Err(e) => Err(e),
102            }
103        } else {
104            Ok(0)
105        }
106    }
107
108    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
109        if let Some(client) = &mut self.client {
110            match client.write(buf) {
111                Ok(n) => Ok(n),
112                Err(e) => {
113                    // Client disconnected
114                    self.client = None;
115                    Err(e)
116                }
117            }
118        } else {
119            // No client connected — discard rather than error.
120            Ok(buf.len())
121        }
122    }
123
124    fn flush(&mut self) -> std::io::Result<()> {
125        if let Some(client) = &mut self.client {
126            client.flush()
127        } else {
128            Ok(())
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_socket_console_creation() {
139        let temp_dir = std::env::temp_dir();
140        let socket_path = temp_dir.join(format!("test_console_{}.sock", std::process::id()));
141
142        let console = SocketConsole::new(&socket_path).unwrap();
143        assert_eq!(console.path(), socket_path);
144        assert!(!console.is_connected());
145    }
146
147    #[test]
148    fn test_socket_console_no_client() {
149        let temp_dir = std::env::temp_dir();
150        let socket_path = temp_dir.join(format!(
151            "test_console_no_client_{}.sock",
152            std::process::id()
153        ));
154
155        let mut console = SocketConsole::new(&socket_path).unwrap();
156
157        let mut buf = [0u8; 10];
158        let n = console.read(&mut buf).unwrap();
159        assert_eq!(n, 0);
160
161        let written = console.write(b"test").unwrap();
162        assert_eq!(written, 4);
163    }
164
165    #[test]
166    fn test_socket_console_accept_no_client() {
167        let temp_dir = std::env::temp_dir();
168        let socket_path = temp_dir.join(format!("test_console_accept_{}.sock", std::process::id()));
169
170        let mut console = SocketConsole::new(&socket_path).unwrap();
171
172        assert!(!console.accept().unwrap());
173    }
174
175    #[test]
176    fn test_socket_console_client_connect() {
177        let temp_dir = std::env::temp_dir();
178        let socket_path =
179            temp_dir.join(format!("test_console_connect_{}.sock", std::process::id()));
180
181        let mut console = SocketConsole::new(&socket_path).unwrap();
182
183        let _client = UnixStream::connect(&socket_path).unwrap();
184
185        assert!(console.accept().unwrap());
186        assert!(console.is_connected());
187    }
188
189    #[test]
190    fn test_socket_console_read_write_with_client() {
191        let temp_dir = std::env::temp_dir();
192        let socket_path = temp_dir.join(format!("test_console_rw_{}.sock", std::process::id()));
193
194        let mut console = SocketConsole::new(&socket_path).unwrap();
195
196        let mut client = UnixStream::connect(&socket_path).unwrap();
197        client.set_nonblocking(true).unwrap();
198        console.accept().unwrap();
199
200        console.write(b"Hello").unwrap();
201
202        let mut buf = [0u8; 10];
203        std::thread::sleep(std::time::Duration::from_millis(10));
204        let n = client.read(&mut buf).unwrap_or(0);
205        if n > 0 {
206            assert_eq!(&buf[..n], b"Hello");
207        }
208
209        client.write_all(b"World").unwrap();
210
211        std::thread::sleep(std::time::Duration::from_millis(10));
212        let mut buf2 = [0u8; 10];
213        let n2 = console.read(&mut buf2).unwrap();
214        if n2 > 0 {
215            assert_eq!(&buf2[..n2], b"World");
216        }
217    }
218
219    #[test]
220    fn test_socket_console_flush() {
221        let temp_dir = std::env::temp_dir();
222        let socket_path = temp_dir.join(format!("test_console_flush_{}.sock", std::process::id()));
223
224        let mut console = SocketConsole::new(&socket_path).unwrap();
225
226        assert!(console.flush().is_ok());
227    }
228
229    #[test]
230    fn test_socket_console_cleanup() {
231        let temp_dir = std::env::temp_dir();
232        let socket_path =
233            temp_dir.join(format!("test_console_cleanup_{}.sock", std::process::id()));
234
235        {
236            let _console = SocketConsole::new(&socket_path).unwrap();
237            assert!(socket_path.exists());
238        }
239
240        assert!(!socket_path.exists());
241    }
242}