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
129impl Terminal {
130    fn from_stdio(file: TerminalFile) -> io::Result<Self> {
131        Ok(Terminal {
132            same_as_stdin: is_same_file(file.as_fd(), stdin().as_fd())?,
133            same_as_stdout: is_same_file(file.as_fd(), stdout().as_fd())?,
134            same_as_stderr: is_same_file(file.as_fd(), stderr().as_fd())?,
135            file,
136        })
137    }
138
139    fn from_controlling(file: TerminalFile) -> Self {
140        Terminal {
141            file,
142            same_as_stdin: false,
143            same_as_stdout: false,
144            same_as_stderr: false,
145        }
146    }
147}
148
149#[derive(Debug)]
150enum TerminalFile {
151    Owned(File),
152    Borrowed(ManuallyDrop<File>),
153}
154
155impl io::Write for Terminal {
156    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
157        self.file.write(buf)
158    }
159
160    fn flush(&mut self) -> io::Result<()> {
161        self.file.flush()
162    }
163}
164
165impl io::Read for Terminal {
166    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
167        self.file.read(buf)
168    }
169}
170
171impl Deref for TerminalFile {
172    type Target = File;
173
174    fn deref(&self) -> &Self::Target {
175        match self {
176            TerminalFile::Owned(f) => f,
177            TerminalFile::Borrowed(f) => f,
178        }
179    }
180}
181
182impl DerefMut for TerminalFile {
183    fn deref_mut(&mut self) -> &mut Self::Target {
184        match self {
185            TerminalFile::Owned(f) => f,
186            TerminalFile::Borrowed(f) => f,
187        }
188    }
189}
190
191impl AsFd for super::Terminal {
192    fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
193        self.0.file.as_fd()
194    }
195}
196
197impl AsFd for super::TerminalLock<'_> {
198    fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
199        self.inner.file.as_fd()
200    }
201}
202
203impl AsFd for super::RawModeGuard<'_> {
204    fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
205        self.0.inner.file.as_fd()
206    }
207}
208
209impl AsRawFd for super::Terminal {
210    fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
211        self.0.file.as_raw_fd()
212    }
213}
214
215impl AsRawFd for super::TerminalLock<'_> {
216    fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
217        self.inner.file.as_raw_fd()
218    }
219}
220
221impl AsRawFd for super::RawModeGuard<'_> {
222    fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
223        self.0.inner.file.as_raw_fd()
224    }
225}
226
227pub(crate) struct RawModeGuard<'a> {
228    inner: &'a mut Terminal,
229    old_termios: Option<termios>,
230}
231
232impl fmt::Debug for RawModeGuard<'_> {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        f.debug_struct("RawModeGuard")
235            .field("inner", &self.inner)
236            .finish_non_exhaustive()
237    }
238}
239
240impl io::Write for RawModeGuard<'_> {
241    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
242        self.inner.write(buf)
243    }
244
245    fn flush(&mut self) -> io::Result<()> {
246        self.inner.flush()
247    }
248}
249
250impl io::Read for RawModeGuard<'_> {
251    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
252        self.inner.read(buf)
253    }
254}
255
256impl Drop for RawModeGuard<'_> {
257    fn drop(&mut self) {
258        if let Some(old_termios) = self.old_termios {
259            _ = attr::set_terminal_attr(self.inner.file.as_fd(), &old_termios);
260        }
261    }
262}
263
264fn to_io_result(value: c_int) -> io::Result<c_int> {
265    if value == -1 {
266        Err(io::Error::last_os_error())
267    } else {
268        Ok(value)
269    }
270}
271
272/// `ttyname_r` returns the path to the terminal device.
273#[cfg(not(target_os = "macos"))]
274fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
275    let mut buf = Vec::with_capacity(64);
276
277    loop {
278        // SAFETY: We pass the capacity of our vec to ttyname_r.
279        let code = unsafe { libc::ttyname_r(fd.as_raw_fd(), buf.as_mut_ptr(), buf.capacity()) };
280        match code {
281            // SAFETY: We own the pointer and we know that if ttyname_r is successful, it returns a null-terminated string.
282            0 => return Ok(unsafe { CStr::from_ptr(buf.as_ptr()) }.to_owned()),
283            libc::ERANGE => buf.reserve(64),
284            code => return Err(io::Error::from_raw_os_error(code)),
285        }
286    }
287}
288
289/// macOS does not have `ttyname_r` (the race free version), so we have to resort to `fcntl`.
290#[cfg(target_os = "macos")]
291fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
292    use libc::{F_GETPATH, PATH_MAX};
293
294    // the buffer size must be >= MAXPATHLEN, see `man fcntl`
295    let buf: [i8; PATH_MAX as usize] = [0; PATH_MAX as usize];
296
297    unsafe {
298        match fcntl(fd.as_raw_fd(), F_GETPATH as c_int, &buf) {
299            0 => {
300                let res = CStr::from_ptr(buf.as_ptr()).to_owned();
301                Ok(res)
302            }
303            _ => Err(io::Error::last_os_error()),
304        }
305    }
306}