1use std::io;
36use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
37use std::path::PathBuf;
38
39use anyhow::{Context, Result};
40use nix::libc;
41use nix::pty::{OpenptyResult, Winsize, openpty};
42use nix::unistd;
43use tokio::io::unix::AsyncFd;
44use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
45
46pub const DEFAULT_TERM: &str = "xterm-256color";
48
49pub const DEFAULT_COLS: u32 = 80;
51
52pub const DEFAULT_ROWS: u32 = 24;
54
55const MAX_DIMENSION: u32 = u16::MAX as u32;
57
58#[derive(Debug, Clone)]
62pub struct PtyConfig {
63 pub term: String,
65
66 pub col_width: u32,
68
69 pub row_height: u32,
71
72 pub pix_width: u32,
74
75 pub pix_height: u32,
77}
78
79impl PtyConfig {
80 pub fn new(
82 term: String,
83 col_width: u32,
84 row_height: u32,
85 pix_width: u32,
86 pix_height: u32,
87 ) -> Self {
88 Self {
89 term,
90 col_width,
91 row_height,
92 pix_width,
93 pix_height,
94 }
95 }
96
97 pub fn winsize(&self) -> Winsize {
101 Winsize {
102 ws_row: self.row_height.min(MAX_DIMENSION) as u16,
103 ws_col: self.col_width.min(MAX_DIMENSION) as u16,
104 ws_xpixel: self.pix_width.min(MAX_DIMENSION) as u16,
105 ws_ypixel: self.pix_height.min(MAX_DIMENSION) as u16,
106 }
107 }
108}
109
110impl Default for PtyConfig {
111 fn default() -> Self {
112 Self {
113 term: DEFAULT_TERM.to_string(),
114 col_width: DEFAULT_COLS,
115 row_height: DEFAULT_ROWS,
116 pix_width: 0,
117 pix_height: 0,
118 }
119 }
120}
121
122pub struct PtyMaster {
127 config: PtyConfig,
129
130 async_fd: AsyncFd<OwnedFd>,
132
133 slave_path: PathBuf,
135}
136
137impl PtyMaster {
138 pub fn open(config: PtyConfig) -> Result<Self> {
156 let OpenptyResult {
158 master: master_fd,
159 slave: slave_fd,
160 } = openpty(None, None).context("Failed to open PTY pair")?;
161
162 let slave_path =
164 unistd::ttyname(slave_fd.as_fd()).context("Failed to get slave TTY path")?;
165
166 Self::set_window_size_fd(slave_fd.as_fd(), &config.winsize())
168 .context("Failed to set initial window size")?;
169
170 drop(slave_fd);
172
173 Self::set_nonblocking(master_fd.as_fd())?;
175
176 let async_fd = AsyncFd::new(master_fd).context("Failed to create AsyncFd")?;
178
179 Ok(Self {
180 config,
181 async_fd,
182 slave_path,
183 })
184 }
185
186 pub fn slave_path(&self) -> &PathBuf {
191 &self.slave_path
192 }
193
194 pub fn config(&self) -> &PtyConfig {
196 &self.config
197 }
198
199 pub fn as_raw_fd(&self) -> RawFd {
201 self.async_fd.get_ref().as_raw_fd()
202 }
203
204 pub fn resize(&mut self, cols: u32, rows: u32) -> Result<()> {
215 self.config.col_width = cols;
216 self.config.row_height = rows;
217
218 let winsize = self.config.winsize();
219 Self::set_window_size_fd(self.async_fd.get_ref().as_fd(), &winsize)
220 }
221
222 fn set_window_size_fd(fd: BorrowedFd<'_>, winsize: &Winsize) -> Result<()> {
224 let result = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSWINSZ, winsize) };
226
227 if result < 0 {
228 Err(io::Error::last_os_error()).context("Failed to set window size (TIOCSWINSZ ioctl)")
229 } else {
230 Ok(())
231 }
232 }
233
234 fn set_nonblocking(fd: BorrowedFd<'_>) -> Result<()> {
236 let flags = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFL).context("F_GETFL")?;
238
239 let new_flags =
241 nix::fcntl::OFlag::from_bits_truncate(flags) | nix::fcntl::OFlag::O_NONBLOCK;
242
243 nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_SETFL(new_flags)).context("F_SETFL")?;
244
245 Ok(())
246 }
247
248 pub async fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
252 loop {
253 let mut guard = self.async_fd.readable().await?;
254
255 match guard.try_io(|inner| {
256 let fd = inner.get_ref().as_raw_fd();
257 let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
259 if n < 0 {
260 Err(io::Error::last_os_error())
261 } else {
262 Ok(n as usize)
263 }
264 }) {
265 Ok(result) => return result,
266 Err(_would_block) => continue,
267 }
268 }
269 }
270
271 pub async fn write(&self, buf: &[u8]) -> io::Result<usize> {
275 loop {
276 let mut guard = self.async_fd.writable().await?;
277
278 match guard.try_io(|inner| {
279 let fd = inner.get_ref().as_raw_fd();
280 let n = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
282 if n < 0 {
283 Err(io::Error::last_os_error())
284 } else {
285 Ok(n as usize)
286 }
287 }) {
288 Ok(result) => return result,
289 Err(_would_block) => continue,
290 }
291 }
292 }
293
294 pub async fn write_all(&self, mut buf: &[u8]) -> io::Result<()> {
298 while !buf.is_empty() {
299 let n = self.write(buf).await?;
300 buf = &buf[n..];
301 }
302 Ok(())
303 }
304}
305
306impl std::fmt::Debug for PtyMaster {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 f.debug_struct("PtyMaster")
309 .field("config", &self.config)
310 .field("slave_path", &self.slave_path)
311 .field("fd", &self.as_raw_fd())
312 .finish()
313 }
314}
315
316pub struct PtyReader<'a> {
320 pty: &'a PtyMaster,
321}
322
323impl<'a> PtyReader<'a> {
324 pub fn new(pty: &'a PtyMaster) -> Self {
326 Self { pty }
327 }
328}
329
330impl AsyncRead for PtyReader<'_> {
331 fn poll_read(
332 self: std::pin::Pin<&mut Self>,
333 cx: &mut std::task::Context<'_>,
334 buf: &mut ReadBuf<'_>,
335 ) -> std::task::Poll<io::Result<()>> {
336 loop {
337 let mut guard = match self.pty.async_fd.poll_read_ready(cx) {
338 std::task::Poll::Ready(Ok(guard)) => guard,
339 std::task::Poll::Ready(Err(e)) => return std::task::Poll::Ready(Err(e)),
340 std::task::Poll::Pending => return std::task::Poll::Pending,
341 };
342
343 let unfilled = buf.initialize_unfilled();
344 let fd = self.pty.async_fd.get_ref().as_raw_fd();
345
346 let result = unsafe { libc::read(fd, unfilled.as_mut_ptr() as *mut _, unfilled.len()) };
348
349 if result < 0 {
350 let err = io::Error::last_os_error();
351 if err.kind() == io::ErrorKind::WouldBlock {
352 guard.clear_ready();
353 continue;
354 }
355 return std::task::Poll::Ready(Err(err));
356 }
357
358 buf.advance(result as usize);
359 return std::task::Poll::Ready(Ok(()));
360 }
361 }
362}
363
364pub struct PtyWriter<'a> {
368 pty: &'a PtyMaster,
369}
370
371impl<'a> PtyWriter<'a> {
372 pub fn new(pty: &'a PtyMaster) -> Self {
374 Self { pty }
375 }
376}
377
378impl AsyncWrite for PtyWriter<'_> {
379 fn poll_write(
380 self: std::pin::Pin<&mut Self>,
381 cx: &mut std::task::Context<'_>,
382 buf: &[u8],
383 ) -> std::task::Poll<io::Result<usize>> {
384 loop {
385 let mut guard = match self.pty.async_fd.poll_write_ready(cx) {
386 std::task::Poll::Ready(Ok(guard)) => guard,
387 std::task::Poll::Ready(Err(e)) => return std::task::Poll::Ready(Err(e)),
388 std::task::Poll::Pending => return std::task::Poll::Pending,
389 };
390
391 let fd = self.pty.async_fd.get_ref().as_raw_fd();
392
393 let result = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
395
396 if result < 0 {
397 let err = io::Error::last_os_error();
398 if err.kind() == io::ErrorKind::WouldBlock {
399 guard.clear_ready();
400 continue;
401 }
402 return std::task::Poll::Ready(Err(err));
403 }
404
405 return std::task::Poll::Ready(Ok(result as usize));
406 }
407 }
408
409 fn poll_flush(
410 self: std::pin::Pin<&mut Self>,
411 _cx: &mut std::task::Context<'_>,
412 ) -> std::task::Poll<io::Result<()>> {
413 std::task::Poll::Ready(Ok(()))
415 }
416
417 fn poll_shutdown(
418 self: std::pin::Pin<&mut Self>,
419 _cx: &mut std::task::Context<'_>,
420 ) -> std::task::Poll<io::Result<()>> {
421 std::task::Poll::Ready(Ok(()))
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_pty_config_default() {
432 let config = PtyConfig::default();
433
434 assert_eq!(config.term, DEFAULT_TERM);
435 assert_eq!(config.col_width, DEFAULT_COLS);
436 assert_eq!(config.row_height, DEFAULT_ROWS);
437 assert_eq!(config.pix_width, 0);
438 assert_eq!(config.pix_height, 0);
439 }
440
441 #[test]
442 fn test_pty_config_new() {
443 let config = PtyConfig::new("vt100".to_string(), 132, 50, 1024, 768);
444
445 assert_eq!(config.term, "vt100");
446 assert_eq!(config.col_width, 132);
447 assert_eq!(config.row_height, 50);
448 assert_eq!(config.pix_width, 1024);
449 assert_eq!(config.pix_height, 768);
450 }
451
452 #[test]
453 fn test_pty_config_winsize() {
454 let config = PtyConfig::new("xterm".to_string(), 80, 24, 640, 480);
455 let winsize = config.winsize();
456
457 assert_eq!(winsize.ws_col, 80);
458 assert_eq!(winsize.ws_row, 24);
459 assert_eq!(winsize.ws_xpixel, 640);
460 assert_eq!(winsize.ws_ypixel, 480);
461 }
462
463 #[test]
464 fn test_pty_config_winsize_overflow_clamping() {
465 let config = PtyConfig::new("xterm".to_string(), 100_000, 100_000, 100_000, 100_000);
467 let winsize = config.winsize();
468
469 assert_eq!(winsize.ws_col, u16::MAX);
470 assert_eq!(winsize.ws_row, u16::MAX);
471 assert_eq!(winsize.ws_xpixel, u16::MAX);
472 assert_eq!(winsize.ws_ypixel, u16::MAX);
473 }
474
475 #[tokio::test]
476 async fn test_pty_master_open() {
477 let config = PtyConfig::default();
478 let result = PtyMaster::open(config);
479
480 assert!(result.is_ok(), "Failed to open PTY: {:?}", result.err());
482
483 let pty = result.unwrap();
484 assert!(pty.slave_path().exists());
485 assert!(pty.as_raw_fd() >= 0);
486 }
487
488 #[tokio::test]
489 async fn test_pty_master_resize() {
490 let config = PtyConfig::default();
491 let mut pty = PtyMaster::open(config).expect("Failed to open PTY");
492
493 assert!(pty.resize(120, 40).is_ok());
495 assert_eq!(pty.config().col_width, 120);
496 assert_eq!(pty.config().row_height, 40);
497 }
498
499 #[tokio::test]
500 async fn test_pty_master_read_write() {
501 use std::fs::OpenOptions;
502
503 let config = PtyConfig::default();
504 let pty = PtyMaster::open(config).expect("Failed to open PTY");
505
506 let slave_path = pty.slave_path();
509 let _slave = OpenOptions::new()
510 .read(true)
511 .write(true)
512 .open(slave_path)
513 .expect("Failed to open PTY slave");
514
515 let test_data = b"hello\n";
517 let write_result = pty.write(test_data).await;
518 assert!(
519 write_result.is_ok(),
520 "Write failed: {:?}",
521 write_result.err()
522 );
523
524 }
527
528 #[tokio::test]
529 async fn test_pty_master_debug() {
530 let config = PtyConfig::default();
531 let pty = PtyMaster::open(config).expect("Failed to open PTY");
532
533 let debug = format!("{:?}", pty);
534 assert!(debug.contains("PtyMaster"));
535 assert!(debug.contains("config"));
536 assert!(debug.contains("slave_path"));
537 }
538
539 #[test]
540 fn test_default_constants() {
541 assert_eq!(DEFAULT_TERM, "xterm-256color");
542 assert_eq!(DEFAULT_COLS, 80);
543 assert_eq!(DEFAULT_ROWS, 24);
544 }
545
546 #[test]
547 fn test_pty_config_clone() {
548 let config = PtyConfig::new("vt220".to_string(), 100, 30, 500, 400);
549 let cloned = config.clone();
550
551 assert_eq!(config.term, cloned.term);
552 assert_eq!(config.col_width, cloned.col_width);
553 assert_eq!(config.row_height, cloned.row_height);
554 assert_eq!(config.pix_width, cloned.pix_width);
555 assert_eq!(config.pix_height, cloned.pix_height);
556 }
557
558 #[test]
559 fn test_pty_config_debug() {
560 let config = PtyConfig::new("screen".to_string(), 200, 60, 1920, 1080);
561 let debug_str = format!("{:?}", config);
562
563 assert!(debug_str.contains("PtyConfig"));
564 assert!(debug_str.contains("screen"));
565 assert!(debug_str.contains("200"));
566 assert!(debug_str.contains("60"));
567 }
568
569 #[test]
570 fn test_winsize_boundary_values() {
571 let config = PtyConfig::new("xterm".to_string(), 0, 0, 0, 0);
573 let winsize = config.winsize();
574 assert_eq!(winsize.ws_col, 0);
575 assert_eq!(winsize.ws_row, 0);
576 assert_eq!(winsize.ws_xpixel, 0);
577 assert_eq!(winsize.ws_ypixel, 0);
578
579 let config = PtyConfig::new(
581 "xterm".to_string(),
582 u16::MAX as u32,
583 u16::MAX as u32,
584 u16::MAX as u32,
585 u16::MAX as u32,
586 );
587 let winsize = config.winsize();
588 assert_eq!(winsize.ws_col, u16::MAX);
589 assert_eq!(winsize.ws_row, u16::MAX);
590 assert_eq!(winsize.ws_xpixel, u16::MAX);
591 assert_eq!(winsize.ws_ypixel, u16::MAX);
592 }
593
594 #[tokio::test]
595 async fn test_pty_master_fd_validity() {
596 let config = PtyConfig::default();
597 let pty = PtyMaster::open(config).expect("Failed to open PTY");
598
599 let fd = pty.as_raw_fd();
601 assert!(
602 fd >= 0,
603 "PTY file descriptor should be valid (non-negative)"
604 );
605 }
606
607 #[tokio::test]
608 async fn test_pty_master_slave_path_format() {
609 let config = PtyConfig::default();
610 let pty = PtyMaster::open(config).expect("Failed to open PTY");
611
612 let slave_path = pty.slave_path();
613
614 let path_str = slave_path.to_string_lossy();
616 assert!(
617 path_str.starts_with("/dev/pts/") || path_str.starts_with("/dev/tty"),
618 "Slave path should be a PTY device: {}",
619 path_str
620 );
621 }
622
623 #[tokio::test]
624 async fn test_pty_master_config_accessor() {
625 let config = PtyConfig::new("linux".to_string(), 132, 43, 1024, 768);
626 let pty = PtyMaster::open(config).expect("Failed to open PTY");
627
628 let retrieved_config = pty.config();
629 assert_eq!(retrieved_config.term, "linux");
630 assert_eq!(retrieved_config.col_width, 132);
631 assert_eq!(retrieved_config.row_height, 43);
632 }
633
634 #[tokio::test]
635 async fn test_pty_master_multiple_resizes() {
636 let config = PtyConfig::default();
637 let mut pty = PtyMaster::open(config).expect("Failed to open PTY");
638
639 assert!(pty.resize(100, 30).is_ok());
641 assert_eq!(pty.config().col_width, 100);
642 assert_eq!(pty.config().row_height, 30);
643
644 assert!(pty.resize(200, 50).is_ok());
645 assert_eq!(pty.config().col_width, 200);
646 assert_eq!(pty.config().row_height, 50);
647
648 assert!(pty.resize(80, 24).is_ok());
650 assert_eq!(pty.config().col_width, 80);
651 assert_eq!(pty.config().row_height, 24);
652 }
653
654 #[test]
655 fn test_pty_reader_new() {
656 }
659
660 #[test]
661 fn test_pty_writer_new() {
662 }
665}