#![cfg(unix)]
use std::io::Read;
use std::os::unix::io::FromRawFd;
use std::os::unix::process::CommandExt;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
pub struct PtyProcess {
child: Child,
master: Option<std::fs::File>,
slave_path: String,
pub captured: Vec<u8>,
}
#[derive(Debug, Clone, Copy)]
pub struct TtyState {
pub icanon: bool,
pub echo: bool,
pub ixon: bool,
pub vmin: u8,
pub vtime: u8,
}
pub fn spawn_in_pty(
program: &str,
args: &[&str],
cols: u16,
rows: u16,
extra_env: &[(&str, &str)],
) -> std::io::Result<PtyProcess> {
spawn_in_pty_opts(program, args, cols, rows, extra_env, true)
}
pub fn spawn_in_pty_opts(
program: &str,
args: &[&str],
cols: u16,
rows: u16,
extra_env: &[(&str, &str)],
ctty: bool,
) -> std::io::Result<PtyProcess> {
let mut master: libc::c_int = -1;
let mut slave: libc::c_int = -1;
let mut winsize = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: cols.saturating_mul(8),
ws_ypixel: rows.saturating_mul(16),
};
#[allow(clippy::unnecessary_mut_passed)]
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut winsize,
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
let (stdin, stdout, stderr) = unsafe {
libc::fcntl(master, libc::F_SETFD, libc::FD_CLOEXEC);
let s0 = libc::dup(slave);
let s1 = libc::dup(slave);
if s0 < 0 || s1 < 0 {
return Err(std::io::Error::last_os_error());
}
(
Stdio::from_raw_fd(slave),
Stdio::from_raw_fd(s0),
Stdio::from_raw_fd(s1),
)
};
let mut cmd = Command::new(program);
cmd.args(args)
.stdin(stdin)
.stdout(stdout)
.stderr(stderr)
.env("TERM", "xterm-256color")
.env("COLORTERM", "truecolor")
.env_remove("ABSTRACTTUI_NO_SPLASH")
.env_remove("NO_COLOR");
for (k, v) in extra_env {
cmd.env(k, v);
}
unsafe {
cmd.pre_exec(move || {
if libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
if ctty && libc::ioctl(0, libc::TIOCSCTTY as libc::c_ulong, 0) < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let child = cmd.spawn()?;
let (master_file, slave_path) = unsafe {
let flags = libc::fcntl(master, libc::F_GETFL);
libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
let name = libc::ptsname(master);
let path = if name.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(name)
.to_string_lossy()
.into_owned()
};
(std::fs::File::from_raw_fd(master), path)
};
Ok(PtyProcess {
child,
master: Some(master_file),
slave_path,
captured: Vec::new(),
})
}
impl PtyProcess {
pub fn read_for(&mut self, window: Duration) -> usize {
let Some(master) = self.master.as_mut() else {
return 0;
};
let start = Instant::now();
let before = self.captured.len();
let mut buf = [0u8; 8192];
while start.elapsed() < window {
match master.read(&mut buf) {
Ok(0) => break, Ok(n) => self.captured.extend_from_slice(&buf[..n]),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(20));
}
Err(_) => break, }
}
self.captured.len() - before
}
pub fn send(&mut self, bytes: &[u8]) {
use std::io::Write;
let Some(master) = self.master.as_mut() else {
return;
};
let _ = master.write_all(bytes);
let _ = master.flush();
}
pub fn wait_with_deadline(&mut self, deadline: Duration) -> Option<i32> {
let start = Instant::now();
loop {
if let Ok(Some(status)) = self.child.try_wait() {
self.read_for(Duration::from_millis(120));
return status.code();
}
if start.elapsed() >= deadline {
return None;
}
self.read_for(Duration::from_millis(60));
}
}
pub fn slave_pending_input(&self) -> Option<usize> {
if self.slave_path.is_empty() {
return None;
}
let c_path = std::ffi::CString::new(self.slave_path.as_str()).ok()?;
unsafe {
let fd = libc::open(
c_path.as_ptr(),
libc::O_RDONLY | libc::O_NOCTTY | libc::O_NONBLOCK,
);
if fd < 0 {
return None;
}
let mut n: libc::c_int = 0;
let rc = libc::ioctl(fd, libc::FIONREAD, &mut n);
libc::close(fd);
if rc != 0 {
return None;
}
Some(n as usize)
}
}
pub fn tty_state(&self) -> Option<TtyState> {
if self.slave_path.is_empty() {
return None;
}
let c_path = std::ffi::CString::new(self.slave_path.as_str()).ok()?;
unsafe {
let fd = libc::open(
c_path.as_ptr(),
libc::O_RDONLY | libc::O_NOCTTY | libc::O_NONBLOCK,
);
if fd < 0 {
return None;
}
let mut tio: libc::termios = std::mem::zeroed();
let rc = libc::tcgetattr(fd, &mut tio);
libc::close(fd);
if rc != 0 {
return None;
}
Some(TtyState {
icanon: tio.c_lflag & libc::ICANON != 0,
echo: tio.c_lflag & libc::ECHO != 0,
ixon: tio.c_iflag & libc::IXON != 0,
vmin: tio.c_cc[libc::VMIN] as u8,
vtime: tio.c_cc[libc::VTIME] as u8,
})
}
}
pub fn kill(&mut self) {
let _ = self.child.kill();
for _ in 0..50 {
if let Ok(Some(_)) = self.child.try_wait() {
return;
}
self.read_for(Duration::from_millis(20));
}
self.master = None; let _ = self.child.wait();
}
}
impl Drop for PtyProcess {
fn drop(&mut self) {
if matches!(self.child.try_wait(), Ok(None) | Err(_)) {
self.kill();
}
}
}