use std::os::unix::io::RawFd;
use crate::ConsoleIo;
pub struct PtyConsole {
master_fd: RawFd,
slave_fd: RawFd,
slave_path: String,
nonblocking: bool,
}
impl PtyConsole {
pub fn new() -> std::io::Result<Self> {
let mut master_fd: libc::c_int = -1;
let mut slave_fd: libc::c_int = -1;
let ret = unsafe {
libc::openpty(
&mut master_fd,
&mut slave_fd,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
let slave_path = unsafe {
let path_ptr = libc::ttyname(slave_fd);
if path_ptr.is_null() {
libc::close(master_fd);
libc::close(slave_fd);
return Err(std::io::Error::last_os_error());
}
std::ffi::CStr::from_ptr(path_ptr)
.to_string_lossy()
.into_owned()
};
tracing::info!(
"Created PTY console: master_fd={}, slave={}",
master_fd,
slave_path
);
Ok(Self {
master_fd,
slave_fd,
slave_path,
nonblocking: false,
})
}
#[must_use]
pub fn slave_path(&self) -> &str {
&self.slave_path
}
#[must_use]
pub const fn master_fd(&self) -> RawFd {
self.master_fd
}
pub fn set_nonblocking(&mut self, nonblocking: bool) -> std::io::Result<()> {
let flags = unsafe { libc::fcntl(self.master_fd, libc::F_GETFL) };
if flags < 0 {
return Err(std::io::Error::last_os_error());
}
let new_flags = if nonblocking {
flags | libc::O_NONBLOCK
} else {
flags & !libc::O_NONBLOCK
};
let ret = unsafe { libc::fcntl(self.master_fd, libc::F_SETFL, new_flags) };
if ret < 0 {
return Err(std::io::Error::last_os_error());
}
self.nonblocking = nonblocking;
Ok(())
}
pub fn set_window_size(&self, rows: u16, cols: u16) -> std::io::Result<()> {
let ws = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
let ret = unsafe { libc::ioctl(self.master_fd, libc::TIOCSWINSZ, &ws) };
if ret < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
#[must_use]
pub fn has_data(&self) -> bool {
let mut pollfd = libc::pollfd {
fd: self.master_fd,
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&mut pollfd, 1, 0) };
ret > 0 && (pollfd.revents & libc::POLLIN) != 0
}
}
impl Drop for PtyConsole {
fn drop(&mut self) {
if self.master_fd >= 0 {
unsafe { libc::close(self.master_fd) };
}
if self.slave_fd >= 0 {
unsafe { libc::close(self.slave_fd) };
}
}
}
impl ConsoleIo for PtyConsole {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let ret = unsafe {
libc::read(
self.master_fd,
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
)
};
if ret < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::WouldBlock {
Ok(0)
} else {
Err(err)
}
} else {
Ok(ret as usize)
}
}
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let ret = unsafe {
libc::write(
self.master_fd,
buf.as_ptr() as *const libc::c_void,
buf.len(),
)
};
if ret < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(ret as usize)
}
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pty_console_creation() {
let pty = PtyConsole::new().unwrap();
assert!(!pty.slave_path().is_empty());
assert!(pty.master_fd() >= 0);
}
#[test]
fn test_pty_console_write_read() {
let mut pty = PtyConsole::new().unwrap();
pty.set_nonblocking(true).unwrap();
let written = pty.write(b"test\n").unwrap();
assert!(written > 0);
}
#[test]
fn test_pty_console_nonblocking() {
let mut pty = PtyConsole::new().unwrap();
pty.set_nonblocking(true).unwrap();
assert!(pty.nonblocking);
let mut buf = [0u8; 10];
let n = pty.read(&mut buf).unwrap();
assert_eq!(n, 0);
}
#[test]
fn test_pty_console_window_size() {
let pty = PtyConsole::new().unwrap();
assert!(pty.set_window_size(24, 80).is_ok());
assert!(pty.set_window_size(50, 132).is_ok());
}
#[test]
fn test_pty_console_has_data() {
let pty = PtyConsole::new().unwrap();
assert!(!pty.has_data());
}
#[test]
fn test_pty_console_flush() {
let mut pty = PtyConsole::new().unwrap();
assert!(pty.flush().is_ok());
}
}