portable_pty/
unix.rs

1//! Working with pseudo-terminals
2
3use crate::{Child, CommandBuilder, MasterPty, PtyPair, PtySize, PtySystem, SlavePty};
4use anyhow::{bail, Error};
5use filedescriptor::FileDescriptor;
6use libc::{self, winsize};
7use std::cell::RefCell;
8use std::ffi::OsStr;
9use std::io::{Read, Write};
10use std::os::fd::AsFd;
11use std::os::unix::ffi::OsStrExt;
12use std::os::unix::io::{AsRawFd, FromRawFd};
13use std::os::unix::process::CommandExt;
14use std::path::PathBuf;
15use std::{io, mem, ptr};
16
17pub use std::os::unix::io::RawFd;
18
19#[derive(Default)]
20pub struct UnixPtySystem {}
21
22fn openpty(size: PtySize) -> anyhow::Result<(UnixMasterPty, UnixSlavePty)> {
23    let mut master: RawFd = -1;
24    let mut slave: RawFd = -1;
25
26    let mut size = winsize {
27        ws_row: size.rows,
28        ws_col: size.cols,
29        ws_xpixel: size.pixel_width,
30        ws_ypixel: size.pixel_height,
31    };
32
33    let result = unsafe {
34        // BSDish systems may require mut pointers to some args
35        #[allow(clippy::unnecessary_mut_passed)]
36        libc::openpty(
37            &mut master,
38            &mut slave,
39            ptr::null_mut(),
40            ptr::null_mut(),
41            &mut size,
42        )
43    };
44
45    if result != 0 {
46        bail!("failed to openpty: {:?}", io::Error::last_os_error());
47    }
48
49    let tty_name = tty_name(slave);
50
51    let master = UnixMasterPty {
52        fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(master) }),
53        took_writer: RefCell::new(false),
54        tty_name,
55    };
56    let slave = UnixSlavePty {
57        fd: PtyFd(unsafe { FileDescriptor::from_raw_fd(slave) }),
58    };
59
60    // Ensure that these descriptors will get closed when we execute
61    // the child process.  This is done after constructing the Pty
62    // instances so that we ensure that the Ptys get drop()'d if
63    // the cloexec() functions fail (unlikely!).
64    cloexec(master.fd.as_raw_fd())?;
65    cloexec(slave.fd.as_raw_fd())?;
66
67    Ok((master, slave))
68}
69
70impl PtySystem for UnixPtySystem {
71    fn openpty(&self, size: PtySize) -> anyhow::Result<PtyPair> {
72        let (master, slave) = openpty(size)?;
73        Ok(PtyPair {
74            master: Box::new(master),
75            slave: Box::new(slave),
76        })
77    }
78}
79
80struct PtyFd(pub FileDescriptor);
81impl std::ops::Deref for PtyFd {
82    type Target = FileDescriptor;
83    fn deref(&self) -> &FileDescriptor {
84        &self.0
85    }
86}
87impl std::ops::DerefMut for PtyFd {
88    fn deref_mut(&mut self) -> &mut FileDescriptor {
89        &mut self.0
90    }
91}
92
93impl Read for PtyFd {
94    fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
95        match self.0.read(buf) {
96            Err(ref e) if e.raw_os_error() == Some(libc::EIO) => {
97                // EIO indicates that the slave pty has been closed.
98                // Treat this as EOF so that std::io::Read::read_to_string
99                // and similar functions gracefully terminate when they
100                // encounter this condition
101                Ok(0)
102            }
103            x => x,
104        }
105    }
106}
107
108fn tty_name(fd: RawFd) -> Option<PathBuf> {
109    let mut buf = vec![0 as std::ffi::c_char; 128];
110
111    loop {
112        let res = unsafe { libc::ttyname_r(fd, buf.as_mut_ptr(), buf.len()) };
113
114        if res == libc::ERANGE {
115            if buf.len() > 64 * 1024 {
116                // on macOS, if the buf is "too big", ttyname_r can
117                // return ERANGE, even though that is supposed to
118                // indicate buf is "too small".
119                return None;
120            }
121            buf.resize(buf.len() * 2, 0 as std::ffi::c_char);
122            continue;
123        }
124
125        return if res == 0 {
126            let cstr = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) };
127            let osstr = OsStr::from_bytes(cstr.to_bytes());
128            Some(PathBuf::from(osstr))
129        } else {
130            None
131        };
132    }
133}
134
135/// On Big Sur, Cocoa leaks various file descriptors to child processes,
136/// so we need to make a pass through the open descriptors beyond just the
137/// stdio descriptors and close them all out.
138/// This is approximately equivalent to the darwin `posix_spawnattr_setflags`
139/// option POSIX_SPAWN_CLOEXEC_DEFAULT which is used as a bit of a cheat
140/// on macOS.
141/// On Linux, gnome/mutter leak shell extension fds to wezterm too, so we
142/// also need to make an effort to clean up the mess.
143///
144/// This function enumerates the open filedescriptors in the current process
145/// and then will forcibly call close(2) on each open fd that is numbered
146/// 3 or higher, effectively closing all descriptors except for the stdio
147/// streams.
148///
149/// The implementation of this function relies on `/dev/fd` being available
150/// to provide the list of open fds.  Any errors in enumerating or closing
151/// the fds are silently ignored.
152pub fn close_random_fds() {
153    // FreeBSD, macOS and presumably other BSDish systems have /dev/fd as
154    // a directory listing the current fd numbers for the process.
155    //
156    // On Linux, /dev/fd is a symlink to /proc/self/fd
157    if let Ok(dir) = std::fs::read_dir("/dev/fd") {
158        let mut fds = vec![];
159        for entry in dir {
160            if let Some(num) = entry
161                .ok()
162                .map(|e| e.file_name())
163                .and_then(|s| s.into_string().ok())
164                .and_then(|n| n.parse::<libc::c_int>().ok())
165            {
166                if num > 2 {
167                    fds.push(num);
168                }
169            }
170        }
171        for fd in fds {
172            unsafe {
173                libc::close(fd);
174            }
175        }
176    }
177}
178
179impl PtyFd {
180    fn resize(&self, size: PtySize) -> Result<(), Error> {
181        let ws_size = winsize {
182            ws_row: size.rows,
183            ws_col: size.cols,
184            ws_xpixel: size.pixel_width,
185            ws_ypixel: size.pixel_height,
186        };
187
188        if unsafe {
189            libc::ioctl(
190                self.0.as_raw_fd(),
191                libc::TIOCSWINSZ as _,
192                &ws_size as *const _,
193            )
194        } != 0
195        {
196            bail!(
197                "failed to ioctl(TIOCSWINSZ): {:?}",
198                io::Error::last_os_error()
199            );
200        }
201
202        Ok(())
203    }
204
205    fn get_size(&self) -> Result<PtySize, Error> {
206        let mut size: winsize = unsafe { mem::zeroed() };
207        if unsafe {
208            libc::ioctl(
209                self.0.as_raw_fd(),
210                libc::TIOCGWINSZ as _,
211                &mut size as *mut _,
212            )
213        } != 0
214        {
215            bail!(
216                "failed to ioctl(TIOCGWINSZ): {:?}",
217                io::Error::last_os_error()
218            );
219        }
220        Ok(PtySize {
221            rows: size.ws_row,
222            cols: size.ws_col,
223            pixel_width: size.ws_xpixel,
224            pixel_height: size.ws_ypixel,
225        })
226    }
227
228    fn spawn_command(&self, builder: CommandBuilder) -> anyhow::Result<std::process::Child> {
229        let configured_umask = builder.umask;
230
231        let mut cmd = builder.as_command()?;
232        let controlling_tty = builder.get_controlling_tty();
233
234        unsafe {
235            cmd.stdin(self.as_stdio()?)
236                .stdout(self.as_stdio()?)
237                .stderr(self.as_stdio()?)
238                .pre_exec(move || {
239                    // Clean up a few things before we exec the program
240                    // Clear out any potentially problematic signal
241                    // dispositions that we might have inherited
242                    for signo in &[
243                        libc::SIGCHLD,
244                        libc::SIGHUP,
245                        libc::SIGINT,
246                        libc::SIGQUIT,
247                        libc::SIGTERM,
248                        libc::SIGALRM,
249                    ] {
250                        libc::signal(*signo, libc::SIG_DFL);
251                    }
252
253                    let empty_set: libc::sigset_t = std::mem::zeroed();
254                    libc::sigprocmask(libc::SIG_SETMASK, &empty_set, std::ptr::null_mut());
255
256                    // Establish ourselves as a session leader.
257                    if libc::setsid() == -1 {
258                        return Err(io::Error::last_os_error());
259                    }
260
261                    // Clippy wants us to explicitly cast TIOCSCTTY using
262                    // type::from(), but the size and potentially signedness
263                    // are system dependent, which is why we're using `as _`.
264                    // Suppress this lint for this section of code.
265                    #[allow(clippy::cast_lossless)]
266                    if controlling_tty {
267                        // Set the pty as the controlling terminal.
268                        // Failure to do this means that delivery of
269                        // SIGWINCH won't happen when we resize the
270                        // terminal, among other undesirable effects.
271                        if libc::ioctl(0, libc::TIOCSCTTY as _, 0) == -1 {
272                            return Err(io::Error::last_os_error());
273                        }
274                    }
275
276                    close_random_fds();
277
278                    if let Some(mask) = configured_umask {
279                        libc::umask(mask);
280                    }
281
282                    Ok(())
283                })
284        };
285
286        let mut child = cmd.spawn()?;
287
288        // Ensure that we close out the slave fds that Child retains;
289        // they are not what we need (we need the master side to reference
290        // them) and won't work in the usual way anyway.
291        // In practice these are None, but it seems best to be move them
292        // out in case the behavior of Command changes in the future.
293        child.stdin.take();
294        child.stdout.take();
295        child.stderr.take();
296
297        Ok(child)
298    }
299}
300
301/// Represents the master end of a pty.
302/// The file descriptor will be closed when the Pty is dropped.
303struct UnixMasterPty {
304    fd: PtyFd,
305    took_writer: RefCell<bool>,
306    tty_name: Option<PathBuf>,
307}
308
309/// Represents the slave end of a pty.
310/// The file descriptor will be closed when the Pty is dropped.
311struct UnixSlavePty {
312    fd: PtyFd,
313}
314
315/// Helper function to set the close-on-exec flag for a raw descriptor
316fn cloexec(fd: RawFd) -> Result<(), Error> {
317    let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
318    if flags == -1 {
319        bail!(
320            "fcntl to read flags failed: {:?}",
321            io::Error::last_os_error()
322        );
323    }
324    let result = unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) };
325    if result == -1 {
326        bail!(
327            "fcntl to set CLOEXEC failed: {:?}",
328            io::Error::last_os_error()
329        );
330    }
331    Ok(())
332}
333
334impl SlavePty for UnixSlavePty {
335    fn spawn_command(
336        &self,
337        builder: CommandBuilder,
338    ) -> Result<Box<dyn Child + Send + Sync>, Error> {
339        Ok(Box::new(self.fd.spawn_command(builder)?))
340    }
341}
342
343impl MasterPty for UnixMasterPty {
344    fn resize(&self, size: PtySize) -> Result<(), Error> {
345        self.fd.resize(size)
346    }
347
348    fn get_size(&self) -> Result<PtySize, Error> {
349        self.fd.get_size()
350    }
351
352    fn try_clone_reader(&self) -> Result<Box<dyn Read + Send>, Error> {
353        let fd = PtyFd(self.fd.try_clone()?);
354        Ok(Box::new(fd))
355    }
356
357    fn take_writer(&self) -> Result<Box<dyn Write + Send>, Error> {
358        if *self.took_writer.borrow() {
359            anyhow::bail!("cannot take writer more than once");
360        }
361        *self.took_writer.borrow_mut() = true;
362        let fd = PtyFd(self.fd.try_clone()?);
363        Ok(Box::new(UnixMasterWriter { fd }))
364    }
365
366    fn as_raw_fd(&self) -> Option<RawFd> {
367        Some(self.fd.0.as_raw_fd())
368    }
369
370    fn tty_name(&self) -> Option<PathBuf> {
371        self.tty_name.clone()
372    }
373
374    fn process_group_leader(&self) -> Option<libc::pid_t> {
375        match unsafe { libc::tcgetpgrp(self.fd.0.as_raw_fd()) } {
376            pid if pid > 0 => Some(pid),
377            _ => None,
378        }
379    }
380
381    fn get_termios(&self) -> Option<nix::sys::termios::Termios> {
382        nix::sys::termios::tcgetattr(self.fd.0.as_fd()).ok()
383    }
384}
385
386/// Represents the master end of a pty.
387/// EOT will be sent, and then the file descriptor will be closed when
388/// the Pty is dropped.
389struct UnixMasterWriter {
390    fd: PtyFd,
391}
392
393impl Drop for UnixMasterWriter {
394    fn drop(&mut self) {
395        let mut t: libc::termios = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
396        if unsafe { libc::tcgetattr(self.fd.0.as_raw_fd(), &mut t) } == 0 {
397            // EOF is only interpreted after a newline, so if it is set,
398            // we send a newline followed by EOF.
399            let eot = t.c_cc[libc::VEOF];
400            if eot != 0 {
401                let _ = self.fd.0.write_all(&[b'\n', eot]);
402            }
403        }
404    }
405}
406
407impl Write for UnixMasterWriter {
408    fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
409        self.fd.write(buf)
410    }
411    fn flush(&mut self) -> Result<(), io::Error> {
412        self.fd.flush()
413    }
414}