portable_pty/
unix.rs

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