Skip to main content

reverie_process/
pty.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use core::mem::MaybeUninit;
10use core::pin::Pin;
11use core::task::Context;
12use core::task::Poll;
13use std::io;
14use std::os::unix::io::AsRawFd;
15use std::os::unix::io::IntoRawFd;
16use std::os::unix::io::RawFd;
17
18use syscalls::Errno;
19use tokio::io::AsyncRead;
20use tokio::io::AsyncWrite;
21use tokio::io::ReadBuf;
22
23use super::fd::AsyncFd;
24use super::fd::Fd;
25
26/// Represents a pseudo-TTY "master".
27#[derive(Debug)]
28pub struct Pty {
29    fd: AsyncFd,
30}
31
32impl Pty {
33    /// Opens a new pseudo-TTY master.
34    ///
35    /// NOTE: As long as there is a handle open to at least one child pty, reads
36    /// will not reach EOF and will continue to return `EWOULDBLOCK`.
37    pub fn open() -> Result<Self, Errno> {
38        let fd = Fd::new(Errno::result(unsafe {
39            libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY)
40        })?);
41
42        Errno::result(unsafe { libc::grantpt(fd.as_raw_fd()) })?;
43        Errno::result(unsafe { libc::unlockpt(fd.as_raw_fd()) })?;
44
45        let fd = AsyncFd::new(fd)?;
46
47        Ok(Self { fd })
48    }
49
50    /// Opens a pseudo-TTY slave that is connected to this master.
51    pub fn child(&self) -> Result<PtyChild, Errno> {
52        const TIOCGPTPEER: libc::c_ulong = 0x5441;
53
54        let parent = self.fd.as_raw_fd();
55
56        let fd = Errno::result(unsafe {
57            // NOTE: This ioctl isn't supported until Linux v4.13 (see
58            // `ioctl_tty(2)`), so we may fallback to path-based slave fd
59            // allocation.
60            libc::ioctl(parent, TIOCGPTPEER, libc::O_RDWR | libc::O_NOCTTY)
61        })
62        .map(Fd::new)
63        .or_else(|_err| {
64            let mut path: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize];
65
66            Errno::result(unsafe { libc::ptsname_r(parent, path.as_mut_ptr(), path.len()) })?;
67
68            Fd::open_c(path.as_ptr(), libc::O_RDWR | libc::O_NOCTTY)
69        })?;
70
71        Ok(PtyChild { fd })
72    }
73}
74
75/// A pseudo-TTY child (or "slave" in TTY parlance). This is passed to child
76/// processes.
77#[derive(Debug)]
78pub struct PtyChild {
79    fd: Fd,
80}
81
82impl PtyChild {
83    /// Sets the pseudo-TTY child as the controlling terminal for the current
84    /// process.
85    ///
86    /// Specifically, this does several things:
87    ///  1. Calls setsid to create a new session.
88    ///  2. Makes this fd the controlling terminal of this process by running the
89    ///     correct ioctl.
90    ///  3. Calls `dup2` to set each stdio stream to redirect to this fd.
91    ///  4. Closes the fd.
92    pub fn login(self) -> Result<(), Errno> {
93        Errno::result(unsafe { libc::login_tty(self.fd.into_raw_fd()) })?;
94        Ok(())
95    }
96
97    /// Sets the window size in rows and columns.
98    pub fn set_window_size(&self, rows: u16, cols: u16) -> Result<(), Errno> {
99        let fd = self.fd.as_raw_fd();
100
101        let winsize = libc::winsize {
102            ws_row: rows,
103            ws_col: cols,
104            ws_xpixel: 0,
105            ws_ypixel: 0,
106        };
107
108        Errno::result(unsafe { libc::ioctl(fd, libc::TIOCSWINSZ, &winsize as *const _) })?;
109
110        Ok(())
111    }
112
113    /// Returns the window size in terms of rows and columns.
114    pub fn window_size(&self) -> Result<(u16, u16), Errno> {
115        let fd = self.fd.as_raw_fd();
116
117        let mut winsize = MaybeUninit::<libc::winsize>::uninit();
118
119        Errno::result(unsafe { libc::ioctl(fd, libc::TIOCGWINSZ, winsize.as_mut_ptr()) })?;
120
121        let winsize = unsafe { winsize.assume_init() };
122
123        Ok((winsize.ws_row, winsize.ws_col))
124    }
125
126    /// Sets the terminal parameters.
127    pub fn set_terminal_params(&self, params: &libc::termios) -> Result<(), Errno> {
128        let fd = self.fd.as_raw_fd();
129        Errno::result(unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, params as *const _) })?;
130        Ok(())
131    }
132
133    /// Gets the terminal parameters.
134    pub fn terminal_params(&self) -> Result<libc::termios, Errno> {
135        let fd = self.fd.as_raw_fd();
136
137        let mut term = MaybeUninit::<libc::termios>::uninit();
138
139        Errno::result(unsafe { libc::tcgetattr(fd, term.as_mut_ptr()) })?;
140
141        Ok(unsafe { term.assume_init() })
142    }
143}
144
145impl AsRawFd for Pty {
146    fn as_raw_fd(&self) -> RawFd {
147        self.fd.as_raw_fd()
148    }
149}
150
151impl AsRawFd for PtyChild {
152    fn as_raw_fd(&self) -> RawFd {
153        self.fd.as_raw_fd()
154    }
155}
156
157impl AsyncWrite for Pty {
158    fn poll_write(
159        mut self: Pin<&mut Self>,
160        cx: &mut Context<'_>,
161        buf: &[u8],
162    ) -> Poll<tokio::io::Result<usize>> {
163        Pin::new(&mut self.fd).poll_write(cx, buf)
164    }
165
166    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
167        Pin::new(&mut self.fd).poll_flush(cx)
168    }
169
170    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
171        Pin::new(&mut self.fd).poll_shutdown(cx)
172    }
173}
174
175impl AsyncRead for Pty {
176    fn poll_read(
177        mut self: Pin<&mut Self>,
178        cx: &mut Context,
179        buf: &mut ReadBuf,
180    ) -> Poll<tokio::io::Result<()>> {
181        Pin::new(&mut self.fd).poll_read(cx, buf)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[tokio::test]
190    async fn test_open() {
191        if crate::test_runs_in_own_process() {
192            return;
193        }
194        let pty = Pty::open().unwrap();
195
196        let child1 = pty.child().unwrap();
197        child1.set_window_size(20, 40).unwrap();
198        assert_eq!(child1.window_size().unwrap(), (20, 40));
199
200        let child2 = pty.child().unwrap();
201        child2.set_window_size(40, 80).unwrap();
202
203        assert_eq!(child2.window_size().unwrap(), (40, 80));
204
205        // Since they're both connected to the same master, changing the window
206        // size of one child affects both of them.
207        assert_eq!(child1.window_size().unwrap(), (40, 80));
208    }
209}