pub mod stream;
pub use nix::errno;
pub use nix::sys::signal::Signal;
pub use nix::sys::wait::WaitStatus;
pub use nix::Error;
use nix::fcntl::{fcntl, open, FcntlArg, FdFlag, OFlag};
use nix::libc::{self, winsize, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
use nix::pty::PtyMaster;
use nix::pty::{grantpt, posix_openpt, unlockpt};
use nix::sys::stat::Mode;
use nix::sys::wait::{self, waitpid};
use nix::sys::{signal, termios};
use nix::unistd::{self, close, dup, dup2, fork, isatty, pipe, setsid, write, ForkResult, Pid};
use nix::{ioctl_write_ptr_bad, Result};
use signal::Signal::SIGKILL;
use std::fs::File;
use std::os::unix::prelude::{AsRawFd, CommandExt, FromRawFd, RawFd};
use std::process::{self, Command};
use std::thread;
use std::time::{self, Duration};
use stream::Stream;
use termios::SpecialCharacterIndices;
const DEFAULT_TERM_COLS: u16 = 80;
const DEFAULT_TERM_ROWS: u16 = 24;
const DEFAULT_VEOF_CHAR: u8 = 0x4; const DEFAULT_INTR_CHAR: u8 = 0x3;
const DEFAULT_TERMINATE_DELAY: Duration = Duration::from_millis(100);
#[derive(Debug)]
pub struct PtyProcess {
master: Master,
child_pid: Pid,
eof_char: u8,
intr_char: u8,
terminate_delay: Duration,
}
impl PtyProcess {
pub fn spawn(mut command: Command) -> Result<Self> {
let master = Master::open()?;
master.grant_slave_access()?;
master.unlock_slave()?;
let (exec_err_pipe_r, exec_err_pipe_w) = pipe()?;
let fork = unsafe { fork()? };
match fork {
ForkResult::Child => {
let err = || -> Result<()> {
make_controlling_tty(&master)?;
let slave_fd = master.get_slave_fd()?;
redirect_std_streams(slave_fd)?;
set_echo(STDIN_FILENO, false)?;
set_term_size(STDIN_FILENO, DEFAULT_TERM_COLS, DEFAULT_TERM_ROWS)?;
close_all_descriptors(&[
STDIN_FILENO,
STDOUT_FILENO,
STDERR_FILENO,
slave_fd,
exec_err_pipe_w,
exec_err_pipe_r,
master.as_raw_fd(),
])?;
close(slave_fd)?;
close(exec_err_pipe_r)?;
drop(master);
fcntl(exec_err_pipe_w, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))?;
let _ = command.exec();
Err(Error::last())
}()
.unwrap_err();
let code = err as i32;
let _ = write(exec_err_pipe_w, &code.to_be_bytes());
let _ = close(exec_err_pipe_w);
process::exit(code);
}
ForkResult::Parent { child } => {
close(exec_err_pipe_w)?;
let mut pipe_buf = [0u8; 4];
unistd::read(exec_err_pipe_r, &mut pipe_buf)?;
close(exec_err_pipe_r)?;
let code = i32::from_be_bytes(pipe_buf);
if code != 0 {
return Err(errno::from_i32(code));
}
set_term_size(master.as_raw_fd(), DEFAULT_TERM_COLS, DEFAULT_TERM_ROWS)?;
let eof_char = get_eof_char();
let intr_char = get_intr_char();
Ok(Self {
master,
child_pid: child,
eof_char,
intr_char,
terminate_delay: DEFAULT_TERMINATE_DELAY,
})
}
}
}
pub fn pid(&self) -> Pid {
self.child_pid
}
pub fn get_raw_handle(&self) -> Result<File> {
self.master.get_file_handle()
}
pub fn get_pty_stream(&self) -> Result<Stream> {
self.get_raw_handle().map(Stream::new)
}
pub fn get_eof_char(&self) -> u8 {
self.eof_char
}
pub fn get_intr_char(&self) -> u8 {
self.intr_char
}
pub fn get_window_size(&self) -> Result<(u16, u16)> {
get_term_size(self.master.as_raw_fd())
}
pub fn set_window_size(&mut self, cols: u16, rows: u16) -> Result<()> {
set_term_size(self.master.as_raw_fd(), cols, rows)
}
pub fn get_echo(&self) -> Result<bool> {
termios::tcgetattr(self.master.as_raw_fd())
.map(|flags| flags.local_flags.contains(termios::LocalFlags::ECHO))
}
pub fn set_echo(&mut self, on: bool, timeout: Option<Duration>) -> Result<bool> {
set_echo(self.master.as_raw_fd(), on)?;
self.wait_echo(on, timeout)
}
pub fn isatty(&self) -> Result<bool> {
isatty(self.master.as_raw_fd())
}
pub fn set_terminate_delay(&mut self, terminate_approach_delay: Duration) {
self.terminate_delay = terminate_approach_delay;
}
pub fn status(&self) -> Result<WaitStatus> {
waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG))
}
pub fn kill(&mut self, signal: signal::Signal) -> Result<()> {
signal::kill(self.child_pid, signal)
}
pub fn signal(&mut self, signal: signal::Signal) -> Result<()> {
self.kill(signal)
}
pub fn wait(&self) -> Result<WaitStatus> {
waitpid(self.child_pid, None)
}
pub fn is_alive(&self) -> Result<bool> {
let status = self.status();
match status {
Ok(WaitStatus::StillAlive) => Ok(true),
Ok(_) | Err(Error::ECHILD) | Err(Error::ESRCH) => Ok(false),
Err(err) => Err(err),
}
}
pub fn exit(&mut self, force: bool) -> Result<bool> {
if !self.is_alive()? {
return Ok(true);
}
for &signal in &[
signal::SIGHUP,
signal::SIGCONT,
signal::SIGINT,
signal::SIGTERM,
] {
if self.try_to_terminate(signal)? {
return Ok(true);
}
}
if !force {
return Ok(false);
}
self.try_to_terminate(SIGKILL)
}
fn try_to_terminate(&mut self, signal: signal::Signal) -> Result<bool> {
self.kill(signal)?;
thread::sleep(self.terminate_delay);
self.is_alive().map(|is_alive| !is_alive)
}
fn wait_echo(&self, on: bool, timeout: Option<Duration>) -> Result<bool> {
let now = time::Instant::now();
while timeout.is_none() || now.elapsed() < timeout.unwrap() {
if on == self.get_echo()? {
return Ok(true);
}
thread::sleep(Duration::from_millis(100));
}
Ok(false)
}
}
impl Drop for PtyProcess {
fn drop(&mut self) {
if let Ok(WaitStatus::StillAlive) = self.status() {
self.exit(true).unwrap();
}
}
}
fn set_term_size(fd: i32, cols: u16, rows: u16) -> Result<()> {
ioctl_write_ptr_bad!(_set_window_size, libc::TIOCSWINSZ, winsize);
let size = winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
let _ = unsafe { _set_window_size(fd, &size) }?;
Ok(())
}
fn get_term_size(fd: i32) -> Result<(u16, u16)> {
nix::ioctl_read_bad!(_get_window_size, libc::TIOCGWINSZ, winsize);
let mut size = winsize {
ws_col: 0,
ws_row: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
let _ = unsafe { _get_window_size(fd, &mut size) }?;
Ok((size.ws_col, size.ws_row))
}
#[derive(Debug)]
struct Master {
fd: PtyMaster,
}
impl Master {
fn open() -> Result<Self> {
let master_fd = posix_openpt(OFlag::O_RDWR)?;
Ok(Self { fd: master_fd })
}
fn grant_slave_access(&self) -> Result<()> {
grantpt(&self.fd)
}
fn unlock_slave(&self) -> Result<()> {
unlockpt(&self.fd)
}
fn get_slave_name(&self) -> Result<String> {
get_slave_name(&self.fd)
}
#[cfg(not(target_os = "freebsd"))]
fn get_slave_fd(&self) -> Result<RawFd> {
let slave_name = self.get_slave_name()?;
let slave_fd = open(
slave_name.as_str(),
OFlag::O_RDWR | OFlag::O_NOCTTY,
Mode::empty(),
)?;
Ok(slave_fd)
}
#[cfg(target_os = "freebsd")]
fn get_slave_fd(&self) -> Result<RawFd> {
let slave_name = self.get_slave_name()?;
let slave_fd = open(
format!("/dev/{}", slave_name.as_str()).as_str(),
OFlag::O_RDWR | OFlag::O_NOCTTY,
Mode::empty(),
)?;
Ok(slave_fd)
}
fn get_file_handle(&self) -> Result<File> {
let fd = dup(self.as_raw_fd())?;
let file = unsafe { File::from_raw_fd(fd) };
Ok(file)
}
}
impl AsRawFd for Master {
fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn get_slave_name(fd: &PtyMaster) -> Result<String> {
nix::pty::ptsname_r(fd)
}
#[cfg(target_os = "freebsd")]
fn get_slave_name(fd: &PtyMaster) -> Result<String> {
use std::ffi::CStr;
use std::os::raw::c_char;
use std::os::unix::prelude::AsRawFd;
let fd = fd.as_raw_fd();
if !isptmaster(fd)? {
return Err(nix::Error::EINVAL);
}
let mut buf: [c_char; 128] = [0; 128];
let _ = fdevname_r(fd, &mut buf)?;
let string = unsafe { CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned();
return Ok(string);
}
#[cfg(target_os = "freebsd")]
fn isptmaster(fd: RawFd) -> Result<bool> {
use nix::libc::ioctl;
use nix::libc::TIOCPTMASTER;
match unsafe { ioctl(fd, TIOCPTMASTER as u64, 0) } {
0 => Ok(true),
_ => Err(Error::last()),
}
}
#[cfg(target_os = "freebsd")]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct fiodgname_arg {
pub len: ::std::os::raw::c_int,
pub buf: *mut ::std::os::raw::c_void,
}
#[cfg(target_os = "freebsd")]
fn fdevname_r(fd: RawFd, buf: &mut [std::os::raw::c_char]) -> Result<()> {
use nix::libc::{ioctl, FIODGNAME};
nix::ioctl_read_bad!(_ioctl_fiodgname, FIODGNAME, fiodgname_arg);
let mut fgn = fiodgname_arg {
len: buf.len() as i32,
buf: buf.as_mut_ptr() as *mut ::std::os::raw::c_void,
};
let _ = unsafe { _ioctl_fiodgname(fd, &mut fgn) }?;
Ok(())
}
#[cfg(target_os = "macos")]
fn get_slave_name(fd: &PtyMaster) -> Result<String> {
use nix::libc::ioctl;
use nix::libc::TIOCPTYGNAME;
use std::ffi::CStr;
use std::os::raw::c_char;
use std::os::unix::prelude::AsRawFd;
let mut buf: [c_char; 128] = [0; 128];
let fd = fd.as_raw_fd();
match unsafe { ioctl(fd, TIOCPTYGNAME as u64, &mut buf) } {
0 => {
let string = unsafe { CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned();
return Ok(string);
}
_ => Err(Error::last()),
}
}
fn redirect_std_streams(fd: RawFd) -> Result<()> {
close(STDIN_FILENO)?;
close(STDOUT_FILENO)?;
close(STDERR_FILENO)?;
dup2(fd, STDIN_FILENO)?;
dup2(fd, STDOUT_FILENO)?;
dup2(fd, STDERR_FILENO)?;
Ok(())
}
fn set_echo(fd: RawFd, on: bool) -> Result<()> {
let mut flags = termios::tcgetattr(fd)?;
match on {
true => flags.local_flags |= termios::LocalFlags::ECHO,
false => flags.local_flags &= !termios::LocalFlags::ECHO,
}
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?;
Ok(())
}
pub fn set_raw(fd: RawFd) -> Result<()> {
let mut flags = termios::tcgetattr(fd)?;
#[cfg(not(target_os = "macos"))]
{
termios::cfmakeraw(&mut flags);
}
#[cfg(target_os = "macos")]
{
use nix::libc::{VMIN, VTIME};
use termios::ControlFlags;
use termios::InputFlags;
use termios::LocalFlags;
use termios::OutputFlags;
flags.input_flags &= !(InputFlags::BRKINT
| InputFlags::ICRNL
| InputFlags::INPCK
| InputFlags::ISTRIP
| InputFlags::IXON);
flags.output_flags &= !OutputFlags::OPOST;
flags.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB);
flags.control_flags |= ControlFlags::CS8;
flags.local_flags &=
!(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG);
flags.control_chars[VMIN] = 1;
flags.control_chars[VTIME] = 0;
}
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?;
Ok(())
}
fn get_this_term_char(char: SpecialCharacterIndices) -> Option<u8> {
for &fd in &[STDIN_FILENO, STDOUT_FILENO] {
if let Ok(char) = get_term_char(fd, char) {
return Some(char);
}
}
None
}
fn get_intr_char() -> u8 {
get_this_term_char(SpecialCharacterIndices::VINTR).unwrap_or(DEFAULT_INTR_CHAR)
}
fn get_eof_char() -> u8 {
get_this_term_char(SpecialCharacterIndices::VEOF).unwrap_or(DEFAULT_VEOF_CHAR)
}
fn get_term_char(fd: RawFd, char: SpecialCharacterIndices) -> Result<u8> {
let flags = termios::tcgetattr(fd)?;
let b = flags.control_chars[char as usize];
Ok(b)
}
fn make_controlling_tty(ptm: &Master) -> Result<()> {
#[cfg(not(any(target_os = "freebsd", target_os = "macos")))]
{
let pts_name = ptm.get_slave_name()?;
let fd = open("/dev/tty", OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty());
match fd {
Ok(fd) => {
close(fd)?;
}
Err(Error::ENXIO) => {
}
Err(err) => return Err(err),
}
setsid()?;
let fd = open("/dev/tty", OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty());
match fd {
Err(Error::ENXIO) => {} Ok(fd) => {
close(fd)?;
return Err(Error::ENOTSUP);
}
Err(_) => return Err(Error::ENOTSUP),
}
let fd = open(pts_name.as_str(), OFlag::O_RDWR, Mode::empty())?;
close(fd)?;
let fd = open("/dev/tty", OFlag::O_WRONLY, Mode::empty())?;
close(fd)?;
}
#[cfg(any(target_os = "freebsd", target_os = "macos"))]
{
let pts_fd = ptm.get_slave_fd()?;
setsid()?;
use nix::libc::ioctl;
use nix::libc::TIOCSCTTY;
match unsafe { ioctl(pts_fd, TIOCSCTTY as u64, 0) } {
0 => {}
_ => return Err(Error::last()),
}
}
Ok(())
}
#[cfg(not(feature = "close-range"))]
fn close_all_descriptors(except: &[RawFd]) -> Result<()> {
use nix::unistd::{sysconf, SysconfVar};
let max_open_fds = sysconf(SysconfVar::OPEN_MAX)?.unwrap() as i32;
for fd in 0..max_open_fds {
if except.contains(&fd) {
continue;
}
let _ = close(fd);
}
Ok(())
}
#[cfg(feature = "close-range")]
fn close_all_descriptors(except: &[RawFd]) -> Result<()> {
if except.is_empty() {
return Ok(());
}
let fds = get_untouched_fds(except);
for range in fds {
match range.len() {
0 => unreachable!("must never happen"),
1 => _ = close(range.start),
_ => {
let first = range.start as std::ffi::c_uint;
let last = (range.end as std::ffi::c_uint).saturating_sub(1);
let _ = unsafe { nix::libc::close_range(first, last, 0) };
}
}
}
Ok(())
}
#[cfg(feature = "close-range")]
fn get_untouched_fds(except: &[RawFd]) -> Vec<std::ops::Range<RawFd>> {
if except.is_empty() {
return vec![0..RawFd::MAX];
}
let mut except = except.to_vec();
except.sort_unstable();
let mut ranges = vec![];
if except[0] > 0 {
ranges.push(0..except[0]);
}
for range in except.windows(2) {
if range[0] + 1 == range[1] {
continue;
}
ranges.push(range[0] + 1..range[1]);
}
if except[except.len() - 1] < RawFd::MAX {
ranges.push((except[except.len() - 1] + 1)..RawFd::MAX);
}
ranges
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_pty() -> Result<()> {
let master = Master::open()?;
master.grant_slave_access()?;
master.unlock_slave()?;
let slavename = master.get_slave_name()?;
let expected_path = if cfg!(target_os = "freebsd") {
"pts/"
} else if cfg!(target_os = "macos") {
"/dev/ttys"
} else {
"/dev/pts/"
};
if !slavename.starts_with(expected_path) {
assert_eq!(expected_path, slavename);
}
Ok(())
}
#[test]
#[ignore = "The test should be run in a sigle thread mode --jobs 1 or --test-threads 1"]
fn release_pty_master() -> Result<()> {
let master = Master::open()?;
let old_master_fd = master.fd.as_raw_fd();
drop(master);
let master = Master::open()?;
assert!(master.fd.as_raw_fd() == old_master_fd);
Ok(())
}
#[cfg(feature = "close-range")]
#[test]
fn test_get_ranges() {
assert_eq!(get_untouched_fds(&[]), vec![0..RawFd::MAX]);
assert_eq!(get_untouched_fds(&[RawFd::MAX]), vec![0..RawFd::MAX]);
assert_eq!(
get_untouched_fds(&[10, RawFd::MAX]),
vec![0..10, 11..RawFd::MAX]
);
assert_eq!(get_untouched_fds(&[100]), vec![0..100, 101..RawFd::MAX]);
assert_eq!(
get_untouched_fds(&[10, 20]),
vec![0..10, 11..20, 21..RawFd::MAX]
);
assert_eq!(
get_untouched_fds(&[1, 2, 10, 20]),
vec![0..1, 3..10, 11..20, 21..RawFd::MAX]
);
assert_eq!(
get_untouched_fds(&[0, 1, 2, 10, 20]),
vec![3..10, 11..20, 21..RawFd::MAX]
);
}
}