use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::process::Stdio;
use std::sync::mpsc as std_mpsc;
use tokio::process::Command;
use tokio::sync::mpsc;
use crate::error::AdapterError;
use crate::observe::{Direction, SharedObserver};
use crate::supervision::SupervisedChild;
const DEFAULT_COLS: u16 = 120;
const DEFAULT_ROWS: u16 = 30;
use crate::bounds::READ_CHUNK;
pub fn spawn(mut command: Command) -> Result<(SupervisedChild, PtyMaster), AdapterError> {
let (master, slave) =
open_pty().map_err(|e| AdapterError::io("agent_pty_unavailable", "pty/open", e))?;
make_slave_raw(slave.as_raw_fd())
.map_err(|e| AdapterError::io("agent_pty_unavailable", "pty/termios", e))?;
set_winsize(master.as_raw_fd(), DEFAULT_COLS, DEFAULT_ROWS)
.map_err(|e| AdapterError::io("agent_pty_unavailable", "pty/winsize", e))?;
command
.stdin(Stdio::from(slave.try_clone().map_err(|e| {
AdapterError::io("agent_pty_unavailable", "pty/dup", e)
})?))
.stdout(Stdio::from(slave))
.stderr(Stdio::piped());
let child = SupervisedChild::spawn(
&mut command,
crate::supervision::IoPlan {
stdin: crate::supervision::StdioKind::Preserve,
stdout: crate::supervision::StdioKind::Preserve,
stderr: crate::supervision::StdioKind::Preserve,
},
)?;
drop(command);
Ok((child, PtyMaster::new(master)))
}
pub struct PtyMaster {
read_rx: mpsc::Receiver<io::Result<Vec<u8>>>,
write_tx: std_mpsc::SyncSender<WriteJob>,
resize_fd: RawFd,
observer: SharedObserver,
eof: bool,
}
struct WriteJob {
bytes: Vec<u8>,
done: tokio::sync::oneshot::Sender<io::Result<()>>,
}
const IO_QUEUE_CAPACITY: usize = 8;
impl PtyMaster {
fn new(master: OwnedFd) -> Self {
let writer_fd = master
.try_clone()
.expect("dup of fresh pty master cannot fail");
let resize_fd = master.as_raw_fd();
let (read_tx, read_rx) = mpsc::channel(IO_QUEUE_CAPACITY);
std::thread::spawn(move || {
let mut master = std::fs::File::from(master);
let mut buffer = [0u8; READ_CHUNK];
loop {
match master.read(&mut buffer) {
Ok(0) => break,
Ok(count) => {
if read_tx.blocking_send(Ok(buffer[..count].to_vec())).is_err() {
return; }
}
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
Err(error) => {
let terminal = error.raw_os_error() == Some(libc::EIO);
if !terminal {
let _ = read_tx.blocking_send(Err(error));
}
break;
}
}
}
let _ = read_tx.blocking_send(Ok(Vec::new())); });
let (write_tx, write_rx) = std_mpsc::sync_channel::<WriteJob>(IO_QUEUE_CAPACITY);
std::thread::spawn(move || {
let mut writer = std::fs::File::from(writer_fd);
while let Ok(job) = write_rx.recv() {
let result = writer.write_all(&job.bytes).and_then(|()| writer.flush());
let _ = job.done.send(result);
}
});
Self {
read_rx,
write_tx,
resize_fd,
observer: crate::observe::none(),
eof: false,
}
}
pub fn with_observer(mut self, observer: SharedObserver) -> Self {
self.observer = observer;
self
}
pub async fn read(&mut self) -> io::Result<Vec<u8>> {
if self.eof {
return Ok(Vec::new());
}
match self.read_rx.recv().await {
Some(Ok(bytes)) => {
if bytes.is_empty() {
self.eof = true;
} else if let Some(observer) = self.observer.as_ref() {
observer.record("pty", Direction::Received, &bytes);
}
Ok(bytes)
}
Some(Err(error)) => Err(error),
None => {
self.eof = true;
Ok(Vec::new())
}
}
}
pub fn writer(&self) -> PtyWriter {
PtyWriter {
write_tx: self.write_tx.clone(),
observer: self.observer.clone(),
}
}
pub async fn write(&self, bytes: &[u8]) -> io::Result<()> {
self.writer().write(bytes).await
}
pub fn resize(&self, cols: u16, rows: u16) -> io::Result<()> {
set_winsize(self.resize_fd, cols, rows)
}
}
#[derive(Clone)]
pub struct PtyWriter {
write_tx: std_mpsc::SyncSender<WriteJob>,
observer: SharedObserver,
}
impl PtyWriter {
pub async fn write(&self, bytes: &[u8]) -> io::Result<()> {
if let Some(observer) = self.observer.as_ref() {
observer.record("pty", Direction::Sent, bytes);
}
let (done, wait) = tokio::sync::oneshot::channel();
let job = WriteJob {
bytes: bytes.to_vec(),
done,
};
let send = self.write_tx.try_send(job);
match send {
Ok(()) => {}
Err(std_mpsc::TrySendError::Full(job)) => {
let tx = self.write_tx.clone();
tokio::task::spawn_blocking(move || tx.send(job))
.await
.map_err(|_| io::Error::other("pty writer join failed"))?
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "pty writer closed"))?;
}
Err(std_mpsc::TrySendError::Disconnected(_)) => {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"pty writer closed",
));
}
}
wait.await
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "pty writer dropped"))?
}
}
fn open_pty() -> io::Result<(OwnedFd, OwnedFd)> {
let master = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC) };
if master < 0 {
return Err(io::Error::last_os_error());
}
let master = unsafe { OwnedFd::from_raw_fd(master) };
unsafe {
if libc::grantpt(master.as_raw_fd()) != 0 || libc::unlockpt(master.as_raw_fd()) != 0 {
return Err(io::Error::last_os_error());
}
static PTSNAME: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = PTSNAME.lock().unwrap();
let name = libc::ptsname(master.as_raw_fd());
if name.is_null() {
return Err(io::Error::other("ptsname failed"));
}
let slave = libc::open(name, libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC);
if slave < 0 {
return Err(io::Error::last_os_error());
}
Ok((master, OwnedFd::from_raw_fd(slave)))
}
}
fn make_slave_raw(fd: RawFd) -> io::Result<()> {
unsafe {
let mut termios = std::mem::zeroed::<libc::termios>();
if libc::tcgetattr(fd, &mut termios) != 0 {
return Err(io::Error::last_os_error());
}
libc::cfmakeraw(&mut termios);
if libc::tcsetattr(fd, libc::TCSANOW, &termios) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn set_winsize(fd: RawFd, cols: u16, rows: u16) -> io::Result<()> {
let winsize = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
if unsafe { libc::ioctl(fd, libc::TIOCSWINSZ, &winsize) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}