pub use nix::sys::{signal, wait};
use nix::{
self,
fcntl::{OFlag, open},
libc::STDERR_FILENO,
pty::{PtyMaster, Winsize, grantpt, posix_openpt, unlockpt},
sys::{
stat,
termios::{self, InputFlags, Termios},
},
unistd::{ForkResult, Pid, close, dup, dup2_stderr, dup2_stdin, dup2_stdout, fork, setsid},
};
use std::os::fd::{AsFd, FromRawFd, IntoRawFd};
use std::{
self,
fs::File,
io,
os::unix::{io::AsRawFd, process::CommandExt},
process::Command,
thread, time,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
time::{Duration, Instant, sleep},
};
#[cfg(any(target_os = "linux", target_os = "android"))]
use nix::pty::ptsname_r;
#[cfg(target_os = "netbsd")]
fn ptsname_r(fd: &PtyMaster) -> nix::Result<String> {
use std::ffi::CStr;
let mut buf: [libc::c_char; 128] = [0; 128];
unsafe {
match libc::ptsname_r(fd.as_raw_fd(), buf.as_mut_ptr(), buf.len()) {
0 => Ok(CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned()),
_ => Err(nix::Error::last()),
}
}
}
pub struct PtyProcess {
pub pty: PtyMaster,
pub child_pid: Pid,
kill_timeout: Option<time::Duration>,
}
#[cfg(target_os = "macos")]
fn ptsname_r(fd: &PtyMaster) -> nix::Result<String> {
use nix::libc::{TIOCPTYGNAME, ioctl};
use std::ffi::CStr;
let mut buf: [i8; 128] = [0; 128];
unsafe {
match ioctl(fd.as_raw_fd(), u64::from(TIOCPTYGNAME), &mut buf) {
0 => {
let res = CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned();
Ok(res)
}
_ => Err(nix::Error::last()),
}
}
}
#[cfg(target_os = "openbsd")]
fn ptsname_r(fd: &PtyMaster) -> nix::Result<String> {
use std::ffi::CStr;
unsafe {
let ptr = libc::ptsname(fd.as_raw_fd());
if ptr.is_null() {
Err(nix::Error::last())
} else {
Ok(CStr::from_ptr(ptr).to_string_lossy().into_owned())
}
}
}
#[cfg(target_os = "freebsd")]
fn ptsname_r(fd: &PtyMaster) -> nix::Result<String> {
use nix::libc::ioctl;
use std::ffi::CStr;
#[repr(C)]
struct fiodgname_arg {
len: libc::c_int,
buf: *mut libc::c_char,
}
const FIODGNAME: libc::c_ulong = 0x80106678;
let mut buf: [libc::c_char; 128] = [0; 128];
let mut arg = fiodgname_arg {
len: buf.len() as libc::c_int,
buf: buf.as_mut_ptr(),
};
unsafe {
match ioctl(fd.as_raw_fd(), FIODGNAME, &mut arg) {
0 => {
let res = CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned();
Ok(format!("/dev/{}", res))
}
_ => Err(nix::Error::last()),
}
}
}
#[derive(Default, Clone)]
pub struct PtyProcessOptions {
pub echo: bool,
pub window_size: Option<Winsize>,
}
impl PtyProcess {
pub fn new(mut command: Command, opts: PtyProcessOptions) -> nix::Result<Self> {
let master_fd = posix_openpt(OFlag::O_RDWR)?;
grantpt(&master_fd)?;
unlockpt(&master_fd)?;
let slave_name = ptsname_r(&master_fd)?;
let window_size = opts.window_size.unwrap_or_else(|| {
let mut size: libc::winsize = unsafe { std::mem::zeroed() };
unsafe { libc::ioctl(io::stdout().as_raw_fd(), libc::TIOCGWINSZ, &mut size) };
size
});
match unsafe { fork()? } {
ForkResult::Child => {
close(master_fd.as_raw_fd())?;
setsid()?; let slave_fd = open(
std::path::Path::new(&slave_name),
OFlag::O_RDWR,
stat::Mode::empty(),
)?;
dup2_stdin(&slave_fd)?;
dup2_stdout(&slave_fd)?;
dup2_stderr(&slave_fd)?;
if slave_fd.as_raw_fd() > STDERR_FILENO {
close(slave_fd)?;
}
set_echo(io::stdin(), opts.echo)?;
set_window_size(io::stdout().as_raw_fd(), window_size)?;
let _ = command.exec();
Err(nix::Error::last())
}
ForkResult::Parent { child: child_pid } => Ok(PtyProcess {
pty: master_fd,
child_pid,
kill_timeout: None,
}),
}
}
pub fn get_file_handle(&self) -> nix::Result<File> {
let fd = dup(&self.pty)?;
Ok(File::from(fd))
}
pub fn status(&self) -> Option<wait::WaitStatus> {
wait::waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG)).ok()
}
pub fn exit(&mut self) -> nix::Result<wait::WaitStatus> {
self.kill(signal::SIGTERM)
}
pub fn kill(&mut self, sig: signal::Signal) -> nix::Result<wait::WaitStatus> {
let start = time::Instant::now();
loop {
match signal::kill(self.child_pid, sig) {
Ok(_) => {}
Err(nix::errno::Errno::ESRCH) => {
return Ok(wait::WaitStatus::Exited(Pid::from_raw(0), 0));
}
Err(e) => return Err(e),
}
match self.status() {
Some(status) if status != wait::WaitStatus::StillAlive => return Ok(status),
Some(_) | None => thread::sleep(time::Duration::from_millis(100)),
}
if let Some(timeout) = self.kill_timeout
&& start.elapsed() > timeout
{
signal::kill(self.child_pid, signal::Signal::SIGKILL)?;
}
}
}
pub fn set_raw(&self) -> nix::Result<Termios> {
let original_mode = termios::tcgetattr(io::stdin())?;
let mut raw_mode = original_mode.clone();
raw_mode.input_flags.remove(
InputFlags::BRKINT
| InputFlags::ICRNL
| InputFlags::INPCK
| InputFlags::ISTRIP
| InputFlags::IXON,
);
raw_mode.output_flags.remove(termios::OutputFlags::OPOST);
raw_mode
.control_flags
.remove(termios::ControlFlags::CSIZE | termios::ControlFlags::PARENB);
raw_mode.control_flags.insert(termios::ControlFlags::CS8);
raw_mode.local_flags.remove(
termios::LocalFlags::ECHO
| termios::LocalFlags::ICANON
| termios::LocalFlags::IEXTEN
| termios::LocalFlags::ISIG,
);
raw_mode.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1;
raw_mode.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0;
termios::tcsetattr(io::stdin(), termios::SetArg::TCSAFLUSH, &raw_mode)?;
Ok(original_mode)
}
pub fn set_mode(&self, original_mode: Termios) -> nix::Result<()> {
termios::tcsetattr(io::stdin(), termios::SetArg::TCSAFLUSH, &original_mode)?;
Ok(())
}
pub fn set_window_size(&self, window_size: Winsize) -> nix::Result<()> {
set_window_size(self.pty.as_raw_fd(), window_size)
}
pub fn kill_timeout(&self) -> Option<time::Duration> {
self.kill_timeout
}
pub fn set_kill_timeout(&mut self, timeout: Option<time::Duration>) {
self.kill_timeout = timeout;
}
pub async fn async_read(&self, buf: &mut [u8]) -> io::Result<usize> {
let fd = dup(&self.pty).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
let mut file = unsafe { tokio::fs::File::from_raw_fd(fd.into_raw_fd()) };
file.read(buf).await
}
pub async fn async_write(&self, buf: &[u8]) -> io::Result<usize> {
let fd = dup(&self.pty).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
let mut file = unsafe { tokio::fs::File::from_raw_fd(fd.into_raw_fd()) };
file.write(buf).await
}
pub async fn async_wait(&mut self) -> nix::Result<wait::WaitStatus> {
loop {
match self.status() {
Some(status) if status != wait::WaitStatus::StillAlive => return Ok(status),
Some(_) | None => sleep(Duration::from_millis(100)).await,
}
}
}
pub async fn async_exit(&mut self) -> nix::Result<wait::WaitStatus> {
self.async_kill(signal::SIGTERM).await
}
pub async fn async_kill(&mut self, sig: signal::Signal) -> nix::Result<wait::WaitStatus> {
let start = Instant::now();
loop {
match signal::kill(self.child_pid, sig) {
Ok(_) => {}
Err(nix::errno::Errno::ESRCH) => {
return Ok(wait::WaitStatus::Exited(Pid::from_raw(0), 0));
}
Err(e) => return Err(e),
}
match self.status() {
Some(status) if status != wait::WaitStatus::StillAlive => return Ok(status),
Some(_) | None => sleep(Duration::from_millis(100)).await,
}
if let Some(timeout) = self.kill_timeout
&& start.elapsed() > timeout
{
signal::kill(self.child_pid, signal::Signal::SIGKILL)?;
}
}
}
}
pub fn set_window_size(raw_fd: i32, window_size: Winsize) -> nix::Result<()> {
unsafe { libc::ioctl(raw_fd, nix::libc::TIOCSWINSZ, &window_size) };
Ok(())
}
pub fn set_echo<Fd: AsFd>(fd: Fd, echo: bool) -> nix::Result<()> {
let mut flags = termios::tcgetattr(&fd)?;
if echo {
flags.local_flags.insert(termios::LocalFlags::ECHO);
} else {
flags.local_flags.remove(termios::LocalFlags::ECHO);
}
termios::tcsetattr(&fd, termios::SetArg::TCSANOW, &flags)?;
Ok(())
}
impl Drop for PtyProcess {
fn drop(&mut self) {
if let Some(wait::WaitStatus::StillAlive) = self.status() {
self.exit().expect("cannot exit");
}
}
}