use super::unix::IoctlReq;
use std::os::unix::io::{FromRawFd, RawFd};
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
fn spawn_child_test(name: &str) -> std::io::Result<(RawFd, std::process::Child)> {
let mut master: libc::c_int = -1;
let mut slave: libc::c_int = -1;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
let ws = libc::winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe { libc::ioctl(master, libc::TIOCSWINSZ as IoctlReq, &ws) };
let stdio = |fd: RawFd| -> std::io::Result<Stdio> {
let d = unsafe { libc::dup(fd) };
if d < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(unsafe { Stdio::from(std::fs::File::from_raw_fd(d)) })
};
let me = std::env::current_exe()?;
let mut cmd = Command::new(me);
cmd.arg(name)
.arg("--ignored")
.arg("--exact")
.arg("--nocapture")
.arg("--test-threads=1")
.env("TERM", "xterm-256color")
.stdin(stdio(slave)?)
.stdout(stdio(slave)?)
.stderr(stdio(slave)?);
let slave_for_child = slave;
unsafe {
cmd.pre_exec(move || {
if libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::ioctl(slave_for_child, libc::TIOCSCTTY as IoctlReq, 0) < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let child = cmd.spawn()?;
unsafe { libc::close(slave) };
Ok((master, child))
}
fn read_master_until(master: RawFd, needle: &[u8], wait: Duration) -> (bool, Vec<u8>) {
let deadline = Instant::now() + wait;
let mut got = Vec::new();
let mut buf = [0u8; 4096];
loop {
if got.windows(needle.len().max(1)).any(|w| w == needle) {
return (true, got);
}
let rem = deadline.saturating_duration_since(Instant::now());
if rem.is_zero() {
return (false, got);
}
let mut p = libc::pollfd {
fd: master,
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&mut p, 1, rem.as_millis().min(300) as i32) };
if rc <= 0 {
continue;
}
let n = unsafe { libc::read(master, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
if n <= 0 {
return (false, got);
}
got.extend_from_slice(&buf[..n as usize]);
}
}
fn write_master(master: RawFd, bytes: &[u8]) {
unsafe { libc::write(master, bytes.as_ptr() as *const libc::c_void, bytes.len()) };
}
fn cleanup(master: RawFd, mut child: std::process::Child) {
let _ = child.kill();
let _ = child.wait();
unsafe { libc::close(master) };
}
#[test]
#[ignore = "RT5-1 child probe — spawned by devtty_alias_vs_real_device_poll"]
fn rt5_child_alias_vs_real() {
unsafe {
let alias = libc::open(c"/dev/tty".as_ptr(), libc::O_RDWR | libc::O_CLOEXEC);
println!(
"PROBE: alias_open={}",
if alias >= 0 { "ok" } else { "FAIL" }
);
if alias < 0 {
return;
}
let mut ios: libc::termios = std::mem::zeroed();
libc::tcgetattr(alias, &mut ios);
libc::cfmakeraw(&mut ios);
libc::tcsetattr(alias, libc::TCSANOW, &ios);
println!("PROBE: READY");
std::thread::sleep(Duration::from_millis(400));
let mut p = libc::pollfd {
fd: alias,
events: libc::POLLIN,
revents: 0,
};
let rc = libc::poll(&mut p, 1, 1000);
let nval = p.revents & libc::POLLNVAL != 0;
let pin = p.revents & libc::POLLIN != 0;
println!(
"PROBE: alias_poll rc={rc} revents={:#x} nval={nval} pollin={pin}",
p.revents
);
let mut buf = [0 as libc::c_char; 1024];
if libc::ttyname_r(alias, buf.as_mut_ptr(), buf.len()) == 0 {
let path = std::ffi::CStr::from_ptr(buf.as_ptr())
.to_string_lossy()
.into_owned();
println!("PROBE: alias_resolves_to={path}");
} else {
println!("PROBE: alias_resolves_to=FAIL");
}
if libc::ttyname_r(0, buf.as_mut_ptr(), buf.len()) == 0 {
let path = std::ffi::CStr::from_ptr(buf.as_ptr())
.to_string_lossy()
.into_owned();
let real = libc::open(buf.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC);
println!(
"PROBE: stdin_resolves_to={path} open={}",
if real >= 0 { "ok" } else { "FAIL" }
);
if real >= 0 {
let mut pr = libc::pollfd {
fd: real,
events: libc::POLLIN,
revents: 0,
};
let rcr = libc::poll(&mut pr, 1, 1000);
let rin = pr.revents & libc::POLLIN != 0;
let rnval = pr.revents & libc::POLLNVAL != 0;
println!(
"PROBE: real_poll rc={rcr} revents={:#x} nval={rnval} pollin={rin}",
pr.revents
);
if rin {
let mut b = [0u8; 8];
let n = libc::read(real, b.as_mut_ptr() as *mut libc::c_void, b.len());
if n > 0 {
println!("PROBE: real_read={}", b[0] as char);
}
}
}
} else {
println!("PROBE: stdin_resolves_to=FAIL");
}
println!("PROBE: DONE");
}
}
#[test]
#[ignore = "RT5-1 child probe — spawned by engine_keystrokes_flow_on_ctty_path"]
fn rt5_child_engine_read() {
use crate::term::{EnterOptions, TermRead, Terminal, UnixTerminal};
let mut term = match UnixTerminal::new() {
Ok(t) => t,
Err(e) => {
println!("ENGINE: new_failed={e}");
return;
}
};
println!("ENGINE: acquired");
if let Err(e) = term.enter(&EnterOptions::default()) {
println!("ENGINE: enter_failed={e}");
return;
}
println!("ENGINE: READY");
match term.read(Some(Instant::now() + Duration::from_secs(4))) {
Ok(TermRead::Input(b)) if !b.is_empty() => {
println!("ENGINE: got={}", b[0] as char);
}
other => println!("ENGINE: got_none ({other:?})"),
}
let degraded = term.degraded().map(str::to_owned);
let _ = term.leave();
match degraded {
Some(d) => println!("ENGINE: degraded={d}"),
None => println!("ENGINE: degraded=none"),
}
println!("ENGINE: DONE");
}
#[test]
fn devtty_alias_vs_real_device_poll() {
let (master, child) = match spawn_child_test("term::rt5_live_tests::rt5_child_alias_vs_real") {
Ok(v) => v,
Err(e) => {
eprintln!("skipping: cannot spawn pty child ({e})");
return;
}
};
let (ready, mut all) = read_master_until(master, b"PROBE: READY", Duration::from_secs(10));
if !ready {
cleanup(master, child);
panic!(
"child never became ready: {}",
String::from_utf8_lossy(&all)
);
}
write_master(master, b"Z");
let (done, out) = read_master_until(master, b"PROBE: DONE", Duration::from_secs(10));
cleanup(master, child);
all.extend_from_slice(&out); let s = String::from_utf8_lossy(&all).into_owned();
assert!(done, "probe incomplete: {s}");
assert!(s.contains("alias_open=ok"), "ctty missing?! {s}");
assert!(s.contains("real_poll") && s.contains("pollin=true"), "{s}");
assert!(s.contains("real_read=Z"), "{s}");
#[cfg(any(target_os = "macos", target_os = "ios"))]
{
assert!(
s.contains("alias_poll") && s.contains("nval=true"),
"expected the Darwin /dev/tty POLLNVAL behavior: {s}"
);
assert!(
s.contains("alias_resolves_to=/dev/tty"),
"expected the circular alias resolution: {s}"
);
}
}
#[test]
fn engine_keystrokes_flow_on_ctty_path() {
let (master, child) = match spawn_child_test("term::rt5_live_tests::rt5_child_engine_read") {
Ok(v) => v,
Err(e) => {
eprintln!("skipping: cannot spawn pty child ({e})");
return;
}
};
let (ready, mut all) = read_master_until(master, b"ENGINE: READY", Duration::from_secs(10));
if !ready {
cleanup(master, child);
panic!(
"engine child never ready: {}",
String::from_utf8_lossy(&all)
);
}
write_master(master, b"x");
let (done, out) = read_master_until(master, b"ENGINE: DONE", Duration::from_secs(10));
cleanup(master, child);
all.extend_from_slice(&out);
let s = String::from_utf8_lossy(&all).into_owned();
assert!(done, "engine probe incomplete: {s}");
assert!(
s.contains("ENGINE: got=x"),
"keystroke did not reach the engine on the ctty path: {s}"
);
assert!(s.contains("ENGINE: degraded=none"), "{s}");
}