use anyhow::{Context, Result};
use nix::fcntl::{fcntl, FcntlArg, OFlag};
use nix::pty::{openpty, OpenptyResult, Winsize};
use nix::sys::termios::{self, SetArg, Termios};
use nix::unistd::{close, dup2, read, setsid, write};
use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
pub struct Pty {
master: OwnedFd,
slave: OwnedFd,
original_termios: Option<Termios>,
}
impl Pty {
pub fn open() -> Result<Self> {
let OpenptyResult { master, slave } = openpty(None, None)
.context("failed to open PTY")?;
let flags = fcntl(master.as_raw_fd(), FcntlArg::F_GETFL)
.context("failed to get PTY flags")?;
let new_flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK;
fcntl(master.as_raw_fd(), FcntlArg::F_SETFL(new_flags))
.context("failed to set PTY flags")?;
Ok(Self {
master,
slave,
original_termios: None,
})
}
pub fn open_with_size(cols: u16, rows: u16) -> Result<Self> {
let winsize = Winsize {
ws_col: cols,
ws_row: rows,
ws_xpixel: 0,
ws_ypixel: 0,
};
let OpenptyResult { master, slave } = openpty(Some(&winsize), None)
.context("failed to open PTY with size")?;
let flags = fcntl(master.as_raw_fd(), FcntlArg::F_GETFL)
.context("failed to get PTY flags")?;
let new_flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK;
fcntl(master.as_raw_fd(), FcntlArg::F_SETFL(new_flags))
.context("failed to set PTY flags")?;
Ok(Self {
master,
slave,
original_termios: None,
})
}
pub fn master_fd(&self) -> RawFd {
self.master.as_raw_fd()
}
pub fn slave_fd(&self) -> RawFd {
self.slave.as_raw_fd()
}
pub fn resize(&self, cols: u16, rows: u16) -> Result<()> {
let winsize = libc::winsize {
ws_col: cols,
ws_row: rows,
ws_xpixel: 0,
ws_ypixel: 0,
};
let ret = unsafe {
libc::ioctl(self.master.as_raw_fd(), libc::TIOCSWINSZ, &winsize)
};
if ret < 0 {
anyhow::bail!("failed to resize PTY: {}", std::io::Error::last_os_error());
}
tracing::debug!("PTY resized to {}x{}", cols, rows);
Ok(())
}
pub fn get_size(&self) -> Result<(u16, u16)> {
let mut winsize = libc::winsize {
ws_col: 0,
ws_row: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
let ret = unsafe {
libc::ioctl(self.master.as_raw_fd(), libc::TIOCGWINSZ, &mut winsize)
};
if ret < 0 {
anyhow::bail!("failed to get PTY size: {}", std::io::Error::last_os_error());
}
Ok((winsize.ws_col, winsize.ws_row))
}
pub unsafe fn setup_slave_for_child(&self) -> Result<()> {
setsid().context("failed to create new session")?;
let ret = unsafe {
libc::ioctl(self.slave.as_raw_fd(), libc::TIOCSCTTY as libc::c_ulong, 0)
};
if ret < 0 {
anyhow::bail!("failed to set controlling terminal: {}", std::io::Error::last_os_error());
}
dup2(self.slave.as_raw_fd(), libc::STDIN_FILENO)
.context("failed to dup2 stdin")?;
dup2(self.slave.as_raw_fd(), libc::STDOUT_FILENO)
.context("failed to dup2 stdout")?;
dup2(self.slave.as_raw_fd(), libc::STDERR_FILENO)
.context("failed to dup2 stderr")?;
if self.slave.as_raw_fd() > libc::STDERR_FILENO {
let _ = unsafe { libc::close(self.slave.as_raw_fd()) };
}
Ok(())
}
pub fn close_slave(&mut self) -> Result<()> {
let slave_fd = self.slave.as_raw_fd();
if slave_fd >= 0 {
close(slave_fd).ok();
}
Ok(())
}
pub fn set_raw_mode(&mut self) -> Result<()> {
let mut termios = termios::tcgetattr(&self.slave)
.context("failed to get terminal attributes")?;
self.original_termios = Some(termios.clone());
termios::cfmakeraw(&mut termios);
termios::tcsetattr(&self.slave, SetArg::TCSANOW, &termios)
.context("failed to set raw mode")?;
Ok(())
}
pub fn restore_termios(&self) -> Result<()> {
if let Some(ref original) = self.original_termios {
termios::tcsetattr(&self.slave, SetArg::TCSANOW, original)
.context("failed to restore terminal settings")?;
}
Ok(())
}
pub fn write_input(&self, data: &[u8]) -> Result<usize> {
let n = write(&self.master, data)
.context("failed to write to PTY")?;
Ok(n)
}
pub fn read_output(&self, buf: &mut [u8]) -> Result<usize> {
match read(self.master.as_raw_fd(), buf) {
Ok(n) => Ok(n),
Err(nix::errno::Errno::EAGAIN) => Ok(0),
Err(e) => Err(e).context("failed to read from PTY"),
}
}
}
pub struct PtyHandle {
pty: Pty,
active: bool,
}
impl PtyHandle {
pub fn new(cols: u16, rows: u16) -> Result<Self> {
let pty = Pty::open_with_size(cols, rows)?;
Ok(Self { pty, active: true })
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn deactivate(&mut self) {
self.active = false;
}
pub fn resize(&self, cols: u16, rows: u16) -> Result<()> {
if self.active {
self.pty.resize(cols, rows)
} else {
Ok(())
}
}
pub fn master_fd(&self) -> RawFd {
self.pty.master_fd()
}
pub fn slave_fd(&self) -> RawFd {
self.pty.slave_fd()
}
pub fn pty(&self) -> &Pty {
&self.pty
}
pub fn pty_mut(&mut self) -> &mut Pty {
&mut self.pty
}
}
pub struct ExecSession {
pub id: String,
pub pty: Option<PtyHandle>,
pub pid: Option<u32>,
pub exit_code: Option<i32>,
pub running: bool,
}
impl ExecSession {
pub fn new(id: String, tty: bool, cols: u16, rows: u16) -> Result<Self> {
let pty = if tty {
Some(PtyHandle::new(cols, rows)?)
} else {
None
};
Ok(Self {
id,
pty,
pid: None,
exit_code: None,
running: false,
})
}
pub fn has_tty(&self) -> bool {
self.pty.is_some()
}
pub fn resize(&self, cols: u16, rows: u16) -> Result<()> {
if let Some(ref pty) = self.pty {
pty.resize(cols, rows)
} else {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pty_open() {
let pty = Pty::open();
assert!(pty.is_ok());
let pty = pty.unwrap();
assert!(pty.master_fd() >= 0);
assert!(pty.slave_fd() >= 0);
}
#[test]
fn test_pty_open_with_size() {
let pty = Pty::open_with_size(80, 24);
assert!(pty.is_ok());
let pty = pty.unwrap();
let (cols, rows) = pty.get_size().unwrap();
assert_eq!(cols, 80);
assert_eq!(rows, 24);
}
#[test]
fn test_pty_resize() {
let pty = Pty::open_with_size(80, 24).unwrap();
pty.resize(120, 40).unwrap();
let (cols, rows) = pty.get_size().unwrap();
assert_eq!(cols, 120);
assert_eq!(rows, 40);
}
#[test]
fn test_pty_handle() {
let handle = PtyHandle::new(100, 30);
assert!(handle.is_ok());
let handle = handle.unwrap();
assert!(handle.is_active());
assert!(handle.master_fd() >= 0);
assert!(handle.slave_fd() >= 0);
}
#[test]
fn test_exec_session_with_tty() {
let session = ExecSession::new("test-session".to_string(), true, 80, 24);
assert!(session.is_ok());
let session = session.unwrap();
assert!(session.has_tty());
assert!(!session.running);
assert!(session.pid.is_none());
}
#[test]
fn test_exec_session_without_tty() {
let session = ExecSession::new("test-session".to_string(), false, 80, 24);
assert!(session.is_ok());
let session = session.unwrap();
assert!(!session.has_tty());
}
}