1use std::io;
7use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
8use std::pin::Pin;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::task::{Context, Poll};
12
13use rustix::fs::{OFlags, fcntl_setfl};
14use rustix::pty::{OpenptFlags, grantpt, openpt, ptsname, unlockpt};
15#[cfg(not(target_os = "macos"))]
16use rustix::termios::{Winsize, tcsetwinsize};
17use tokio::io::unix::AsyncFd;
18use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
19
20use crate::config::WindowSize;
21use crate::error::{PtyError, Result};
22use crate::traits::PtyMaster;
23
24pub struct UnixPtyMaster {
29 async_fd: AsyncFd<OwnedFd>,
31 open: Arc<AtomicBool>,
33 #[cfg(target_os = "macos")]
40 drain: Option<macos_drain::Drain>,
41}
42
43impl std::fmt::Debug for UnixPtyMaster {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("UnixPtyMaster")
46 .field("fd", &self.async_fd.as_raw_fd())
47 .field("open", &self.open.load(Ordering::SeqCst))
48 .finish()
49 }
50}
51
52impl UnixPtyMaster {
53 pub fn open() -> Result<(Self, String)> {
61 #[cfg(target_os = "linux")]
70 let master_fd = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
71 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
72 #[cfg(not(target_os = "linux"))]
73 let master_fd = {
74 let fd = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY)
75 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
76 let flags = rustix::io::fcntl_getfd(&fd)
77 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
78 rustix::io::fcntl_setfd(&fd, flags | rustix::io::FdFlags::CLOEXEC)
79 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
80 fd
81 };
82
83 grantpt(&master_fd)
85 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
86
87 unlockpt(&master_fd)
89 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
90
91 let slave_name = ptsname(&master_fd, Vec::new())
93 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
94 let slave_path = slave_name
95 .to_str()
96 .map_err(|_| {
97 PtyError::Create(io::Error::new(
98 io::ErrorKind::InvalidData,
99 "invalid slave path encoding",
100 ))
101 })?
102 .to_string();
103
104 fcntl_setfl(&master_fd, OFlags::NONBLOCK)
106 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
107
108 let async_fd = AsyncFd::new(master_fd).map_err(PtyError::Create)?;
110
111 Ok((
112 Self {
113 async_fd,
114 open: Arc::new(AtomicBool::new(true)),
115 #[cfg(target_os = "macos")]
116 drain: None,
117 },
118 slave_path,
119 ))
120 }
121
122 #[cfg(target_os = "macos")]
139 pub(crate) fn start_read_drain(&mut self) -> Result<()> {
140 if self.drain.is_none() {
141 let drain =
142 macos_drain::Drain::start(self.async_fd.as_raw_fd()).map_err(PtyError::Io)?;
143 self.drain = Some(drain);
144 }
145 Ok(())
146 }
147
148 pub fn slave_name(&self) -> Result<String> {
152 let name = ptsname(self.async_fd.get_ref(), Vec::new())
153 .map_err(|e| PtyError::Io(io::Error::from_raw_os_error(e.raw_os_error())))?;
154 name.to_str()
155 .map(std::string::ToString::to_string)
156 .map_err(|_| {
157 PtyError::Io(io::Error::new(
158 io::ErrorKind::InvalidData,
159 "invalid slave path encoding",
160 ))
161 })
162 }
163
164 #[must_use]
166 pub fn is_open(&self) -> bool {
167 self.open.load(Ordering::SeqCst)
168 }
169
170 pub fn set_window_size(&self, size: WindowSize) -> Result<()> {
172 if !self.is_open() {
173 return Err(PtyError::Closed);
174 }
175
176 #[cfg(target_os = "macos")]
178 {
179 #[allow(clippy::struct_field_names)]
180 #[repr(C)]
181 struct LibcWinsize {
182 ws_row: libc::c_ushort,
183 ws_col: libc::c_ushort,
184 ws_xpixel: libc::c_ushort,
185 ws_ypixel: libc::c_ushort,
186 }
187
188 let winsize = LibcWinsize {
189 ws_row: size.rows,
190 ws_col: size.cols,
191 ws_xpixel: size.xpixel,
192 ws_ypixel: size.ypixel,
193 };
194
195 #[allow(unsafe_code)]
198 let result = unsafe {
199 libc::ioctl(
200 self.async_fd.as_raw_fd(),
201 libc::TIOCSWINSZ,
202 &raw const winsize,
203 )
204 };
205
206 if result == -1 {
207 return Err(PtyError::Resize(io::Error::last_os_error()));
208 }
209 Ok(())
210 }
211
212 #[cfg(not(target_os = "macos"))]
214 {
215 let winsize = Winsize {
216 ws_col: size.cols,
217 ws_row: size.rows,
218 ws_xpixel: size.xpixel,
219 ws_ypixel: size.ypixel,
220 };
221
222 tcsetwinsize(self.async_fd.get_ref(), winsize)
223 .map_err(|e| PtyError::Resize(io::Error::from_raw_os_error(e.raw_os_error())))
224 }
225 }
226
227 pub fn get_window_size(&self) -> Result<WindowSize> {
229 if !self.is_open() {
230 return Err(PtyError::Closed);
231 }
232
233 let winsize = rustix::termios::tcgetwinsize(self.async_fd.get_ref())
234 .map_err(|e| PtyError::GetAttributes(io::Error::from_raw_os_error(e.raw_os_error())))?;
235
236 Ok(WindowSize {
237 cols: winsize.ws_col,
238 rows: winsize.ws_row,
239 xpixel: winsize.ws_xpixel,
240 ypixel: winsize.ws_ypixel,
241 })
242 }
243
244 pub fn close(&mut self) -> Result<()> {
246 self.open.store(false, Ordering::SeqCst);
247 Ok(())
248 }
249}
250
251impl AsRawFd for UnixPtyMaster {
252 fn as_raw_fd(&self) -> RawFd {
253 self.async_fd.as_raw_fd()
254 }
255}
256
257impl AsyncRead for UnixPtyMaster {
258 fn poll_read(
259 self: Pin<&mut Self>,
260 cx: &mut Context<'_>,
261 buf: &mut ReadBuf<'_>,
262 ) -> Poll<io::Result<()>> {
263 if !self.open.load(Ordering::SeqCst) {
264 return Poll::Ready(Ok(())); }
266
267 #[cfg(target_os = "macos")]
270 if let Some(drain) = self.drain.as_ref() {
271 return drain.poll_read(cx.waker(), buf);
272 }
273
274 loop {
275 let mut guard = match self.async_fd.poll_read_ready(cx) {
276 Poll::Ready(Ok(guard)) => guard,
277 Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
278 Poll::Pending => return Poll::Pending,
279 };
280
281 let unfilled = buf.initialize_unfilled();
282 match rustix::io::read(self.async_fd.get_ref(), unfilled) {
283 Ok(0) => {
284 return Poll::Ready(Ok(()));
286 }
287 Ok(n) => {
288 buf.advance(n);
289 return Poll::Ready(Ok(()));
290 }
291 Err(rustix::io::Errno::AGAIN) => {
292 guard.clear_ready();
293 }
294 Err(rustix::io::Errno::INTR) => {
295 }
299 Err(e) => {
300 return Poll::Ready(Err(io::Error::from_raw_os_error(e.raw_os_error())));
301 }
302 }
303 }
304 }
305}
306
307impl AsyncWrite for UnixPtyMaster {
308 fn poll_write(
309 self: Pin<&mut Self>,
310 cx: &mut Context<'_>,
311 buf: &[u8],
312 ) -> Poll<io::Result<usize>> {
313 if !self.open.load(Ordering::SeqCst) {
314 return Poll::Ready(Err(io::Error::new(io::ErrorKind::BrokenPipe, "PTY closed")));
315 }
316
317 loop {
318 let mut guard = match self.async_fd.poll_write_ready(cx) {
319 Poll::Ready(Ok(guard)) => guard,
320 Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
321 Poll::Pending => return Poll::Pending,
322 };
323
324 match rustix::io::write(self.async_fd.get_ref(), buf) {
325 Ok(n) => return Poll::Ready(Ok(n)),
326 Err(rustix::io::Errno::AGAIN) => {
327 guard.clear_ready();
328 }
329 Err(rustix::io::Errno::INTR) => {
330 }
334 Err(e) => {
335 return Poll::Ready(Err(io::Error::from_raw_os_error(e.raw_os_error())));
336 }
337 }
338 }
339 }
340
341 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
342 Poll::Ready(Ok(()))
343 }
344
345 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
346 self.open.store(false, Ordering::SeqCst);
347 Poll::Ready(Ok(()))
348 }
349}
350
351impl PtyMaster for UnixPtyMaster {
352 fn resize(&self, size: WindowSize) -> Result<()> {
353 self.set_window_size(size)
354 }
355
356 fn window_size(&self) -> Result<WindowSize> {
357 self.get_window_size()
358 }
359
360 fn close(&mut self) -> Result<()> {
361 Self::close(self)
362 }
363
364 fn is_open(&self) -> bool {
365 Self::is_open(self)
366 }
367
368 fn as_raw_fd(&self) -> RawFd {
369 AsRawFd::as_raw_fd(self)
370 }
371}
372
373pub fn open_slave(path: &str) -> Result<OwnedFd> {
379 use std::path::Path;
380
381 use rustix::fs::{Mode, OFlags, open};
382
383 let fd = open(
388 Path::new(path),
389 OFlags::RDWR | OFlags::NOCTTY | OFlags::CLOEXEC,
390 Mode::empty(),
391 )
392 .map_err(|e| PtyError::Create(io::Error::from_raw_os_error(e.raw_os_error())))?;
393
394 Ok(fd)
395}
396
397#[cfg(target_os = "macos")]
423pub(crate) mod macos_drain {
424 use std::collections::VecDeque;
425 use std::io;
426 use std::os::unix::io::RawFd;
427 use std::sync::atomic::{AtomicBool, Ordering};
428 use std::sync::{Arc, Mutex};
429 use std::task::{Poll, Waker};
430
431 use tokio::io::ReadBuf;
432
433 const BUFFER_CAP: usize = 1 << 20; struct Shared {
439 buf: VecDeque<u8>,
440 eof: bool,
441 err: Option<io::ErrorKind>,
442 waker: Option<Waker>,
443 }
444
445 pub(crate) struct Drain {
447 shared: Arc<Mutex<Shared>>,
448 stop: Arc<AtomicBool>,
449 }
450
451 impl Drain {
452 #[allow(unsafe_code)]
454 pub(crate) fn start(master_fd: RawFd) -> io::Result<Self> {
455 let read_fd = unsafe { libc::dup(master_fd) };
460 if read_fd < 0 {
461 return Err(io::Error::last_os_error());
462 }
463 unsafe {
466 libc::fcntl(read_fd, libc::F_SETFD, libc::FD_CLOEXEC);
467 }
468
469 let shared = Arc::new(Mutex::new(Shared {
470 buf: VecDeque::new(),
471 eof: false,
472 err: None,
473 waker: None,
474 }));
475 let stop = Arc::new(AtomicBool::new(false));
476
477 let thread_shared = Arc::clone(&shared);
478 let thread_stop = Arc::clone(&stop);
479 std::thread::Builder::new()
483 .name("pty-macos-drain".into())
484 .spawn(move || drain_loop(read_fd, &thread_shared, &thread_stop))
485 .inspect_err(|_e| {
486 unsafe { libc::close(read_fd) };
488 })?;
489
490 Ok(Self { shared, stop })
491 }
492
493 pub(crate) fn poll_read(
495 &self,
496 waker: &Waker,
497 buf: &mut ReadBuf<'_>,
498 ) -> Poll<io::Result<()>> {
499 let mut sh = self.shared.lock().unwrap();
500 if !sh.buf.is_empty() {
501 let n = sh.buf.len().min(buf.remaining());
502 let (front, back) = sh.buf.as_slices();
504 let take_front = front.len().min(n);
505 buf.put_slice(&front[..take_front]);
506 let take_back = n - take_front;
507 if take_back > 0 {
508 buf.put_slice(&back[..take_back]);
509 }
510 sh.buf.drain(..n);
511 return Poll::Ready(Ok(()));
512 }
513 if let Some(kind) = sh.err {
514 return Poll::Ready(Err(kind.into()));
515 }
516 if sh.eof {
517 return Poll::Ready(Ok(()));
519 }
520 sh.waker = Some(waker.clone());
521 Poll::Pending
522 }
523 }
524
525 impl Drop for Drain {
526 fn drop(&mut self) {
527 self.stop.store(true, Ordering::Relaxed);
528 }
529 }
530
531 fn push(shared: &Arc<Mutex<Shared>>, data: &[u8]) {
534 let mut sh = shared.lock().unwrap();
535 sh.buf.extend(data);
536 let waker = sh.waker.take();
537 drop(sh);
538 if let Some(w) = waker {
539 w.wake();
540 }
541 }
542
543 fn finish(shared: &Arc<Mutex<Shared>>, err: Option<io::ErrorKind>) {
546 let mut sh = shared.lock().unwrap();
547 match err {
548 Some(kind) if sh.err.is_none() => sh.err = Some(kind),
549 Some(_) => {}
550 None => sh.eof = true,
551 }
552 let waker = sh.waker.take();
553 drop(sh);
554 if let Some(w) = waker {
555 w.wake();
556 }
557 }
558
559 #[allow(unsafe_code)]
560 fn drain_loop(fd: RawFd, shared: &Arc<Mutex<Shared>>, stop: &Arc<AtomicBool>) {
561 use std::cmp::Ordering as CmpOrdering;
562
563 let mut tmp = [0u8; 4096];
564 'outer: loop {
565 if stop.load(Ordering::Relaxed) {
566 break;
567 }
568 let full = { shared.lock().unwrap().buf.len() >= BUFFER_CAP };
571 if full {
572 std::thread::sleep(std::time::Duration::from_millis(2));
573 continue;
574 }
575 let mut pfd = libc::pollfd {
576 fd,
577 events: libc::POLLIN,
578 revents: 0,
579 };
580 let pr = unsafe { libc::poll(std::ptr::from_mut(&mut pfd), 1, 50) };
583 if stop.load(Ordering::Relaxed) {
584 break;
585 }
586 if pr < 0 {
587 let e = io::Error::last_os_error();
588 if e.raw_os_error() == Some(libc::EINTR) {
589 continue;
590 }
591 finish(shared, Some(e.kind()));
592 break;
593 }
594 if pr == 0 {
595 continue; }
597 loop {
599 let n = unsafe { libc::read(fd, tmp.as_mut_ptr().cast(), tmp.len()) };
602 match n.cmp(&0) {
603 CmpOrdering::Greater => {
604 push(shared, &tmp[..n as usize]);
606 }
607 CmpOrdering::Equal => {
608 finish(shared, None); break 'outer;
610 }
611 CmpOrdering::Less => {
612 let e = io::Error::last_os_error();
613 match e.raw_os_error() {
614 Some(libc::EAGAIN) => break, Some(libc::EINTR) => {} _ => {
617 finish(shared, Some(e.kind()));
618 break 'outer;
619 }
620 }
621 }
622 }
623 }
624 }
625 unsafe { libc::close(fd) };
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 #[tokio::test]
635 async fn open_pty() {
636 let result = UnixPtyMaster::open();
637 assert!(result.is_ok());
638
639 let (master, slave_path) = result.unwrap();
640 assert!(master.is_open());
641 assert!(
643 slave_path.starts_with("/dev/pts/")
644 || slave_path.starts_with("/dev/ttys")
645 || slave_path.starts_with("/dev/ttyp")
646 || slave_path.starts_with("/dev/pty")
647 );
648 }
649
650 #[tokio::test]
654 async fn master_is_close_on_exec() {
655 let (master, _slave_path) = UnixPtyMaster::open().expect("open");
656 let flags = rustix::io::fcntl_getfd(master.async_fd.get_ref()).expect("F_GETFD");
657 assert!(
658 flags.contains(rustix::io::FdFlags::CLOEXEC),
659 "master fd is missing FD_CLOEXEC"
660 );
661 }
662
663 #[tokio::test]
664 async fn window_size_operations() {
665 let (master, _slave_path) = UnixPtyMaster::open().unwrap();
666
667 #[cfg(target_os = "macos")]
669 let _slave_fd = open_slave(&_slave_path).unwrap();
670
671 let size = WindowSize::new(120, 40);
673 assert!(master.set_window_size(size).is_ok());
674
675 let retrieved = master.get_window_size().unwrap();
677 assert_eq!(retrieved.cols, 120);
678 assert_eq!(retrieved.rows, 40);
679 }
680
681 #[tokio::test]
682 async fn close_pty() {
683 let (mut master, _) = UnixPtyMaster::open().unwrap();
684 assert!(master.is_open());
685
686 master.close().unwrap();
687 assert!(!master.is_open());
688 }
689}