use std::os::{ fd::{AsRawFd as _, FromRawFd as _}, unix::prelude::OsStrExt as _, }; #[derive(Debug)] pub struct Pty(std::os::fd::OwnedFd); impl Pty { pub fn open() -> crate::Result { let pt = rustix::pty::openpt( // can't use CLOEXEC here because it's linux-specific rustix::pty::OpenptFlags::RDWR | rustix::pty::OpenptFlags::NOCTTY, )?; rustix::pty::grantpt(&pt)?; rustix::pty::unlockpt(&pt)?; let mut flags = rustix::io::fcntl_getfd(&pt)?; flags |= rustix::io::FdFlags::CLOEXEC; rustix::io::fcntl_setfd(&pt, flags)?; Ok(Self(pt)) } pub fn set_term_size(&self, size: crate::Size) -> crate::Result<()> { let size = libc::winsize::from(size); let fd = self.0.as_raw_fd(); // TODO: upstream this to rustix let ret = unsafe { libc::ioctl(fd, libc::TIOCSWINSZ, std::ptr::addr_of!(size)) }; if ret == -1 { Err(rustix::io::Errno::from_raw_os_error( std::io::Error::last_os_error().raw_os_error().unwrap_or(0), ) .into()) } else { Ok(()) } } pub fn pts(&self) -> crate::Result { Ok(Pts(std::fs::OpenOptions::new() .read(true) .write(true) .open(std::ffi::OsStr::from_bytes( rustix::pty::ptsname(&self.0, vec![])?.as_bytes(), ))? .into())) } #[cfg(feature = "async")] pub fn set_nonblocking(&self) -> rustix::io::Result<()> { let mut opts = rustix::fs::fcntl_getfl(&self.0)?; opts |= rustix::fs::OFlags::NONBLOCK; rustix::fs::fcntl_setfl(&self.0, opts)?; Ok(()) } } impl From for std::os::fd::OwnedFd { fn from(pty: Pty) -> Self { let Pty(nix_ptymaster) = pty; let raw_fd = nix_ptymaster.as_raw_fd(); std::mem::forget(nix_ptymaster); // Safety: nix::pty::PtyMaster is required to contain a valid file // descriptor, and we ensured that the file descriptor will remain // valid by skipping the drop implementation for nix::pty::PtyMaster unsafe { Self::from_raw_fd(raw_fd) } } } impl std::os::fd::AsFd for Pty { fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { let raw_fd = self.0.as_raw_fd(); // Safety: nix::pty::PtyMaster is required to contain a valid file // descriptor, and it is owned by self unsafe { std::os::fd::BorrowedFd::borrow_raw(raw_fd) } } } impl std::os::fd::AsRawFd for Pty { fn as_raw_fd(&self) -> std::os::fd::RawFd { self.0.as_raw_fd() } } impl std::io::Read for Pty { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { rustix::io::read(&self.0, buf).map_err(std::io::Error::from) } } impl std::io::Write for Pty { fn write(&mut self, buf: &[u8]) -> std::io::Result { rustix::io::write(&self.0, buf).map_err(std::io::Error::from) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } impl std::io::Read for &Pty { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { rustix::io::read(&self.0, buf).map_err(std::io::Error::from) } } impl std::io::Write for &Pty { fn write(&mut self, buf: &[u8]) -> std::io::Result { rustix::io::write(&self.0, buf).map_err(std::io::Error::from) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } pub struct Pts(std::os::fd::OwnedFd); impl Pts { pub fn setup_subprocess( &self, ) -> std::io::Result<( std::process::Stdio, std::process::Stdio, std::process::Stdio, )> { Ok(( self.0.try_clone()?.into(), self.0.try_clone()?.into(), self.0.try_clone()?.into(), )) } pub fn session_leader(&self) -> impl FnMut() -> std::io::Result<()> { let pts_fd = self.0.as_raw_fd(); move || { rustix::process::setsid()?; rustix::process::ioctl_tiocsctty(unsafe { std::os::fd::BorrowedFd::borrow_raw(pts_fd) })?; Ok(()) } } } impl From for std::os::fd::OwnedFd { fn from(pts: Pts) -> Self { pts.0 } } impl std::os::fd::AsFd for Pts { fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { self.0.as_fd() } } impl std::os::fd::AsRawFd for Pts { fn as_raw_fd(&self) -> std::os::fd::RawFd { self.0.as_raw_fd() } }