use std::io::{Read, Write};
use tokio::sync::mpsc;
use tracing::{debug, error, trace};
pub struct AsyncPtyReader<R: Read + Send + 'static> {
reader: R,
tx: mpsc::Sender<Vec<u8>>,
buffer_size: usize,
}
impl<R: Read + Send + 'static> AsyncPtyReader<R> {
pub fn new(reader: R, tx: mpsc::Sender<Vec<u8>>) -> Self {
Self {
reader,
tx,
buffer_size: 4096,
}
}
pub fn with_buffer_size(mut self, size: usize) -> Self {
self.buffer_size = size;
self
}
pub async fn run(self) {
let buffer_size = self.buffer_size;
let mut reader = self.reader;
let tx = self.tx;
let result = tokio::task::spawn_blocking(move || {
let mut buf = vec![0u8; buffer_size];
loop {
match reader.read(&mut buf) {
Ok(0) => {
debug!("PTY reader: EOF");
break;
}
Ok(n) => {
trace!("PTY reader: read {} bytes", n);
if tx.blocking_send(buf[..n].to_vec()).is_err() {
debug!("PTY reader: channel closed");
break;
}
}
Err(e) => {
#[cfg(unix)]
if e.raw_os_error() == Some(libc::EIO) {
debug!("PTY reader: PTY closed (EIO)");
break;
}
if e.kind() == std::io::ErrorKind::BrokenPipe {
debug!("PTY reader: broken pipe");
break;
}
error!("PTY reader error: {}", e);
break;
}
}
}
})
.await;
if let Err(e) = result {
error!("PTY reader task panicked: {}", e);
}
}
}
pub struct AsyncPtyWriter<W: Write + Send + 'static> {
writer: W,
rx: mpsc::Receiver<Vec<u8>>,
}
impl<W: Write + Send + 'static> AsyncPtyWriter<W> {
pub fn new(writer: W, rx: mpsc::Receiver<Vec<u8>>) -> Self {
Self { writer, rx }
}
pub async fn run(self) {
let mut writer = self.writer;
let mut rx = self.rx;
let result = tokio::task::spawn_blocking(move || {
while let Some(data) = {
tokio::runtime::Handle::try_current()
.ok()
.and_then(|h| h.block_on(async { rx.recv().await }))
.or_else(|| {
rx.blocking_recv()
})
} {
trace!("PTY writer: writing {} bytes", data.len());
if let Err(e) = writer.write_all(&data) {
if e.kind() == std::io::ErrorKind::BrokenPipe {
debug!("PTY writer: broken pipe");
break;
}
error!("PTY writer error: {}", e);
break;
}
if let Err(e) = writer.flush() {
error!("PTY writer flush error: {}", e);
break;
}
}
debug!("PTY writer: channel closed");
})
.await;
if let Err(e) = result {
error!("PTY writer task panicked: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::time::Duration;
#[tokio::test]
async fn test_async_reader_basic() {
let data = b"Hello, World!\nTest line 2\n";
let cursor = Cursor::new(data.to_vec());
let (tx, mut rx) = mpsc::channel(32);
let reader = AsyncPtyReader::new(cursor, tx);
let handle = tokio::spawn(reader.run());
let mut received = Vec::new();
while let Ok(Some(chunk)) =
tokio::time::timeout(Duration::from_millis(100), rx.recv()).await
{
received.extend(chunk);
}
let _ = tokio::time::timeout(Duration::from_millis(100), handle).await;
assert_eq!(received, data);
}
#[tokio::test]
async fn test_async_reader_empty() {
let cursor = Cursor::new(Vec::new());
let (tx, mut rx) = mpsc::channel(32);
let reader = AsyncPtyReader::new(cursor, tx);
let handle = tokio::spawn(reader.run());
let result = tokio::time::timeout(Duration::from_millis(100), rx.recv()).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
let _ = handle.await;
}
#[tokio::test]
async fn test_async_writer_basic() {
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
let (tx, rx) = mpsc::channel(32);
let writer = AsyncPtyWriter::new(cursor, rx);
tx.send(b"Hello".to_vec()).await.unwrap();
tx.send(b", World!".to_vec()).await.unwrap();
drop(tx);
let handle = tokio::spawn(writer.run());
let _ = tokio::time::timeout(Duration::from_millis(500), handle).await;
}
#[tokio::test]
async fn test_channel_creation() {
let (reader_tx, mut reader_rx) = mpsc::channel::<Vec<u8>>(32);
let (writer_tx, _writer_rx) = mpsc::channel::<Vec<u8>>(32);
reader_tx.send(b"test".to_vec()).await.unwrap();
let received = reader_rx.recv().await.unwrap();
assert_eq!(received, b"test");
writer_tx.send(b"input".to_vec()).await.unwrap();
}
#[tokio::test]
async fn test_reader_channel_closed() {
let data = b"Some data that won't be fully read";
let cursor = Cursor::new(data.to_vec());
let (tx, rx) = mpsc::channel(1); let reader = AsyncPtyReader::new(cursor, tx);
drop(rx);
let handle = tokio::spawn(reader.run());
let result = tokio::time::timeout(Duration::from_millis(100), handle).await;
assert!(result.is_ok()); }
}