#![allow(dead_code)]
use std::{
fs::File,
os::{
fd::{AsFd, BorrowedFd, OwnedFd},
unix::prelude::{AsRawFd, FromRawFd},
},
thread,
time::{self, Duration},
};
pub use nix::Error;
use nix::{
fcntl::{open, OFlag},
ioctl_write_ptr_bad,
libc::{self, winsize},
pty::{grantpt, posix_openpt, unlockpt, PtyMaster},
sys::{stat::Mode, termios},
unistd::{close, isatty, setsid},
Result,
};
use termios::SpecialCharacterIndices;
use tokio::process::Command;
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,
eof_char: u8,
intr_char: u8,
terminate_delay: Duration,
}
impl PtyProcess {
pub fn spawn(
mut command: Command,
rows: u16,
cols: u16,
) -> std::io::Result<(Self, tokio::process::Child)> {
let master = Master::open()?;
master.grant_slave_access()?;
master.unlock_slave()?;
let slave_fd = master.get_slave_fd()?;
command
.stdin(slave_fd.try_clone()?)
.stdout(slave_fd.try_clone()?)
.stderr(slave_fd);
let pts_name = master.get_slave_name()?;
unsafe {
command.pre_exec(move || -> std::io::Result<()> {
make_controlling_tty(&pts_name)?;
let stdin = std::io::stdin();
set_echo(&stdin, false)?;
set_term_size(stdin.as_raw_fd(), cols, rows)?;
Ok(())
});
}
Ok((
PtyProcess {
master,
eof_char: get_eof_char(),
intr_char: get_intr_char(),
terminate_delay: DEFAULT_TERMINATE_DELAY,
},
command.spawn()?,
))
}
pub fn get_raw_handle(&self) -> std::io::Result<File> {
self.master.get_file_handle()
}
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_fd().as_raw_fd())
}
pub fn set_window_size(&self, cols: u16, rows: u16) -> Result<()> {
set_term_size(self.master.as_fd().as_raw_fd(), cols, rows)
}
pub fn get_echo(&self) -> Result<bool> {
termios::tcgetattr(&self.master)
.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, on)?;
self.wait_echo(on, timeout)
}
pub fn isatty(&self) -> Result<bool> {
isatty(self.master.as_fd().as_raw_fd())
}
pub fn set_terminate_delay(&mut self, terminate_approach_delay: Duration) {
self.terminate_delay = terminate_approach_delay;
}
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)
}
}
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)?;
nix::fcntl::fcntl(
master_fd.as_raw_fd(),
nix::fcntl::FcntlArg::F_SETFD(nix::fcntl::FdFlag::FD_CLOEXEC),
)?;
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)
}
fn get_slave_fd(&self) -> Result<OwnedFd> {
let slave_name = self.get_slave_name()?;
let slave_fd = open(
slave_name.as_str(),
OFlag::O_RDWR | OFlag::O_NOCTTY,
Mode::empty(),
)?;
Ok(unsafe { OwnedFd::from_raw_fd(slave_fd) })
}
fn get_file_handle(&self) -> std::io::Result<File> {
let fd = self.as_fd().try_clone_to_owned()?;
let file = fd.into();
Ok(file)
}
}
impl AsFd for Master {
fn as_fd(&self) -> BorrowedFd<'_> {
unsafe { BorrowedFd::borrow_raw(self.fd.as_raw_fd()) }
}
}
fn get_slave_name(fd: &PtyMaster) -> Result<String> {
nix::pty::ptsname_r(fd)
}
fn set_echo(fd: impl AsFd, on: bool) -> Result<()> {
let mut flags = termios::tcgetattr(fd.as_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: impl AsFd) -> Result<()> {
let mut flags = termios::tcgetattr(fd.as_fd())?;
termios::cfmakeraw(&mut flags);
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?;
Ok(())
}
fn get_this_term_char(char: SpecialCharacterIndices) -> Option<u8> {
for &fd in &[std::io::stdin().as_fd(), std::io::stdout().as_fd()] {
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: impl AsFd, char: SpecialCharacterIndices) -> Result<u8> {
let flags = termios::tcgetattr(fd)?;
let b = flags.control_chars[char as usize];
Ok(b)
}
fn make_controlling_tty(pts_name: &str) -> Result<()> {
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, OFlag::O_RDWR, Mode::empty())?;
close(fd)?;
let fd = open("/dev/tty", OFlag::O_WRONLY, Mode::empty())?;
close(fd)?;
Ok(())
}