use std::io;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PollResult {
Stdin,
Master,
Both,
Timeout,
Error,
}
pub fn is_supported() -> bool {
cfg!(unix)
}
pub fn unsupported_error() -> io::Error {
io::Error::new(
io::ErrorKind::Unsupported,
"PTY support (--exec) is not available on this platform",
)
}
#[cfg(unix)]
mod unix {
use super::*;
use nix::libc;
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
use nix::pty::{openpty, OpenptyResult};
use nix::sys::termios::{self, LocalFlags, SetArg, Termios};
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use nix::unistd::{dup2, fork, read, write, ForkResult, Pid};
use std::ffi::CString;
use std::io::Write;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
pub struct PtySession {
master: OwnedFd,
child_pid: Pid,
original_termios: Option<Termios>,
keyboard_count: usize,
child_alive: bool,
}
impl PtySession {
pub fn spawn(command: &str) -> io::Result<Self> {
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Empty command"));
}
let stdin_fd = unsafe { BorrowedFd::borrow_raw(libc::STDIN_FILENO) };
let original_termios = termios::tcgetattr(stdin_fd).ok();
let OpenptyResult { master, slave } =
openpty(None, None).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
match unsafe { fork() } {
Ok(ForkResult::Child) => {
drop(master);
let _ = nix::unistd::setsid();
let slave_fd = slave.as_raw_fd();
let _ = dup2(slave_fd, libc::STDIN_FILENO);
let _ = dup2(slave_fd, libc::STDOUT_FILENO);
let _ = dup2(slave_fd, libc::STDERR_FILENO);
if slave_fd > libc::STDERR_FILENO {
drop(slave);
}
let program = match CString::new(parts[0]) {
Ok(c) => c,
Err(_) => std::process::exit(127),
};
let args: Vec<CString> =
parts.iter().filter_map(|s| CString::new(*s).ok()).collect();
let _ = nix::unistd::execvp(&program, &args);
std::process::exit(127);
}
Ok(ForkResult::Parent { child }) => {
drop(slave);
if let Some(ref orig) = original_termios {
let mut raw = orig.clone();
raw.local_flags.remove(LocalFlags::ICANON);
raw.local_flags.remove(LocalFlags::ECHO);
raw.local_flags.remove(LocalFlags::ISIG);
raw.control_chars[libc::VMIN] = 1;
raw.control_chars[libc::VTIME] = 0;
let _ = termios::tcsetattr(stdin_fd, SetArg::TCSANOW, &raw);
}
let _ = io::stdout().write_all(b"\x1b[?7h");
let _ = io::stdout().flush();
unsafe {
let flags = libc::fcntl(master.as_raw_fd(), libc::F_GETFL);
libc::fcntl(master.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK);
}
Ok(PtySession {
master,
child_pid: child,
original_termios,
keyboard_count: 0,
child_alive: true,
})
}
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
}
}
pub fn poll(&self, timeout: Duration) -> PollResult {
let stdin_fd = libc::STDIN_FILENO;
let _master_fd = self.master.as_raw_fd();
let mut fds = [
PollFd::new(
unsafe { BorrowedFd::borrow_raw(stdin_fd) },
PollFlags::POLLIN,
),
PollFd::new(self.master.as_fd(), PollFlags::POLLIN),
];
let timeout_ms = timeout.as_millis() as i32;
let poll_timeout = PollTimeout::try_from(timeout_ms).unwrap_or(PollTimeout::ZERO);
match poll(&mut fds, poll_timeout) {
Ok(0) => PollResult::Timeout,
Ok(_) => {
let stdin_ready = fds[0]
.revents()
.map(|r| r.contains(PollFlags::POLLIN))
.unwrap_or(false);
let master_ready = fds[1]
.revents()
.map(|r| r.contains(PollFlags::POLLIN))
.unwrap_or(false);
match (stdin_ready, master_ready) {
(true, true) => PollResult::Both,
(true, false) => PollResult::Stdin,
(false, true) => PollResult::Master,
(false, false) => PollResult::Timeout,
}
}
Err(_) => PollResult::Error,
}
}
pub fn read_master(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match read(self.master.as_raw_fd(), buf) {
Ok(n) => Ok(n),
Err(nix::errno::Errno::EAGAIN) => Ok(0),
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
}
}
#[allow(dead_code)]
pub fn read_master_byte(&mut self) -> io::Result<Option<u8>> {
let mut buf = [0u8; 1];
match self.read_master(&mut buf)? {
0 => Ok(None),
_ => Ok(Some(buf[0])),
}
}
pub fn write_master(&mut self, data: &[u8]) -> io::Result<usize> {
self.keyboard_count += data.len();
write(&self.master, data).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}
pub fn write_master_byte(&mut self, byte: u8) -> io::Result<()> {
self.write_master(&[byte])?;
Ok(())
}
pub fn read_stdin_byte(&self) -> io::Result<Option<u8>> {
let mut buf = [0u8; 1];
match read(libc::STDIN_FILENO, &mut buf) {
Ok(0) => Ok(None),
Ok(_) => Ok(Some(buf[0])),
Err(nix::errno::Errno::EAGAIN) => Ok(None),
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
}
}
pub fn keyboard_count(&self) -> usize {
self.keyboard_count
}
pub fn reset_keyboard_count(&mut self) {
self.keyboard_count = 0;
}
pub fn is_alive(&mut self) -> bool {
if !self.child_alive {
return false;
}
match waitpid(self.child_pid, Some(WaitPidFlag::WNOHANG)) {
Ok(WaitStatus::StillAlive) => true,
Ok(_) => {
self.child_alive = false;
false
}
Err(_) => {
self.child_alive = false;
false
}
}
}
pub fn wait(&mut self) -> io::Result<i32> {
match waitpid(self.child_pid, None) {
Ok(WaitStatus::Exited(_, code)) => {
self.child_alive = false;
Ok(code)
}
Ok(WaitStatus::Signaled(_, signal, _)) => {
self.child_alive = false;
Ok(128 + signal as i32)
}
Ok(_) => Ok(0),
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
}
}
#[allow(dead_code)]
pub fn master_fd(&self) -> RawFd {
self.master.as_raw_fd()
}
}
impl Drop for PtySession {
fn drop(&mut self) {
if let Some(ref orig) = self.original_termios {
let stdin_fd = unsafe { BorrowedFd::borrow_raw(libc::STDIN_FILENO) };
let _ = termios::tcsetattr(stdin_fd, SetArg::TCSADRAIN, orig);
}
if self.child_alive {
let _ = waitpid(self.child_pid, None);
}
}
}
}
#[cfg(unix)]
pub use unix::PtySession;
#[cfg(windows)]
pub struct PtySession;
#[cfg(windows)]
impl PtySession {
pub fn spawn(_command: &str) -> io::Result<Self> {
Err(unsupported_error())
}
pub fn poll(&self, _timeout: Duration) -> PollResult {
PollResult::Error
}
pub fn read_master(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(unsupported_error())
}
pub fn read_master_byte(&mut self) -> io::Result<Option<u8>> {
Err(unsupported_error())
}
pub fn write_master(&mut self, _data: &[u8]) -> io::Result<usize> {
Err(unsupported_error())
}
pub fn write_master_byte(&mut self, _byte: u8) -> io::Result<()> {
Err(unsupported_error())
}
pub fn read_stdin_byte(&self) -> io::Result<Option<u8>> {
Err(unsupported_error())
}
pub fn keyboard_count(&self) -> usize {
0
}
pub fn reset_keyboard_count(&mut self) {}
pub fn is_alive(&mut self) -> bool {
false
}
pub fn wait(&mut self) -> io::Result<i32> {
Err(unsupported_error())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_supported() {
#[cfg(unix)]
assert!(is_supported());
#[cfg(windows)]
assert!(!is_supported());
}
#[test]
fn test_poll_result_eq() {
assert_eq!(PollResult::Stdin, PollResult::Stdin);
assert_ne!(PollResult::Stdin, PollResult::Master);
}
}