use std::os::unix::io::{FromRawFd, OwnedFd, RawFd};
use crate::{AppError, AppResult, ErrorCode};
use super::size::PtySize;
pub(crate) struct PtyPair {
pub(crate) master: OwnedFd,
pub(crate) slave: OwnedFd,
}
pub(crate) fn open_pty(size: PtySize) -> AppResult<PtyPair> {
let mut master_fd: RawFd = -1;
let mut slave_fd: RawFd = -1;
let mut winsize = size.to_winsize();
let result = unsafe {
libc::openpty(
&raw mut master_fd,
&raw mut slave_fd,
std::ptr::null_mut(),
std::ptr::null_mut(),
&raw mut winsize,
)
};
if result != 0 {
return Err(AppError::new(
ErrorCode::Internal,
format!(
"failed to allocate pseudoterminal: {}",
std::io::Error::last_os_error()
),
));
}
let master = unsafe { OwnedFd::from_raw_fd(master_fd) };
let slave = unsafe { OwnedFd::from_raw_fd(slave_fd) };
set_nonblocking(&master)?;
set_cloexec(&master)?;
Ok(PtyPair { master, slave })
}
fn set_nonblocking(fd: &OwnedFd) -> AppResult<()> {
use std::os::unix::io::AsRawFd;
let raw = fd.as_raw_fd();
let flags = unsafe { libc::fcntl(raw, libc::F_GETFL) };
if flags < 0 {
return Err(fcntl_error("F_GETFL"));
}
if unsafe { libc::fcntl(raw, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 {
return Err(fcntl_error("F_SETFL O_NONBLOCK"));
}
Ok(())
}
fn set_cloexec(fd: &OwnedFd) -> AppResult<()> {
use std::os::unix::io::AsRawFd;
let raw = fd.as_raw_fd();
let flags = unsafe { libc::fcntl(raw, libc::F_GETFD) };
if flags < 0 {
return Err(fcntl_error("F_GETFD"));
}
if unsafe { libc::fcntl(raw, libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0 {
return Err(fcntl_error("F_SETFD FD_CLOEXEC"));
}
Ok(())
}
fn fcntl_error(op: &str) -> AppError {
AppError::new(
ErrorCode::Internal,
format!(
"failed to configure pseudoterminal master ({op}): {}",
std::io::Error::last_os_error()
),
)
}
#[cfg(test)]
mod tests {
use std::os::unix::io::AsRawFd;
use super::*;
#[test]
fn open_pty_returns_distinct_live_fds() {
let pair = open_pty(PtySize::new(30, 100)).expect("openpty");
assert!(pair.master.as_raw_fd() >= 0);
assert!(pair.slave.as_raw_fd() >= 0);
assert_ne!(pair.master.as_raw_fd(), pair.slave.as_raw_fd());
}
#[test]
fn master_is_nonblocking_and_cloexec() {
let pair = open_pty(PtySize::default()).expect("openpty");
let raw = pair.master.as_raw_fd();
let status = unsafe { libc::fcntl(raw, libc::F_GETFL) };
assert!(
status & libc::O_NONBLOCK != 0,
"master must be non-blocking"
);
let descriptor = unsafe { libc::fcntl(raw, libc::F_GETFD) };
assert!(
descriptor & libc::FD_CLOEXEC != 0,
"master must be close-on-exec"
);
}
#[test]
fn fcntl_error_uses_internal_code() {
let error = fcntl_error("TEST");
assert_eq!(error.code(), ErrorCode::Internal);
assert!(error.to_string().contains("TEST"));
}
}