use std;
use std::fs::File;
use std::process::Command;
use std::os::unix::process::CommandExt;
use std::os::unix::io::{FromRawFd, AsRawFd};
use std::{thread, time};
use nix::pty::{posix_openpt, grantpt, unlockpt, PtyMaster};
use nix::fcntl::{OFlag, open};
use nix;
use nix::sys::{stat, termios};
use nix::unistd::{fork, ForkResult, setsid, dup, dup2, Pid};
use nix::libc::{STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO};
pub use nix::sys::{wait, signal};
use crate::errors::*;
pub struct PtyProcess {
pub pty: PtyMaster,
pub child_pid: Pid,
kill_timeout: Option<time::Duration>,
}
#[cfg(target_os = "linux")]
use nix::pty::ptsname_r;
#[cfg(target_os = "macos")]
fn ptsname_r(fd: &PtyMaster) -> nix::Result<String> {
use std::ffi::CStr;
use nix::libc::{ioctl, TIOCPTYGNAME};
let mut buf: [i8; 128] = [0; 128];
unsafe {
match ioctl(fd.as_raw_fd(), TIOCPTYGNAME as u64, &mut buf) {
0 => {
let res = CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned();
Ok(res)
}
_ => Err(nix::Error::last()),
}
}
}
impl PtyProcess {
pub fn new(mut command: Command) -> Result<Self> {
|| -> nix::Result<Self> {
let master_fd = posix_openpt(OFlag::O_RDWR)?;
grantpt(&master_fd)?;
unlockpt(&master_fd)?;
let slave_name = ptsname_r(&master_fd)?;
match fork()? {
ForkResult::Child => {
setsid()?; let slave_fd = open(std::path::Path::new(&slave_name),
OFlag::O_RDWR,
stat::Mode::empty())?;
dup2(slave_fd, STDIN_FILENO)?;
dup2(slave_fd, STDOUT_FILENO)?;
dup2(slave_fd, STDERR_FILENO)?;
let mut flags = termios::tcgetattr(STDIN_FILENO)?;
flags.local_flags &= !termios::LocalFlags::ECHO;
termios::tcsetattr(STDIN_FILENO, termios::SetArg::TCSANOW, &flags)?;
command.exec();
Err(nix::Error::last())
}
ForkResult::Parent { child: child_pid } => {
Ok(PtyProcess {
pty: master_fd,
child_pid: child_pid,
kill_timeout: None,
})
}
}
}()
.chain_err(|| format!("could not execute {:?}", command))
}
pub fn get_file_handle(&self) -> File {
let fd = dup(self.pty.as_raw_fd()).unwrap();
unsafe { File::from_raw_fd(fd) }
}
pub fn set_kill_timeout(&mut self, timeout_ms: Option<u64>) {
self.kill_timeout = timeout_ms.and_then(|millis| Some(time::Duration::from_millis(millis)));
}
pub fn status(&self) -> Option<wait::WaitStatus> {
if let Ok(status) = wait::waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG)) {
Some(status)
} else {
None
}
}
pub fn wait(&self) -> Result<wait::WaitStatus> {
wait::waitpid(self.child_pid, None).chain_err(|| "wait: cannot read status")
}
pub fn exit(&mut self) -> Result<wait::WaitStatus> {
self.kill(signal::SIGTERM)
}
pub fn signal(&mut self, sig: signal::Signal) -> Result<()> {
signal::kill(self.child_pid, sig)
.chain_err(|| "failed to send signal to process")?;
Ok(())
}
pub fn kill(&mut self, sig: signal::Signal) -> Result<wait::WaitStatus> {
let start = time::Instant::now();
loop {
match signal::kill(self.child_pid, sig) {
Ok(_) => {}
Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => {
return Ok(wait::WaitStatus::Exited(Pid::from_raw(0), 0))
}
Err(e) => return Err(format!("kill resulted in error: {:?}", e).into()),
}
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 {
if start.elapsed() > timeout {
signal::kill(self.child_pid, signal::Signal::SIGKILL).chain_err(|| "")?
}
}
}
}
}
impl Drop for PtyProcess {
fn drop(&mut self) {
match self.status() {
Some(wait::WaitStatus::StillAlive) => {
self.exit().expect("cannot exit");
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{BufReader, LineWriter};
use nix::sys::{wait, signal};
use std::io::prelude::*;
#[test]
fn test_cat() {
|| -> std::io::Result<()> {
let process = PtyProcess::new(Command::new("cat")).expect("could not execute cat");
let f = process.get_file_handle();
let mut writer = LineWriter::new(&f);
let mut reader = BufReader::new(&f);
writer.write(b"hello cat\n")?;
let mut buf = String::new();
reader.read_line(&mut buf)?;
assert_eq!(buf, "hello cat\r\n");
thread::sleep(time::Duration::from_millis(100));
writer.write_all(&[3])?; writer.flush()?;
let should =
wait::WaitStatus::Signaled(process.child_pid, signal::Signal::SIGINT, false);
assert_eq!(should, wait::waitpid(process.child_pid, None).unwrap());
Ok(())
}()
.unwrap_or_else(|e| panic!("test_cat failed: {}", e));
}
}