1pub mod stream;
36
37pub use nix::errno;
38pub use nix::sys::signal::Signal;
39pub use nix::sys::wait::WaitStatus;
40pub use nix::Error;
41
42use nix::fcntl::{fcntl, open, FcntlArg, FdFlag, OFlag};
43use nix::libc::{self, winsize, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
44use nix::pty::PtyMaster;
45use nix::pty::{grantpt, posix_openpt, unlockpt};
46use nix::sys::stat::Mode;
47use nix::sys::wait::{self, waitpid};
48use nix::sys::{signal, termios};
49use nix::unistd::{
50 self, close, dup, dup2, fork, isatty, pipe, setsid, sysconf, write, ForkResult, Pid, SysconfVar,
51};
52use nix::{ioctl_write_ptr_bad, Result};
53use signal::Signal::SIGKILL;
54use std::fs::File;
55use std::os::unix::prelude::{AsRawFd, CommandExt, FromRawFd, RawFd};
56use std::process::{self, Command};
57use std::thread;
58use std::time::{self, Duration};
59use stream::Stream;
60use termios::SpecialCharacterIndices;
61
62const DEFAULT_TERM_COLS: u16 = 80;
63const DEFAULT_TERM_ROWS: u16 = 24;
64
65const DEFAULT_VEOF_CHAR: u8 = 0x4; const DEFAULT_INTR_CHAR: u8 = 0x3; const DEFAULT_TERMINATE_DELAY: Duration = Duration::from_millis(100);
69
70#[derive(Debug)]
85pub struct PtyProcess {
86 master: Master,
87 child_pid: Pid,
88 eof_char: u8,
89 intr_char: u8,
90 terminate_delay: Duration,
91}
92
93impl PtyProcess {
94 pub fn spawn(mut command: Command) -> Result<Self> {
102 let master = Master::open()?;
103 master.grant_slave_access()?;
104 master.unlock_slave()?;
105
106 let (exec_err_pipe_r, exec_err_pipe_w) = pipe()?;
108
109 let fork = unsafe { fork()? };
110 match fork {
111 ForkResult::Child => {
112 let err = || -> Result<()> {
113 make_controlling_tty(&master)?;
114
115 let slave_fd = master.get_slave_fd()?;
116 redirect_std_streams(slave_fd)?;
117
118 set_echo(STDIN_FILENO, false)?;
119 set_term_size(STDIN_FILENO, DEFAULT_TERM_COLS, DEFAULT_TERM_ROWS)?;
120
121 close_all_descriptors(&[
123 0,
124 1,
125 2,
126 slave_fd,
127 exec_err_pipe_w,
128 exec_err_pipe_r,
129 master.as_raw_fd(),
130 ])?;
131
132 close(slave_fd)?;
133 close(exec_err_pipe_r)?;
134 drop(master);
135
136 fcntl(exec_err_pipe_w, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))?;
138
139 let _ = command.exec();
140 Err(Error::last())
141 }()
142 .unwrap_err();
143
144 let code = err as i32;
145
146 let _ = write(exec_err_pipe_w, &code.to_be_bytes());
148 let _ = close(exec_err_pipe_w);
149
150 process::exit(code);
151 }
152 ForkResult::Parent { child } => {
153 close(exec_err_pipe_w)?;
154
155 let mut pipe_buf = [0u8; 4];
156 unistd::read(exec_err_pipe_r, &mut pipe_buf)?;
157 close(exec_err_pipe_r)?;
158 let code = i32::from_be_bytes(pipe_buf);
159 if code != 0 {
160 return Err(errno::from_i32(code));
161 }
162
163 set_term_size(master.as_raw_fd(), DEFAULT_TERM_COLS, DEFAULT_TERM_ROWS)?;
166
167 let eof_char = get_eof_char();
168 let intr_char = get_intr_char();
169
170 Ok(Self {
171 master,
172 child_pid: child,
173 eof_char,
174 intr_char,
175 terminate_delay: DEFAULT_TERMINATE_DELAY,
176 })
177 }
178 }
179 }
180
181 pub fn pid(&self) -> Pid {
183 self.child_pid
184 }
185
186 pub fn get_raw_handle(&self) -> Result<File> {
212 self.master.get_file_handle()
213 }
214
215 pub fn get_pty_stream(&self) -> Result<Stream> {
221 self.get_raw_handle().map(Stream::new)
222 }
223
224 pub fn get_eof_char(&self) -> u8 {
226 self.eof_char
227 }
228
229 pub fn get_intr_char(&self) -> u8 {
231 self.intr_char
232 }
233
234 pub fn get_window_size(&self) -> Result<(u16, u16)> {
238 get_term_size(self.master.as_raw_fd())
239 }
240
241 pub fn set_window_size(&mut self, cols: u16, rows: u16) -> Result<()> {
243 set_term_size(self.master.as_raw_fd(), cols, rows)
244 }
245
246 pub fn get_echo(&self) -> Result<bool> {
248 termios::tcgetattr(self.master.as_raw_fd())
249 .map(|flags| flags.local_flags.contains(termios::LocalFlags::ECHO))
250 }
251
252 pub fn set_echo(&mut self, on: bool, timeout: Option<Duration>) -> Result<bool> {
254 set_echo(self.master.as_raw_fd(), on)?;
255 self.wait_echo(on, timeout)
256 }
257
258 pub fn isatty(&self) -> Result<bool> {
260 isatty(self.master.as_raw_fd())
261 }
262
263 pub fn set_terminate_delay(&mut self, terminate_approach_delay: Duration) {
265 self.terminate_delay = terminate_approach_delay;
266 }
267
268 pub fn status(&self) -> Result<WaitStatus> {
270 waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG))
271 }
272
273 pub fn kill(&mut self, signal: signal::Signal) -> Result<()> {
277 signal::kill(self.child_pid, signal)
278 }
279
280 pub fn signal(&mut self, signal: signal::Signal) -> Result<()> {
284 self.kill(signal)
285 }
286
287 pub fn wait(&self) -> Result<WaitStatus> {
297 waitpid(self.child_pid, None)
298 }
299
300 pub fn is_alive(&self) -> Result<bool> {
308 let status = self.status();
309 match status {
310 Ok(status) if status == WaitStatus::StillAlive => Ok(true),
311 Ok(_) | Err(Error::ECHILD) | Err(Error::ESRCH) => Ok(false),
312 Err(err) => Err(err),
313 }
314 }
315
316 pub fn exit(&mut self, force: bool) -> Result<bool> {
330 if !self.is_alive()? {
331 return Ok(true);
332 }
333
334 for &signal in &[
335 signal::SIGHUP,
336 signal::SIGCONT,
337 signal::SIGINT,
338 signal::SIGTERM,
339 ] {
340 if self.try_to_terminate(signal)? {
341 return Ok(true);
342 }
343 }
344
345 if !force {
346 return Ok(false);
347 }
348
349 self.try_to_terminate(SIGKILL)
350 }
351
352 fn try_to_terminate(&mut self, signal: signal::Signal) -> Result<bool> {
353 self.kill(signal)?;
354 thread::sleep(self.terminate_delay);
355
356 self.is_alive().map(|is_alive| !is_alive)
357 }
358
359 fn wait_echo(&self, on: bool, timeout: Option<Duration>) -> Result<bool> {
360 let now = time::Instant::now();
361 while timeout.is_none() || now.elapsed() < timeout.unwrap() {
362 if on == self.get_echo()? {
363 return Ok(true);
364 }
365
366 thread::sleep(Duration::from_millis(100));
367 }
368
369 Ok(false)
370 }
371}
372
373impl Drop for PtyProcess {
374 fn drop(&mut self) {
375 if let Ok(WaitStatus::StillAlive) = self.status() {
376 self.exit(true).unwrap();
377 }
378 }
379}
380
381fn set_term_size(fd: i32, cols: u16, rows: u16) -> Result<()> {
382 ioctl_write_ptr_bad!(_set_window_size, libc::TIOCSWINSZ, winsize);
383
384 let size = winsize {
385 ws_row: rows,
386 ws_col: cols,
387 ws_xpixel: 0,
388 ws_ypixel: 0,
389 };
390
391 let _ = unsafe { _set_window_size(fd, &size) }?;
392
393 Ok(())
394}
395
396fn get_term_size(fd: i32) -> Result<(u16, u16)> {
397 nix::ioctl_read_bad!(_get_window_size, libc::TIOCGWINSZ, winsize);
398
399 let mut size = winsize {
400 ws_col: 0,
401 ws_row: 0,
402 ws_xpixel: 0,
403 ws_ypixel: 0,
404 };
405
406 let _ = unsafe { _get_window_size(fd, &mut size) }?;
407
408 Ok((size.ws_col, size.ws_row))
409}
410
411#[derive(Debug)]
412struct Master {
413 fd: PtyMaster,
414}
415
416impl Master {
417 fn open() -> Result<Self> {
418 let master_fd = posix_openpt(OFlag::O_RDWR)?;
419 Ok(Self { fd: master_fd })
420 }
421
422 fn grant_slave_access(&self) -> Result<()> {
423 grantpt(&self.fd)
424 }
425
426 fn unlock_slave(&self) -> Result<()> {
427 unlockpt(&self.fd)
428 }
429
430 fn get_slave_name(&self) -> Result<String> {
431 get_slave_name(&self.fd)
432 }
433
434 #[cfg(not(target_os = "freebsd"))]
435 fn get_slave_fd(&self) -> Result<RawFd> {
436 let slave_name = self.get_slave_name()?;
437 let slave_fd = open(
438 slave_name.as_str(),
439 OFlag::O_RDWR | OFlag::O_NOCTTY,
440 Mode::empty(),
441 )?;
442 Ok(slave_fd)
443 }
444
445 #[cfg(target_os = "freebsd")]
446 fn get_slave_fd(&self) -> Result<RawFd> {
447 let slave_name = self.get_slave_name()?;
448 let slave_fd = open(
449 format!("/dev/{}", slave_name.as_str()).as_str(),
450 OFlag::O_RDWR | OFlag::O_NOCTTY,
451 Mode::empty(),
452 )?;
453 Ok(slave_fd)
454 }
455
456 fn get_file_handle(&self) -> Result<File> {
457 let fd = dup(self.as_raw_fd())?;
458 let file = unsafe { File::from_raw_fd(fd) };
459
460 Ok(file)
461 }
462}
463
464impl AsRawFd for Master {
465 fn as_raw_fd(&self) -> RawFd {
466 self.fd.as_raw_fd()
467 }
468}
469
470#[cfg(target_os = "linux")]
471fn get_slave_name(fd: &PtyMaster) -> Result<String> {
472 nix::pty::ptsname_r(fd)
473}
474
475#[cfg(target_os = "freebsd")]
476fn get_slave_name(fd: &PtyMaster) -> Result<String> {
477 use std::ffi::CStr;
478 use std::os::raw::c_char;
479 use std::os::unix::prelude::AsRawFd;
480
481 let fd = fd.as_raw_fd();
482
483 if !isptmaster(fd)? {
484 return Err(nix::Error::EINVAL);
486 }
487
488 let mut buf: [c_char; 128] = [0; 128];
490
491 let _ = fdevname_r(fd, &mut buf)?;
492
493 let string = unsafe { CStr::from_ptr(buf.as_ptr()) }
495 .to_string_lossy()
496 .into_owned();
497
498 return Ok(string);
499}
500
501#[cfg(target_os = "freebsd")]
503fn isptmaster(fd: RawFd) -> Result<bool> {
504 use nix::libc::ioctl;
505 use nix::libc::TIOCPTMASTER;
506 match unsafe { ioctl(fd, TIOCPTMASTER as u64, 0) } {
507 0 => Ok(true),
508 _ => Err(Error::last()),
509 }
510}
511
512#[cfg(target_os = "freebsd")]
516#[repr(C)]
517#[derive(Debug, Copy, Clone)]
518pub struct fiodgname_arg {
519 pub len: ::std::os::raw::c_int,
520 pub buf: *mut ::std::os::raw::c_void,
521}
522
523#[cfg(target_os = "freebsd")]
525fn fdevname_r(fd: RawFd, buf: &mut [std::os::raw::c_char]) -> Result<()> {
526 use nix::libc::{ioctl, FIODGNAME};
527
528 nix::ioctl_read_bad!(_ioctl_fiodgname, FIODGNAME, fiodgname_arg);
529
530 let mut fgn = fiodgname_arg {
531 len: buf.len() as i32,
532 buf: buf.as_mut_ptr() as *mut ::std::os::raw::c_void,
533 };
534
535 let _ = unsafe { _ioctl_fiodgname(fd, &mut fgn) }?;
536
537 Ok(())
538}
539
540#[cfg(target_os = "macos")]
543fn get_slave_name(fd: &PtyMaster) -> Result<String> {
544 use nix::libc::ioctl;
545 use nix::libc::TIOCPTYGNAME;
546 use std::ffi::CStr;
547 use std::os::raw::c_char;
548 use std::os::unix::prelude::AsRawFd;
549
550 let mut buf: [c_char; 128] = [0; 128];
555
556 let fd = fd.as_raw_fd();
557
558 match unsafe { ioctl(fd, TIOCPTYGNAME as u64, &mut buf) } {
559 0 => {
560 let string = unsafe { CStr::from_ptr(buf.as_ptr()) }
561 .to_string_lossy()
562 .into_owned();
563 return Ok(string);
564 }
565 _ => Err(Error::last()),
566 }
567}
568
569fn redirect_std_streams(fd: RawFd) -> Result<()> {
570 close(STDIN_FILENO)?;
573 close(STDOUT_FILENO)?;
574 close(STDERR_FILENO)?;
575
576 dup2(fd, STDIN_FILENO)?;
578 dup2(fd, STDOUT_FILENO)?;
579 dup2(fd, STDERR_FILENO)?;
580
581 Ok(())
582}
583
584fn set_echo(fd: RawFd, on: bool) -> Result<()> {
585 let mut flags = termios::tcgetattr(fd)?;
588 match on {
589 true => flags.local_flags |= termios::LocalFlags::ECHO,
590 false => flags.local_flags &= !termios::LocalFlags::ECHO,
591 }
592
593 termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?;
594 Ok(())
595}
596
597pub fn set_raw(fd: RawFd) -> Result<()> {
598 let mut flags = termios::tcgetattr(fd)?;
599
600 #[cfg(not(target_os = "macos"))]
601 {
602 termios::cfmakeraw(&mut flags);
603 }
604 #[cfg(target_os = "macos")]
605 {
606 use nix::libc::{VMIN, VTIME};
608 use termios::ControlFlags;
609 use termios::InputFlags;
610 use termios::LocalFlags;
611 use termios::OutputFlags;
612
613 flags.input_flags &= !(InputFlags::BRKINT
614 | InputFlags::ICRNL
615 | InputFlags::INPCK
616 | InputFlags::ISTRIP
617 | InputFlags::IXON);
618 flags.output_flags &= !OutputFlags::OPOST;
619 flags.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB);
620 flags.control_flags |= ControlFlags::CS8;
621 flags.local_flags &=
622 !(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG);
623 flags.control_chars[VMIN] = 1;
624 flags.control_chars[VTIME] = 0;
625 }
626
627 termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?;
628 Ok(())
629}
630
631fn get_this_term_char(char: SpecialCharacterIndices) -> Option<u8> {
632 for &fd in &[STDIN_FILENO, STDOUT_FILENO] {
633 if let Ok(char) = get_term_char(fd, char) {
634 return Some(char);
635 }
636 }
637
638 None
639}
640
641fn get_intr_char() -> u8 {
642 get_this_term_char(SpecialCharacterIndices::VINTR).unwrap_or(DEFAULT_INTR_CHAR)
643}
644
645fn get_eof_char() -> u8 {
646 get_this_term_char(SpecialCharacterIndices::VEOF).unwrap_or(DEFAULT_VEOF_CHAR)
647}
648
649fn get_term_char(fd: RawFd, char: SpecialCharacterIndices) -> Result<u8> {
650 let flags = termios::tcgetattr(fd)?;
651 let b = flags.control_chars[char as usize];
652 Ok(b)
653}
654
655fn make_controlling_tty(ptm: &Master) -> Result<()> {
656 #[cfg(not(any(target_os = "freebsd", target_os = "macos")))]
657 {
658 let pts_name = ptm.get_slave_name()?;
659 let fd = open("/dev/tty", OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty());
666 match fd {
667 Ok(fd) => {
668 close(fd)?;
669 }
670 Err(Error::ENXIO) => {
671 }
675 Err(err) => return Err(err),
676 }
677
678 setsid()?;
681
682 let fd = open("/dev/tty", OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty());
685 match fd {
686 Err(Error::ENXIO) => {} Ok(fd) => {
688 close(fd)?;
689 return Err(Error::ENOTSUP);
690 }
691 Err(_) => return Err(Error::ENOTSUP),
692 }
693
694 let fd = open(pts_name.as_str(), OFlag::O_RDWR, Mode::empty())?;
696 close(fd)?;
697
698 let fd = open("/dev/tty", OFlag::O_WRONLY, Mode::empty())?;
700 close(fd)?;
701 }
702
703 #[cfg(any(target_os = "freebsd", target_os = "macos"))]
704 {
705 let pts_fd = ptm.get_slave_fd()?;
706
707 setsid()?;
709
710 use nix::libc::ioctl;
711 use nix::libc::TIOCSCTTY;
712 match unsafe { ioctl(pts_fd, TIOCSCTTY as u64, 0) } {
713 0 => {}
714 _ => return Err(Error::last()),
715 }
716 }
717
718 Ok(())
719}
720
721fn close_all_descriptors(except: &[RawFd]) -> Result<()> {
723 let max_open_fds = sysconf(SysconfVar::OPEN_MAX)?.unwrap() as i32;
725 (0..max_open_fds)
726 .filter(|fd| !except.contains(fd))
727 .for_each(|fd| {
728 let _ = close(fd);
731 });
732
733 Ok(())
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739
740 #[test]
741 fn create_pty() -> Result<()> {
742 let master = Master::open()?;
743 master.grant_slave_access()?;
744 master.unlock_slave()?;
745 let slavename = master.get_slave_name()?;
746
747 let expected_path = if cfg!(target_os = "freebsd") {
748 "pts/"
749 } else if cfg!(target_os = "macos") {
750 "/dev/ttys"
751 } else {
752 "/dev/pts/"
753 };
754
755 if !slavename.starts_with(expected_path) {
756 assert_eq!(expected_path, slavename);
757 }
758
759 Ok(())
760 }
761
762 #[test]
763 #[ignore = "The test should be run in a sigle thread mode --jobs 1 or --test-threads 1"]
764 fn release_pty_master() -> Result<()> {
765 let master = Master::open()?;
766 let old_master_fd = master.fd.as_raw_fd();
767
768 drop(master);
769
770 let master = Master::open()?;
771
772 assert!(master.fd.as_raw_fd() == old_master_fd);
773
774 Ok(())
775 }
776}