use std::io::{Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
pub struct Pty {
pub master: OwnedFd,
pub slave: OwnedFd,
}
pub fn open() -> std::io::Result<Pty> {
let mut master: RawFd = -1;
let mut slave: RawFd = -1;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
core::ptr::null_mut(),
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Pty {
master: unsafe { OwnedFd::from_raw_fd(master) },
slave: unsafe { OwnedFd::from_raw_fd(slave) },
})
}
pub fn copy_window_size(from: RawFd, to: RawFd) {
let mut size: libc::winsize = unsafe { core::mem::zeroed() };
if unsafe { libc::ioctl(from, libc::TIOCGWINSZ, &mut size) } == 0 {
unsafe { libc::ioctl(to, libc::TIOCSWINSZ, &size) };
}
}
pub struct RawMode {
fd: RawFd,
original: libc::termios,
}
impl RawMode {
pub fn enable(fd: RawFd) -> std::io::Result<Option<RawMode>> {
if unsafe { libc::isatty(fd) } != 1 {
return Ok(None); }
let mut original: libc::termios = unsafe { core::mem::zeroed() };
if unsafe { libc::tcgetattr(fd, &mut original) } != 0 {
return Err(std::io::Error::last_os_error());
}
let mut raw = original;
unsafe { libc::cfmakeraw(&mut raw) };
if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } != 0 {
return Err(std::io::Error::last_os_error());
}
remember(fd, original);
Ok(Some(RawMode { fd, original }))
}
}
impl Drop for RawMode {
fn drop(&mut self) {
unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &self.original) };
forget(self.fd);
}
}
static SAVED: std::sync::Mutex<Vec<(RawFd, libc::termios)>> = std::sync::Mutex::new(Vec::new());
static HOOK: std::sync::Once = std::sync::Once::new();
fn remember(fd: RawFd, original: libc::termios) {
HOOK.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
restore_all();
previous(info);
}));
});
let mut saved = SAVED.lock().expect("saved terminals");
saved.retain(|(seen, _)| *seen != fd);
saved.push((fd, original));
}
fn forget(fd: RawFd) {
if let Ok(mut saved) = SAVED.lock() {
saved.retain(|(seen, _)| *seen != fd);
}
}
pub fn restore_all() {
let mut saved = match SAVED.lock() {
Ok(saved) => saved,
Err(poisoned) => poisoned.into_inner(),
};
for (fd, original) in saved.drain(..) {
unsafe { libc::tcsetattr(fd, libc::TCSANOW, &original) };
}
}
pub fn relay(master: OwnedFd) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let master_fd = master.as_raw_fd();
let writer = unsafe { OwnedFd::from_raw_fd(libc::dup(master_fd)) };
std::thread::spawn(move || {
let mut input = std::io::stdin();
let mut out = std::fs::File::from(writer);
let mut buf = [0u8; 4096];
while let Ok(n) = input.read(&mut buf) {
if n == 0 || out.write_all(&buf[..n]).is_err() {
break;
}
}
});
let mut input = std::fs::File::from(master);
let mut out = std::io::stdout();
let mut buf = [0u8; 4096];
loop {
match input.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
if out.write_all(&buf[..n]).is_err() || out.flush().is_err() {
break;
}
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_pty_pair_is_two_distinct_terminals() {
let pty = open().expect("openpty");
let master = pty.master.as_raw_fd();
let slave = pty.slave.as_raw_fd();
assert_ne!(master, slave);
assert_eq!(unsafe { libc::isatty(slave) }, 1, "the slave must be a tty");
}
#[test]
fn bytes_written_to_the_master_arrive_at_the_slave() {
let pty = open().expect("openpty");
let mut master = std::fs::File::from(pty.master);
let mut slave = std::fs::File::from(pty.slave);
master.write_all(b"hello\n").unwrap();
let mut buf = [0u8; 64];
let n = slave.read(&mut buf).unwrap();
assert!(n > 0);
assert!(buf[..n].starts_with(b"hello"), "{:?}", &buf[..n]);
}
#[test]
fn window_size_is_copied_between_terminals() {
let a = open().expect("openpty");
let b = open().expect("openpty");
let want = libc::winsize {
ws_row: 40,
ws_col: 132,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe { libc::ioctl(a.slave.as_raw_fd(), libc::TIOCSWINSZ, &want) };
copy_window_size(a.slave.as_raw_fd(), b.slave.as_raw_fd());
let mut got: libc::winsize = unsafe { core::mem::zeroed() };
unsafe { libc::ioctl(b.slave.as_raw_fd(), libc::TIOCGWINSZ, &mut got) };
assert_eq!((got.ws_row, got.ws_col), (40, 132));
}
const MEANINGFUL: libc::tcflag_t = libc::ICANON | libc::ECHO | libc::ISIG | libc::IEXTEN;
fn serial() -> std::sync::MutexGuard<'static, ()> {
static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
SERIAL
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn lflags(fd: RawFd) -> libc::tcflag_t {
let mut t: libc::termios = unsafe { core::mem::zeroed() };
assert_eq!(unsafe { libc::tcgetattr(fd, &mut t) }, 0);
t.c_lflag & MEANINGFUL
}
#[test]
fn raw_mode_restores_the_original_settings_on_drop() {
let _serial = serial();
let pty = open().expect("openpty");
let fd = pty.slave.as_raw_fd();
let before = lflags(fd);
assert_ne!(
before & libc::ICANON,
0,
"a fresh pty starts in canonical mode"
);
{
let guard = RawMode::enable(fd).unwrap();
assert!(guard.is_some(), "a pty slave is a terminal");
let during = lflags(fd);
assert_eq!(
during & libc::ICANON,
0,
"raw mode leaves canonical input on"
);
assert_eq!(during & libc::ECHO, 0, "raw mode leaves local echo on");
}
assert_eq!(lflags(fd), before, "the terminal was not restored");
}
#[test]
fn raw_mode_is_restored_even_when_no_destructor_runs() {
let _serial = serial();
let pty = open().expect("openpty");
let fd = pty.slave.as_raw_fd();
let before = lflags(fd);
let guard = RawMode::enable(fd).expect("enable").expect("a terminal");
assert_ne!(lflags(fd), before, "raw mode did not take effect");
std::mem::forget(guard);
assert_ne!(lflags(fd), before, "something restored it early");
restore_all();
assert_eq!(lflags(fd), before, "the hook did not put the terminal back");
}
#[test]
fn a_restored_terminal_is_forgotten() {
let _serial = serial();
let pty = open().expect("openpty");
let fd = pty.slave.as_raw_fd();
let before = lflags(fd);
drop(RawMode::enable(fd).expect("enable"));
assert_eq!(lflags(fd), before);
let mut raw: libc::termios = unsafe { core::mem::zeroed() };
assert_eq!(unsafe { libc::tcgetattr(fd, &mut raw) }, 0);
unsafe { libc::cfmakeraw(&mut raw) };
assert_eq!(unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) }, 0);
let deliberate = lflags(fd);
restore_all();
assert_eq!(
lflags(fd),
deliberate,
"a descriptor nobody is holding was overwritten"
);
}
#[test]
fn a_terminal_restored_by_restore_all_is_forgotten_too() {
let _serial = serial();
let first = open().expect("openpty");
let stale = first.slave.as_raw_fd();
std::mem::forget(RawMode::enable(stale).expect("enable").expect("a terminal"));
restore_all();
let second = open().expect("openpty");
assert_eq!(
unsafe { libc::dup2(second.master.as_raw_fd(), stale) },
stale
);
let fd = second.slave.as_raw_fd();
let mut raw: libc::termios = unsafe { core::mem::zeroed() };
assert_eq!(unsafe { libc::tcgetattr(fd, &mut raw) }, 0);
unsafe { libc::cfmakeraw(&mut raw) };
assert_eq!(unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) }, 0);
let deliberate = lflags(fd);
restore_all();
assert_eq!(
lflags(fd),
deliberate,
"settings saved for descriptor {stale} were written to the file that now has it"
);
}
#[test]
fn raw_mode_restores_even_when_the_scope_panics() {
let _serial = serial();
let pty = open().expect("openpty");
let fd = pty.slave.as_raw_fd();
let before = lflags(fd);
let result = std::panic::catch_unwind(|| {
let _guard = RawMode::enable(fd).unwrap();
panic!("something went wrong inside the sandbox run");
});
assert!(result.is_err());
assert_eq!(lflags(fd), before, "raw mode outlived the panic");
}
#[test]
fn a_pipe_is_not_put_into_raw_mode() {
let mut fds = [0 as RawFd; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
let guard = RawMode::enable(fds[0]).unwrap();
assert!(guard.is_none(), "a pipe has no terminal settings to change");
unsafe {
libc::close(fds[0]);
libc::close(fds[1]);
}
}
}