terminal_trx/
unix.rs

1use crate::StdioLocks;
2use libc::{c_int, fcntl, termios, F_GETFL, O_RDWR};
3use std::ffi::{CStr, CString, OsStr};
4use std::fmt;
5use std::fs::{File, OpenOptions};
6use std::io::{self, stderr, stdin, stdout, IsTerminal};
7use std::mem::{self, ManuallyDrop};
8use std::ops::{Deref, DerefMut};
9use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd as _};
10use std::os::unix::ffi::OsStrExt;
11
12mod attr;
13#[cfg(test)]
14mod pty_utils;
15#[cfg(test)]
16mod tests;
17
18pub(crate) fn terminal() -> io::Result<Terminal> {
19    None.or_else(|| reuse_tty_from_stdio(stderr).transpose())
20        .or_else(|| reuse_tty_from_stdio(stdout).transpose())
21        .or_else(|| reuse_tty_from_stdio(stdin).transpose())
22        .map(|r| r.and_then(Terminal::from_stdio))
23        .unwrap_or_else(|| Ok(Terminal::from_controlling(open_controlling_tty()?)))
24}
25
26fn reuse_tty_from_stdio<S: IsTerminal + AsFd>(
27    stream: impl FnOnce() -> S,
28) -> io::Result<Option<TerminalFile>> {
29    let stream = stream();
30
31    if stream.is_terminal() {
32        // This branch here is a bit questionable to me:
33        // I've seen a lot of code that re-uses the standard I/O fd if possible.
34        // But I don't quite understand what the benefit of that is. Is it to have as little fds open as possible?
35        // Is it a lot faster than opening the tty ourselves?
36        if is_read_write(stream.as_fd())? {
37            // SAFETY: We know that the file descriptor is valid.
38            // However we break the assumption that the file descriptor is owned.
39            // That's why the file is immediately wrapped in a ManuallyDrop to prevent
40            // the standard I/O descriptor from being closed.
41            let file = unsafe { File::from_raw_fd(stream.as_fd().as_raw_fd()) };
42            Ok(Some(TerminalFile::Borrowed(ManuallyDrop::new(file))))
43        } else {
44            reopen_tty(stream.as_fd())
45                .map(TerminalFile::Owned)
46                .map(Some)
47        }
48    } else {
49        Ok(None)
50    }
51}
52
53fn open_controlling_tty() -> io::Result<TerminalFile> {
54    OpenOptions::new()
55        .read(true)
56        .write(true)
57        .open("/dev/tty")
58        .map(TerminalFile::Owned)
59}
60
61fn is_read_write(fd: BorrowedFd) -> io::Result<bool> {
62    // SAFETY: We know that the file descriptor is valid.
63    let mode = to_io_result(unsafe { fcntl(fd.as_raw_fd(), F_GETFL) })?;
64    Ok(mode & O_RDWR == O_RDWR)
65}
66
67fn reopen_tty(fd: BorrowedFd) -> io::Result<File> {
68    let name = ttyname_r(fd)?;
69    OpenOptions::new()
70        .read(true)
71        .write(true)
72        .open(OsStr::from_bytes(name.as_bytes()))
73}
74
75fn is_same_file(a: BorrowedFd, b: BorrowedFd) -> io::Result<bool> {
76    Ok(a.as_raw_fd() == b.as_raw_fd() || {
77        let stat_a = fstat(a)?;
78        let stat_b = fstat(b)?;
79        stat_a.st_dev == stat_b.st_dev && stat_a.st_ino == stat_b.st_ino
80    })
81}
82
83fn fstat(fd: BorrowedFd) -> io::Result<libc::stat> {
84    // SAFETY: If fstat is successful, then we get a valid stat structure.
85    let mut stat = unsafe { mem::zeroed() };
86    // SAFETY: We know that the file descriptor is valid.
87    to_io_result(unsafe { libc::fstat(fd.as_raw_fd(), &mut stat) })?;
88    Ok(stat)
89}
90
91#[derive(Debug)]
92pub(crate) struct Terminal {
93    file: TerminalFile,
94    same_as_stdin: bool,
95    same_as_stdout: bool,
96    same_as_stderr: bool,
97}
98
99impl Terminal {
100    pub(crate) fn lock_stdio(&self) -> StdioLocks {
101        StdioLocks {
102            stdin_lock: self.same_as_stdin.then(|| stdin().lock()),
103            stdout_lock: self.same_as_stdout.then(|| stdout().lock()),
104            stderr_lock: self.same_as_stderr.then(|| stderr().lock()),
105        }
106    }
107
108    pub(crate) fn enable_raw_mode(&mut self) -> io::Result<RawModeGuard<'_>> {
109        let fd = self.file.as_fd();
110        let old_termios = attr::get_terminal_attr(fd)?;
111
112        if !attr::is_raw_mode_enabled(&old_termios) {
113            let mut termios = old_termios;
114            attr::enable_raw_mode(&mut termios);
115            attr::set_terminal_attr(fd, &termios)?;
116            Ok(RawModeGuard {
117                inner: self,
118                old_termios: Some(old_termios),
119            })
120        } else {
121            Ok(RawModeGuard {
122                inner: self,
123                old_termios: None,
124            })
125        }
126    }
127
128    pub(crate) fn has_connected_stdio_stream(&self) -> bool {
129        self.same_as_stdin || self.same_as_stdout || self.same_as_stderr
130    }
131}
132
133impl Terminal {
134    fn from_stdio(file: TerminalFile) -> io::Result<Self> {
135        Ok(Terminal {
136            same_as_stdin: is_same_file(file.as_fd(), stdin().as_fd())?,
137            same_as_stdout: is_same_file(file.as_fd(), stdout().as_fd())?,
138            same_as_stderr: is_same_file(file.as_fd(), stderr().as_fd())?,
139            file,
140        })
141    }
142
143    fn from_controlling(file: TerminalFile) -> Self {
144        Terminal {
145            file,
146            same_as_stdin: false,
147            same_as_stdout: false,
148            same_as_stderr: false,
149        }
150    }
151}
152
153#[derive(Debug)]
154enum TerminalFile {
155    Owned(File),
156    Borrowed(ManuallyDrop<File>),
157}
158
159impl io::Write for Terminal {
160    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
161        self.file.write(buf)
162    }
163
164    fn flush(&mut self) -> io::Result<()> {
165        self.file.flush()
166    }
167}
168
169impl io::Read for Terminal {
170    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
171        self.file.read(buf)
172    }
173}
174
175impl Deref for TerminalFile {
176    type Target = File;
177
178    fn deref(&self) -> &Self::Target {
179        match self {
180            TerminalFile::Owned(f) => f,
181            TerminalFile::Borrowed(f) => f,
182        }
183    }
184}
185
186impl DerefMut for TerminalFile {
187    fn deref_mut(&mut self) -> &mut Self::Target {
188        match self {
189            TerminalFile::Owned(f) => f,
190            TerminalFile::Borrowed(f) => f,
191        }
192    }
193}
194
195impl AsFd for super::Terminal {
196    fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
197        self.0.file.as_fd()
198    }
199}
200
201impl AsFd for super::TerminalLock<'_> {
202    fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
203        self.inner.file.as_fd()
204    }
205}
206
207impl AsFd for super::RawModeGuard<'_> {
208    fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
209        self.0.inner.file.as_fd()
210    }
211}
212
213impl AsRawFd for super::Terminal {
214    fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
215        self.0.file.as_raw_fd()
216    }
217}
218
219impl AsRawFd for super::TerminalLock<'_> {
220    fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
221        self.inner.file.as_raw_fd()
222    }
223}
224
225impl AsRawFd for super::RawModeGuard<'_> {
226    fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
227        self.0.inner.file.as_raw_fd()
228    }
229}
230
231pub(crate) struct RawModeGuard<'a> {
232    inner: &'a mut Terminal,
233    old_termios: Option<termios>,
234}
235
236impl fmt::Debug for RawModeGuard<'_> {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        f.debug_struct("RawModeGuard")
239            .field("inner", &self.inner)
240            .finish_non_exhaustive()
241    }
242}
243
244impl io::Write for RawModeGuard<'_> {
245    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
246        self.inner.write(buf)
247    }
248
249    fn flush(&mut self) -> io::Result<()> {
250        self.inner.flush()
251    }
252}
253
254impl io::Read for RawModeGuard<'_> {
255    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
256        self.inner.read(buf)
257    }
258}
259
260impl Drop for RawModeGuard<'_> {
261    fn drop(&mut self) {
262        if let Some(old_termios) = self.old_termios {
263            _ = attr::set_terminal_attr(self.inner.file.as_fd(), &old_termios);
264        }
265    }
266}
267
268fn to_io_result(value: c_int) -> io::Result<c_int> {
269    if value == -1 {
270        Err(io::Error::last_os_error())
271    } else {
272        Ok(value)
273    }
274}
275
276/// `ttyname_r` returns the path to the terminal device.
277#[cfg(not(target_os = "macos"))]
278fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
279    let mut buf = Vec::with_capacity(64);
280
281    loop {
282        // SAFETY: We pass the capacity of our vec to ttyname_r.
283        let code = unsafe { libc::ttyname_r(fd.as_raw_fd(), buf.as_mut_ptr(), buf.capacity()) };
284        match code {
285            // SAFETY: We own the pointer and we know that if ttyname_r is successful, it returns a null-terminated string.
286            0 => return Ok(unsafe { CStr::from_ptr(buf.as_ptr()) }.to_owned()),
287            libc::ERANGE => buf.reserve(64),
288            code => return Err(io::Error::from_raw_os_error(code)),
289        }
290    }
291}
292
293/// macOS does not have `ttyname_r` (the race free version), so we have to resort to `fcntl`.
294#[cfg(target_os = "macos")]
295fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
296    use libc::{F_GETPATH, PATH_MAX};
297
298    // the buffer size must be >= MAXPATHLEN, see `man fcntl`
299    let buf: [i8; PATH_MAX as usize] = [0; PATH_MAX as usize];
300
301    unsafe {
302        match fcntl(fd.as_raw_fd(), F_GETPATH as c_int, &buf) {
303            0 => {
304                let res = CStr::from_ptr(buf.as_ptr()).to_owned();
305                Ok(res)
306            }
307            _ => Err(io::Error::last_os_error()),
308        }
309    }
310}