use anyhow::{Context, Result};
use nix::pty::{openpty, OpenptyResult, Winsize};
use std::fs::File;
use std::io::{Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
use std::os::unix::process::CommandExt;
use std::process::{Child, Command};
use tokio::sync::broadcast;
use crate::core::event_router::{CaptureEvent, Event, EventRouter, LifecycleEvent};
fn set_cloexec(fd: RawFd) -> std::io::Result<()> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
return Err(std::io::Error::last_os_error());
}
if unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
fn parent_winsize() -> Winsize {
let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
let ok = unsafe { libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut ws) } == 0;
if ok && ws.ws_row > 0 && ws.ws_col > 0 {
Winsize {
ws_row: ws.ws_row,
ws_col: ws.ws_col,
ws_xpixel: ws.ws_xpixel,
ws_ypixel: ws.ws_ypixel,
}
} else {
Winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
}
}
}
pub struct PtyResizer {
master: File,
}
impl PtyResizer {
pub fn resize(&self, rows: u16, cols: u16) -> Result<()> {
let ws = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
let ret = unsafe { libc::ioctl(self.master.as_raw_fd(), libc::TIOCSWINSZ, &ws) };
if ret != 0 {
return Err(anyhow::Error::new(std::io::Error::last_os_error())
.context("ioctl(TIOCSWINSZ) failed"));
}
Ok(())
}
}
pub struct PtyShell {
_child: Child,
master_read: File,
master_write: File,
}
impl PtyShell {
pub fn spawn(program: &str) -> Result<Self> {
let winsize = parent_winsize();
let OpenptyResult { master, slave } =
openpty(&winsize, None).context("Failed to create PTY")?;
set_cloexec(master.as_raw_fd()).context("Failed to set FD_CLOEXEC on PTY master")?;
let slave_fd = slave.as_raw_fd();
let child = unsafe {
Command::new(program)
.pre_exec(move || {
for &signo in &[
libc::SIGCHLD,
libc::SIGHUP,
libc::SIGINT,
libc::SIGQUIT,
libc::SIGTERM,
libc::SIGALRM,
libc::SIGPIPE,
] {
libc::signal(signo, libc::SIG_DFL);
}
let empty: libc::sigset_t = std::mem::zeroed();
libc::sigprocmask(libc::SIG_SETMASK, &empty, std::ptr::null_mut());
if libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::ioctl(slave_fd, libc::TIOCSCTTY as _, 0) < 0 {
return Err(std::io::Error::last_os_error());
}
libc::dup2(slave_fd, 0);
libc::dup2(slave_fd, 1);
libc::dup2(slave_fd, 2);
if slave_fd > 2 {
libc::close(slave_fd);
}
Ok(())
})
.spawn()
.context(format!("Failed to spawn {}", program))?
};
drop(slave);
let master_fd = master.as_raw_fd();
let master_read = unsafe { File::from_raw_fd(libc::dup(master_fd)) };
let master_write = unsafe { File::from_raw_fd(master.into_raw_fd()) };
Ok(Self {
_child: child,
master_read,
master_write,
})
}
pub fn get_writer(&self) -> Result<File> {
self.master_write
.try_clone()
.context("Failed to clone PTY master writer")
}
pub fn get_reader(&self) -> Result<File> {
self.master_read
.try_clone()
.context("Failed to clone PTY master reader")
}
pub fn get_resizer(&self) -> Result<PtyResizer> {
let master = self
.master_write
.try_clone()
.context("Failed to clone PTY master for resize")?;
Ok(PtyResizer { master })
}
pub fn forward_output(
&mut self,
mut event_rx: broadcast::Receiver<Event>,
router: EventRouter,
) -> Result<()> {
let mut reader = self.get_reader()?;
let mut stdout = std::io::stdout();
let mut buf = [0u8; 4096];
let reader_fd = reader.as_raw_fd();
let mut shutdown_received = false;
const POLL_TIMEOUT_MS: libc::c_int = 100;
loop {
match event_rx.try_recv() {
Ok(Event::Lifecycle(LifecycleEvent::Shutdown)) => {
log::debug!("Shell forwarder received shutdown signal");
shutdown_received = true;
break;
}
Ok(_) => {} Err(broadcast::error::TryRecvError::Empty) => {}
Err(broadcast::error::TryRecvError::Closed) => break,
Err(broadcast::error::TryRecvError::Lagged(_)) => {}
}
if matches!(self._child.try_wait(), Ok(Some(_))) {
log::debug!("Shell process exited; stopping forwarder");
break;
}
let mut pfd = libc::pollfd {
fd: reader_fd,
events: libc::POLLIN,
revents: 0,
};
let n = unsafe { libc::poll(&mut pfd, 1, POLL_TIMEOUT_MS) };
if n < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
log::warn!("poll() on PTY master failed: {}", err);
break;
}
if n == 0 {
continue;
}
match reader.read(&mut buf) {
Ok(0) => break, Ok(n) => {
stdout.write_all(&buf[..n])?;
stdout.flush()?;
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => break, }
}
if !shutdown_received {
router.send(Event::Capture(CaptureEvent::Stop));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_pty_shell_creation() {
use super::*;
if std::env::var("CI").is_ok() {
return;
}
if let Ok(shell) = PtyShell::spawn("/bin/echo world") {
let world = &mut String::new();
shell.get_reader().unwrap().read_to_string(world).unwrap();
assert!(world.contains("world"));
}
}
}