use std::io;
use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Poll};
use rustix::fs::{OFlags, fcntl_setfl};
use rustix::pty::{OpenptFlags, grantpt, openpt, ptsname, unlockpt};
#[cfg(not(target_os = "macos"))]
use rustix::termios::{Winsize, tcsetwinsize};
use tokio::io::unix::AsyncFd;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use crate::config::WindowSize;
use crate::error::{PtyError, Result};
use crate::traits::PtyMaster;
pub struct UnixPtyMaster {
async_fd: AsyncFd<OwnedFd>,
open: Arc<AtomicBool>,
#[cfg(target_os = "macos")]
drain: Option<macos_drain::Drain>,
}
impl std::fmt::Debug for UnixPtyMaster {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UnixPtyMaster")
.field("fd", &self.async_fd.as_raw_fd())
.field("open", &self.open.load(Ordering::SeqCst))
.finish()
}
}
impl UnixPtyMaster {
pub fn open() -> Result<(Self, String)> {
#[cfg(target_os = "linux")]
let master_fd = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
#[cfg(not(target_os = "linux"))]
let master_fd = {
let fd = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY)
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
let flags = rustix::io::fcntl_getfd(&fd)
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
rustix::io::fcntl_setfd(&fd, flags | rustix::io::FdFlags::CLOEXEC)
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
fd
};
grantpt(&master_fd)
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
unlockpt(&master_fd)
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
let slave_name = ptsname(&master_fd, Vec::new())
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
let slave_path = slave_name
.to_str()
.map_err(|_| {
PtyError::Create(io::Error::new(
io::ErrorKind::InvalidData,
"invalid slave path encoding",
))
})?
.to_string();
fcntl_setfl(&master_fd, OFlags::NONBLOCK)
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
let async_fd = AsyncFd::new(master_fd).map_err(PtyError::Create)?;
Ok((
Self {
async_fd,
open: Arc::new(AtomicBool::new(true)),
#[cfg(target_os = "macos")]
drain: None,
},
slave_path,
))
}
#[cfg(target_os = "macos")]
pub(crate) fn start_read_drain(&mut self) -> Result<()> {
if self.drain.is_none() {
let drain =
macos_drain::Drain::start(self.async_fd.as_raw_fd()).map_err(PtyError::Io)?;
self.drain = Some(drain);
}
Ok(())
}
pub fn slave_name(&self) -> Result<String> {
let name = ptsname(self.async_fd.get_ref(), Vec::new())
.map_err(|e| PtyError::Io(io::Error::from_raw_os_error(e.raw_os_error())))?;
name.to_str()
.map(std::string::ToString::to_string)
.map_err(|_| {
PtyError::Io(io::Error::new(
io::ErrorKind::InvalidData,
"invalid slave path encoding",
))
})
}
#[must_use]
pub fn is_open(&self) -> bool {
self.open.load(Ordering::SeqCst)
}
pub fn set_window_size(&self, size: WindowSize) -> Result<()> {
if !self.is_open() {
return Err(PtyError::Closed);
}
#[cfg(target_os = "macos")]
{
#[allow(clippy::struct_field_names)]
#[repr(C)]
struct LibcWinsize {
ws_row: libc::c_ushort,
ws_col: libc::c_ushort,
ws_xpixel: libc::c_ushort,
ws_ypixel: libc::c_ushort,
}
let winsize = LibcWinsize {
ws_row: size.rows,
ws_col: size.cols,
ws_xpixel: size.xpixel,
ws_ypixel: size.ypixel,
};
#[allow(unsafe_code)]
let result = unsafe {
libc::ioctl(
self.async_fd.as_raw_fd(),
libc::TIOCSWINSZ,
&raw const winsize,
)
};
if result == -1 {
return Err(PtyError::Resize(io::Error::last_os_error()));
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
{
let winsize = Winsize {
ws_col: size.cols,
ws_row: size.rows,
ws_xpixel: size.xpixel,
ws_ypixel: size.ypixel,
};
tcsetwinsize(self.async_fd.get_ref(), winsize)
.map_err(|e| PtyError::Resize(io::Error::from_raw_os_error(e.raw_os_error())))
}
}
pub fn get_window_size(&self) -> Result<WindowSize> {
if !self.is_open() {
return Err(PtyError::Closed);
}
let winsize = rustix::termios::tcgetwinsize(self.async_fd.get_ref())
.map_err(|e| PtyError::GetAttributes(io::Error::from_raw_os_error(e.raw_os_error())))?;
Ok(WindowSize {
cols: winsize.ws_col,
rows: winsize.ws_row,
xpixel: winsize.ws_xpixel,
ypixel: winsize.ws_ypixel,
})
}
pub fn close(&mut self) -> Result<()> {
self.open.store(false, Ordering::SeqCst);
Ok(())
}
}
impl AsRawFd for UnixPtyMaster {
fn as_raw_fd(&self) -> RawFd {
self.async_fd.as_raw_fd()
}
}
impl AsyncRead for UnixPtyMaster {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
if !self.open.load(Ordering::SeqCst) {
return Poll::Ready(Ok(())); }
#[cfg(target_os = "macos")]
if let Some(drain) = self.drain.as_ref() {
return drain.poll_read(cx.waker(), buf);
}
loop {
let mut guard = match self.async_fd.poll_read_ready(cx) {
Poll::Ready(Ok(guard)) => guard,
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
};
let unfilled = buf.initialize_unfilled();
match rustix::io::read(self.async_fd.get_ref(), unfilled) {
Ok(0) => {
return Poll::Ready(Ok(()));
}
Ok(n) => {
buf.advance(n);
return Poll::Ready(Ok(()));
}
Err(rustix::io::Errno::AGAIN) => {
guard.clear_ready();
}
Err(rustix::io::Errno::INTR) => {
}
Err(e) => {
return Poll::Ready(Err(io::Error::from_raw_os_error(e.raw_os_error())));
}
}
}
}
}
impl AsyncWrite for UnixPtyMaster {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
if !self.open.load(Ordering::SeqCst) {
return Poll::Ready(Err(io::Error::new(io::ErrorKind::BrokenPipe, "PTY closed")));
}
loop {
let mut guard = match self.async_fd.poll_write_ready(cx) {
Poll::Ready(Ok(guard)) => guard,
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
};
match rustix::io::write(self.async_fd.get_ref(), buf) {
Ok(n) => return Poll::Ready(Ok(n)),
Err(rustix::io::Errno::AGAIN) => {
guard.clear_ready();
}
Err(rustix::io::Errno::INTR) => {
}
Err(e) => {
return Poll::Ready(Err(io::Error::from_raw_os_error(e.raw_os_error())));
}
}
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.open.store(false, Ordering::SeqCst);
Poll::Ready(Ok(()))
}
}
impl PtyMaster for UnixPtyMaster {
fn resize(&self, size: WindowSize) -> Result<()> {
self.set_window_size(size)
}
fn window_size(&self) -> Result<WindowSize> {
self.get_window_size()
}
fn close(&mut self) -> Result<()> {
Self::close(self)
}
fn is_open(&self) -> bool {
Self::is_open(self)
}
fn as_raw_fd(&self) -> RawFd {
AsRawFd::as_raw_fd(self)
}
}
pub fn open_slave(path: &str) -> Result<OwnedFd> {
use std::path::Path;
use rustix::fs::{Mode, OFlags, open};
let fd = open(
Path::new(path),
OFlags::RDWR | OFlags::NOCTTY | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
Ok(fd)
}
#[cfg(target_os = "macos")]
pub(crate) mod macos_drain {
use std::collections::VecDeque;
use std::io;
use std::os::unix::io::RawFd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Poll, Waker};
use tokio::io::ReadBuf;
const BUFFER_CAP: usize = 1 << 20;
struct Shared {
buf: VecDeque<u8>,
eof: bool,
err: Option<io::ErrorKind>,
waker: Option<Waker>,
}
pub(crate) struct Drain {
shared: Arc<Mutex<Shared>>,
stop: Arc<AtomicBool>,
}
impl Drain {
#[allow(unsafe_code)]
pub(crate) fn start(master_fd: RawFd) -> io::Result<Self> {
let read_fd = unsafe { libc::dup(master_fd) };
if read_fd < 0 {
return Err(io::Error::last_os_error());
}
unsafe {
libc::fcntl(read_fd, libc::F_SETFD, libc::FD_CLOEXEC);
}
let shared = Arc::new(Mutex::new(Shared {
buf: VecDeque::new(),
eof: false,
err: None,
waker: None,
}));
let stop = Arc::new(AtomicBool::new(false));
let thread_shared = Arc::clone(&shared);
let thread_stop = Arc::clone(&stop);
std::thread::Builder::new()
.name("pty-macos-drain".into())
.spawn(move || drain_loop(read_fd, &thread_shared, &thread_stop))
.inspect_err(|_e| {
unsafe { libc::close(read_fd) };
})?;
Ok(Self { shared, stop })
}
pub(crate) fn poll_read(
&self,
waker: &Waker,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let mut sh = self.shared.lock().unwrap();
if !sh.buf.is_empty() {
let n = sh.buf.len().min(buf.remaining());
let (front, back) = sh.buf.as_slices();
let take_front = front.len().min(n);
buf.put_slice(&front[..take_front]);
let take_back = n - take_front;
if take_back > 0 {
buf.put_slice(&back[..take_back]);
}
sh.buf.drain(..n);
return Poll::Ready(Ok(()));
}
if let Some(kind) = sh.err {
return Poll::Ready(Err(kind.into()));
}
if sh.eof {
return Poll::Ready(Ok(()));
}
sh.waker = Some(waker.clone());
Poll::Pending
}
}
impl Drop for Drain {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
}
}
fn push(shared: &Arc<Mutex<Shared>>, data: &[u8]) {
let mut sh = shared.lock().unwrap();
sh.buf.extend(data);
let waker = sh.waker.take();
drop(sh);
if let Some(w) = waker {
w.wake();
}
}
fn finish(shared: &Arc<Mutex<Shared>>, err: Option<io::ErrorKind>) {
let mut sh = shared.lock().unwrap();
match err {
Some(kind) if sh.err.is_none() => sh.err = Some(kind),
Some(_) => {}
None => sh.eof = true,
}
let waker = sh.waker.take();
drop(sh);
if let Some(w) = waker {
w.wake();
}
}
#[allow(unsafe_code)]
fn drain_loop(fd: RawFd, shared: &Arc<Mutex<Shared>>, stop: &Arc<AtomicBool>) {
use std::cmp::Ordering as CmpOrdering;
let mut tmp = [0u8; 4096];
'outer: loop {
if stop.load(Ordering::Relaxed) {
break;
}
let full = { shared.lock().unwrap().buf.len() >= BUFFER_CAP };
if full {
std::thread::sleep(std::time::Duration::from_millis(2));
continue;
}
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let pr = unsafe { libc::poll(std::ptr::from_mut(&mut pfd), 1, 50) };
if stop.load(Ordering::Relaxed) {
break;
}
if pr < 0 {
let e = io::Error::last_os_error();
if e.raw_os_error() == Some(libc::EINTR) {
continue;
}
finish(shared, Some(e.kind()));
break;
}
if pr == 0 {
continue; }
loop {
let n = unsafe { libc::read(fd, tmp.as_mut_ptr().cast(), tmp.len()) };
match n.cmp(&0) {
CmpOrdering::Greater => {
push(shared, &tmp[..n as usize]);
}
CmpOrdering::Equal => {
finish(shared, None); break 'outer;
}
CmpOrdering::Less => {
let e = io::Error::last_os_error();
match e.raw_os_error() {
Some(libc::EAGAIN) => break, Some(libc::EINTR) => {} _ => {
finish(shared, Some(e.kind()));
break 'outer;
}
}
}
}
}
}
unsafe { libc::close(fd) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn open_pty() {
let result = UnixPtyMaster::open();
assert!(result.is_ok());
let (master, slave_path) = result.unwrap();
assert!(master.is_open());
assert!(
slave_path.starts_with("/dev/pts/")
|| slave_path.starts_with("/dev/ttys")
|| slave_path.starts_with("/dev/ttyp")
|| slave_path.starts_with("/dev/pty")
);
}
#[tokio::test]
async fn master_is_close_on_exec() {
let (master, _slave_path) = UnixPtyMaster::open().expect("open");
let flags = rustix::io::fcntl_getfd(master.async_fd.get_ref()).expect("F_GETFD");
assert!(
flags.contains(rustix::io::FdFlags::CLOEXEC),
"master fd is missing FD_CLOEXEC"
);
}
#[tokio::test]
async fn window_size_operations() {
let (master, _slave_path) = UnixPtyMaster::open().unwrap();
#[cfg(target_os = "macos")]
let _slave_fd = open_slave(&_slave_path).unwrap();
let size = WindowSize::new(120, 40);
assert!(master.set_window_size(size).is_ok());
let retrieved = master.get_window_size().unwrap();
assert_eq!(retrieved.cols, 120);
assert_eq!(retrieved.rows, 40);
}
#[tokio::test]
async fn close_pty() {
let (mut master, _) = UnixPtyMaster::open().unwrap();
assert!(master.is_open());
master.close().unwrap();
assert!(!master.is_open());
}
}