Skip to main content

arcbox_virtio_console/
pty.rs

1//! PTY (pseudo-terminal) console backend.
2//!
3//! Provides a real terminal interface that can be connected to by terminal
4//! emulators or the host shell.
5
6use std::os::unix::io::RawFd;
7
8use crate::ConsoleIo;
9
10/// PTY (pseudo-terminal) console backend.
11///
12/// Provides a real terminal interface that can be connected to
13/// by terminal emulators or the host shell.
14pub struct PtyConsole {
15    /// Master file descriptor.
16    master_fd: RawFd,
17    /// Slave file descriptor (opened for the guest).
18    slave_fd: RawFd,
19    /// Path to the slave PTY device.
20    slave_path: String,
21    /// Non-blocking mode.
22    nonblocking: bool,
23}
24
25impl PtyConsole {
26    /// Creates a new PTY console.
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if PTY allocation fails.
31    pub fn new() -> std::io::Result<Self> {
32        let mut master_fd: libc::c_int = -1;
33        let mut slave_fd: libc::c_int = -1;
34
35        // SAFETY: openpty writes the new master/slave fds via the out-pointers
36        // we provided. Any failure is reported via the return code, in which
37        // case we propagate `last_os_error()` without using the (uninitialised) fds.
38        let ret = unsafe {
39            libc::openpty(
40                &mut master_fd,
41                &mut slave_fd,
42                std::ptr::null_mut(),
43                std::ptr::null_mut(),
44                std::ptr::null_mut(),
45            )
46        };
47
48        if ret < 0 {
49            return Err(std::io::Error::last_os_error());
50        }
51
52        // SAFETY: ttyname returns a pointer into a static buffer owned by libc;
53        // we only borrow it long enough to copy into an owned String. On error
54        // (null pointer) we close the fds before returning.
55        let slave_path = unsafe {
56            let path_ptr = libc::ttyname(slave_fd);
57            if path_ptr.is_null() {
58                libc::close(master_fd);
59                libc::close(slave_fd);
60                return Err(std::io::Error::last_os_error());
61            }
62            std::ffi::CStr::from_ptr(path_ptr)
63                .to_string_lossy()
64                .into_owned()
65        };
66
67        tracing::info!(
68            "Created PTY console: master_fd={}, slave={}",
69            master_fd,
70            slave_path
71        );
72
73        Ok(Self {
74            master_fd,
75            slave_fd,
76            slave_path,
77            nonblocking: false,
78        })
79    }
80
81    /// Returns the path to the slave PTY device.
82    ///
83    /// Users can connect to this path with a terminal emulator:
84    /// - `screen /dev/pts/X`
85    /// - `minicom -D /dev/pts/X`
86    #[must_use]
87    pub fn slave_path(&self) -> &str {
88        &self.slave_path
89    }
90
91    /// Returns the master file descriptor.
92    #[must_use]
93    pub const fn master_fd(&self) -> RawFd {
94        self.master_fd
95    }
96
97    /// Sets non-blocking mode on the master.
98    pub fn set_nonblocking(&mut self, nonblocking: bool) -> std::io::Result<()> {
99        // SAFETY: F_GETFL/F_SETFL on a fd we own; fcntl is signal-safe and
100        // the only side effect on success is updating the fd's status flags.
101        let flags = unsafe { libc::fcntl(self.master_fd, libc::F_GETFL) };
102        if flags < 0 {
103            return Err(std::io::Error::last_os_error());
104        }
105
106        let new_flags = if nonblocking {
107            flags | libc::O_NONBLOCK
108        } else {
109            flags & !libc::O_NONBLOCK
110        };
111
112        // SAFETY: see above — fcntl on our owned fd.
113        let ret = unsafe { libc::fcntl(self.master_fd, libc::F_SETFL, new_flags) };
114        if ret < 0 {
115            return Err(std::io::Error::last_os_error());
116        }
117
118        self.nonblocking = nonblocking;
119        Ok(())
120    }
121
122    /// Sets terminal size.
123    pub fn set_window_size(&self, rows: u16, cols: u16) -> std::io::Result<()> {
124        let ws = libc::winsize {
125            ws_row: rows,
126            ws_col: cols,
127            ws_xpixel: 0,
128            ws_ypixel: 0,
129        };
130
131        // SAFETY: TIOCSWINSZ takes a `&winsize` we borrow on the stack; the
132        // ioctl reads it and does not retain the pointer.
133        let ret = unsafe { libc::ioctl(self.master_fd, libc::TIOCSWINSZ, &ws) };
134        if ret < 0 {
135            Err(std::io::Error::last_os_error())
136        } else {
137            Ok(())
138        }
139    }
140
141    /// Checks if data is available to read.
142    #[must_use]
143    pub fn has_data(&self) -> bool {
144        let mut pollfd = libc::pollfd {
145            fd: self.master_fd,
146            events: libc::POLLIN,
147            revents: 0,
148        };
149
150        // SAFETY: poll borrows our pollfd for the duration of the call (timeout 0).
151        let ret = unsafe { libc::poll(&mut pollfd, 1, 0) };
152        ret > 0 && (pollfd.revents & libc::POLLIN) != 0
153    }
154}
155
156impl Drop for PtyConsole {
157    fn drop(&mut self) {
158        // SAFETY: closing fds we exclusively own (no aliases handed out).
159        if self.master_fd >= 0 {
160            unsafe { libc::close(self.master_fd) };
161        }
162        if self.slave_fd >= 0 {
163            unsafe { libc::close(self.slave_fd) };
164        }
165    }
166}
167
168impl ConsoleIo for PtyConsole {
169    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
170        // SAFETY: read into a buffer borrowed mutably from `buf`; libc only
171        // writes within `buf.len()` bytes of `buf.as_mut_ptr()`.
172        let ret = unsafe {
173            libc::read(
174                self.master_fd,
175                buf.as_mut_ptr() as *mut libc::c_void,
176                buf.len(),
177            )
178        };
179
180        if ret < 0 {
181            let err = std::io::Error::last_os_error();
182            if err.kind() == std::io::ErrorKind::WouldBlock {
183                Ok(0)
184            } else {
185                Err(err)
186            }
187        } else {
188            Ok(ret as usize)
189        }
190    }
191
192    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
193        // SAFETY: write reads `buf.len()` bytes starting at `buf.as_ptr()`,
194        // both borrowed from the caller for the duration of the call.
195        let ret = unsafe {
196            libc::write(
197                self.master_fd,
198                buf.as_ptr() as *const libc::c_void,
199                buf.len(),
200            )
201        };
202
203        if ret < 0 {
204            Err(std::io::Error::last_os_error())
205        } else {
206            Ok(ret as usize)
207        }
208    }
209
210    fn flush(&mut self) -> std::io::Result<()> {
211        // PTY doesn't need explicit flush
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_pty_console_creation() {
222        let pty = PtyConsole::new().unwrap();
223        assert!(!pty.slave_path().is_empty());
224        assert!(pty.master_fd() >= 0);
225    }
226
227    #[test]
228    fn test_pty_console_write_read() {
229        let mut pty = PtyConsole::new().unwrap();
230        pty.set_nonblocking(true).unwrap();
231
232        let written = pty.write(b"test\n").unwrap();
233        assert!(written > 0);
234
235        // Note: Reading from PTY may not return data immediately — the slave
236        // side has to echo back first.
237    }
238
239    #[test]
240    fn test_pty_console_nonblocking() {
241        let mut pty = PtyConsole::new().unwrap();
242
243        pty.set_nonblocking(true).unwrap();
244        assert!(pty.nonblocking);
245
246        let mut buf = [0u8; 10];
247        let n = pty.read(&mut buf).unwrap();
248        assert_eq!(n, 0);
249    }
250
251    #[test]
252    fn test_pty_console_window_size() {
253        let pty = PtyConsole::new().unwrap();
254
255        assert!(pty.set_window_size(24, 80).is_ok());
256        assert!(pty.set_window_size(50, 132).is_ok());
257    }
258
259    #[test]
260    fn test_pty_console_has_data() {
261        let pty = PtyConsole::new().unwrap();
262        assert!(!pty.has_data());
263    }
264
265    #[test]
266    fn test_pty_console_flush() {
267        let mut pty = PtyConsole::new().unwrap();
268        assert!(pty.flush().is_ok());
269    }
270}