#![allow(dead_code)]
use std::ffi::CString;
use std::os::unix::io::RawFd;
use std::time::{Duration, Instant};
pub struct PtySession {
master_fd: RawFd,
child_pid: libc::pid_t,
}
impl PtySession {
pub fn spawn(program: &str, args: &[&str], envs: &[(&str, &str)]) -> std::io::Result<Self> {
let mut master: libc::c_int = -1;
let mut slave: libc::c_int = -1;
let mut winsize = libc::winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
let winp: *mut libc::winsize = &mut winsize;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut(),
winp,
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
unsafe {
let flags = libc::fcntl(master, libc::F_GETFL);
libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
let c_program = CString::new(program).expect("program has no NUL bytes");
let argv_owned: Vec<CString> = std::iter::once(program)
.chain(args.iter().copied())
.map(|a| CString::new(a).expect("arg has no NUL bytes"))
.collect();
let mut argv: Vec<*const libc::c_char> = argv_owned.iter().map(|a| a.as_ptr()).collect();
argv.push(std::ptr::null());
let mut env_pairs: Vec<(String, String)> =
vec![("TERM".to_string(), "xterm-256color".to_string())];
env_pairs.extend(envs.iter().map(|(k, v)| (k.to_string(), v.to_string())));
let envp_owned: Vec<CString> = env_pairs
.iter()
.map(|(k, v)| CString::new(format!("{k}={v}")).expect("env has no NUL bytes"))
.collect();
let mut envp: Vec<*const libc::c_char> = envp_owned.iter().map(|e| e.as_ptr()).collect();
envp.push(std::ptr::null());
let pid = unsafe { libc::fork() };
if pid < 0 {
let err = std::io::Error::last_os_error();
unsafe {
libc::close(master);
libc::close(slave);
}
return Err(err);
}
if pid == 0 {
unsafe {
libc::close(master);
if libc::login_tty(slave) != 0 {
libc::_exit(127);
}
libc::execve(c_program.as_ptr(), argv.as_ptr(), envp.as_ptr());
libc::_exit(127);
}
}
unsafe {
libc::close(slave);
}
Ok(PtySession {
master_fd: master,
child_pid: pid,
})
}
pub fn set_winsize(&self, rows: u16, cols: u16) {
let winsize = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe {
libc::ioctl(self.master_fd, libc::TIOCSWINSZ as _, &winsize);
}
}
pub fn write_bytes(&self, bytes: &[u8]) {
unsafe {
libc::write(self.master_fd, bytes.as_ptr().cast(), bytes.len());
}
}
pub fn read_available(&self, timeout: Duration) -> Vec<u8> {
let deadline = Instant::now() + timeout;
let mut out = Vec::new();
loop {
let now = Instant::now();
if now >= deadline {
break;
}
let remaining_ms = (deadline - now).as_millis().min(i32::MAX as u128) as i32;
let mut fds = [libc::pollfd {
fd: self.master_fd,
events: libc::POLLIN,
revents: 0,
}];
let n = unsafe { libc::poll(fds.as_mut_ptr(), 1, remaining_ms) };
if n <= 0 {
break; }
if fds[0].revents & libc::POLLIN == 0 {
break; }
let mut buf = [0u8; 4096];
let read = unsafe { libc::read(self.master_fd, buf.as_mut_ptr().cast(), buf.len()) };
if read > 0 {
out.extend_from_slice(&buf[..read as usize]);
break;
}
if read == 0 {
break; }
let err = std::io::Error::last_os_error();
if err.kind() != std::io::ErrorKind::WouldBlock {
break; }
}
out
}
pub fn exited(&self) -> bool {
let mut status: libc::c_int = 0;
unsafe { libc::waitpid(self.child_pid, &mut status, libc::WNOHANG) == self.child_pid }
}
pub fn kill_if_alive(&self, deadline: Duration) -> bool {
let start = Instant::now();
loop {
let mut status: libc::c_int = 0;
let rc = unsafe { libc::waitpid(self.child_pid, &mut status, libc::WNOHANG) };
if rc == self.child_pid {
return false; }
if Instant::now().duration_since(start) >= deadline {
unsafe {
libc::kill(self.child_pid, libc::SIGKILL);
}
loop {
let _ = self.read_available(Duration::from_millis(20));
let rc = unsafe { libc::waitpid(self.child_pid, &mut status, libc::WNOHANG) };
if rc == self.child_pid {
return true;
}
}
}
let _ = self.read_available(Duration::from_millis(20));
}
}
}
impl Drop for PtySession {
fn drop(&mut self) {
unsafe {
let mut status: libc::c_int = 0;
let reaped = libc::waitpid(self.child_pid, &mut status, libc::WNOHANG);
if reaped == 0 {
libc::kill(self.child_pid, libc::SIGKILL);
loop {
let _ = self.read_available(Duration::from_millis(20));
let rc = libc::waitpid(self.child_pid, &mut status, libc::WNOHANG);
if rc == self.child_pid {
break;
}
}
}
libc::close(self.master_fd);
}
}
}
pub struct FakeTerminal {
fg: String,
bg: String,
carry: Vec<u8>,
}
const CARRY_CAP: usize = 64;
impl FakeTerminal {
pub fn dark() -> Self {
Self::new("rgb:ffff/ffff/ffff", "rgb:1e1e/1e1e/2e2e")
}
pub fn light() -> Self {
Self::new("rgb:0000/0000/0000", "rgb:ffff/ffff/ffff")
}
fn new(fg: &str, bg: &str) -> Self {
FakeTerminal {
fg: fg.to_string(),
bg: bg.to_string(),
carry: Vec::new(),
}
}
pub fn set(&mut self, fg: &str, bg: &str) {
self.fg = fg.to_string();
self.bg = bg.to_string();
}
pub fn respond(&mut self, session: &PtySession, chunk: &[u8]) {
self.carry.extend_from_slice(chunk);
if self.carry.len() > CARRY_CAP {
let drop_from = self.carry.len() - CARRY_CAP;
self.carry.drain(..drop_from);
}
let fg_reply = format!("\x1b]10;{}\x1b\\", self.fg);
let bg_reply = format!("\x1b]11;{}\x1b\\", self.bg);
self.answer(session, b"\x1b]10;?", &fg_reply);
self.answer(session, b"\x1b]11;?", &bg_reply);
self.answer(session, b"\x1b[c", "\x1b[?1;2c");
}
fn answer(&mut self, session: &PtySession, needle: &[u8], reply: &str) {
if let Some(pos) = find(&self.carry, needle) {
session.write_bytes(reply.as_bytes());
self.carry.drain(..pos + needle.len());
}
}
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.len() > haystack.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
pub fn wait_for(
session: &PtySession,
terminal: &mut FakeTerminal,
needle: &[u8],
timeout: Duration,
) -> bool {
let deadline = Instant::now() + timeout;
let mut seen: Vec<u8> = Vec::new();
loop {
let now = Instant::now();
if now >= deadline {
return false;
}
let slice = (deadline - now).min(Duration::from_millis(50));
let chunk = session.read_available(slice);
if chunk.is_empty() {
continue;
}
terminal.respond(session, &chunk);
seen.extend_from_slice(&chunk);
if find(&seen, needle).is_some() {
return true;
}
}
}
pub fn wait_for_in_order(
session: &PtySession,
terminal: &mut FakeTerminal,
needles: &[&[u8]],
timeout: Duration,
) -> Vec<u8> {
match try_wait_for_in_order(session, terminal, needles, timeout) {
Ok(seen) => seen,
Err(msg) => panic!("{msg}"),
}
}
pub fn try_wait_for_in_order(
session: &PtySession,
terminal: &mut FakeTerminal,
needles: &[&[u8]],
timeout: Duration,
) -> Result<Vec<u8>, String> {
let deadline = Instant::now() + timeout;
let mut seen: Vec<u8> = Vec::new();
loop {
match first_unmatched_in_order(&seen, needles) {
None => return Ok(seen),
Some(missing) => {
let now = Instant::now();
if now >= deadline {
return Err(format!(
"needle {:?} never arrived in order; stream so far: {:?}",
String::from_utf8_lossy(needles[missing]),
String::from_utf8_lossy(&seen)
));
}
let slice = (deadline - now).min(Duration::from_millis(50));
let chunk = session.read_available(slice);
if chunk.is_empty() {
continue;
}
terminal.respond(session, &chunk);
seen.extend_from_slice(&chunk);
}
}
}
}
pub fn first_unmatched_in_order(seen: &[u8], needles: &[&[u8]]) -> Option<usize> {
let mut pos = 0;
for (i, needle) in needles.iter().enumerate() {
match find(&seen[pos..], needle) {
Some(at) => pos += at + needle.len(),
None => return Some(i),
}
}
None
}
pub fn mkfifo_at(path: &std::path::Path) {
let cpath =
std::ffi::CString::new(path.as_os_str().as_encoded_bytes().to_vec()).expect("fifo path");
assert_eq!(unsafe { libc::mkfifo(cpath.as_ptr(), 0o600) }, 0, "mkfifo");
}
pub fn write_fifo(path: &std::path::Path, bytes: &[u8]) {
use std::io::Write;
let mut writer = std::fs::OpenOptions::new()
.write(true)
.open(path)
.expect("fifo write end");
writer.write_all(bytes).expect("fifo write");
}
pub fn counter_cmd(path: &std::path::Path) -> String {
format!(
"echo run >> {p}; printf 'count-%s' $(wc -l < {p})",
p = path.display()
)
}
pub fn wait_for_counter(path: &std::path::Path, n: usize) {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let count = std::fs::read_to_string(path)
.map(|s| s.lines().count())
.unwrap_or(0);
if count >= n {
return;
}
assert!(
Instant::now() < deadline,
"counter stuck at {count}, wanted {n}"
);
std::thread::sleep(Duration::from_millis(20));
}
}
pub fn assert_counter_settled_at(path: &std::path::Path, n: usize) {
let deadline = Instant::now() + Duration::from_millis(900);
while Instant::now() < deadline {
let count = std::fs::read_to_string(path)
.map(|s| s.lines().count())
.unwrap_or(0);
assert!(count <= n, "counter moved past {n}: {count}");
std::thread::sleep(Duration::from_millis(30));
}
let count = std::fs::read_to_string(path)
.map(|s| s.lines().count())
.unwrap_or(0);
assert_eq!(count, n, "counter settled at the wrong value");
}