Skip to main content

shell_tunnel/pty/
async_adapter.rs

1//! Async adapters for PTY I/O.
2//!
3//! These adapters convert blocking PTY read/write operations into
4//! async-friendly channel-based communication to avoid blocking
5//! the tokio runtime.
6
7use std::io::{Read, Write};
8use tokio::sync::mpsc;
9use tracing::{debug, error, trace};
10
11/// Async reader for PTY output.
12///
13/// Runs in a blocking thread and sends output chunks through a channel.
14pub struct AsyncPtyReader<R: Read + Send + 'static> {
15    reader: R,
16    tx: mpsc::Sender<Vec<u8>>,
17    buffer_size: usize,
18}
19
20impl<R: Read + Send + 'static> AsyncPtyReader<R> {
21    /// Create a new AsyncPtyReader.
22    ///
23    /// # Arguments
24    ///
25    /// * `reader` - The PTY reader (blocking).
26    /// * `tx` - Channel sender for output data.
27    pub fn new(reader: R, tx: mpsc::Sender<Vec<u8>>) -> Self {
28        Self {
29            reader,
30            tx,
31            buffer_size: 4096,
32        }
33    }
34
35    /// Create with custom buffer size.
36    pub fn with_buffer_size(mut self, size: usize) -> Self {
37        self.buffer_size = size;
38        self
39    }
40
41    /// Start the reader loop in a blocking thread.
42    ///
43    /// This method spawns a blocking task that reads from the PTY
44    /// and sends data through the channel. It returns when:
45    /// - The PTY is closed (read returns 0 or EIO)
46    /// - The channel is closed (receiver dropped)
47    /// - An unrecoverable error occurs
48    pub async fn run(self) {
49        let buffer_size = self.buffer_size;
50        let mut reader = self.reader;
51        let tx = self.tx;
52
53        let result = tokio::task::spawn_blocking(move || {
54            let mut buf = vec![0u8; buffer_size];
55
56            loop {
57                match reader.read(&mut buf) {
58                    Ok(0) => {
59                        debug!("PTY reader: EOF");
60                        break;
61                    }
62                    Ok(n) => {
63                        trace!("PTY reader: read {} bytes", n);
64                        if tx.blocking_send(buf[..n].to_vec()).is_err() {
65                            debug!("PTY reader: channel closed");
66                            break;
67                        }
68                    }
69                    Err(e) => {
70                        // EIO on Unix typically means the PTY slave was closed
71                        #[cfg(unix)]
72                        if e.raw_os_error() == Some(libc::EIO) {
73                            debug!("PTY reader: PTY closed (EIO)");
74                            break;
75                        }
76
77                        // Check for broken pipe or similar
78                        if e.kind() == std::io::ErrorKind::BrokenPipe {
79                            debug!("PTY reader: broken pipe");
80                            break;
81                        }
82
83                        error!("PTY reader error: {}", e);
84                        break;
85                    }
86                }
87            }
88        })
89        .await;
90
91        if let Err(e) = result {
92            error!("PTY reader task panicked: {}", e);
93        }
94    }
95}
96
97/// Async writer for PTY input.
98///
99/// Receives data through a channel and writes to the PTY in a blocking thread.
100pub struct AsyncPtyWriter<W: Write + Send + 'static> {
101    writer: W,
102    rx: mpsc::Receiver<Vec<u8>>,
103}
104
105impl<W: Write + Send + 'static> AsyncPtyWriter<W> {
106    /// Create a new AsyncPtyWriter.
107    ///
108    /// # Arguments
109    ///
110    /// * `writer` - The PTY writer (blocking).
111    /// * `rx` - Channel receiver for input data.
112    pub fn new(writer: W, rx: mpsc::Receiver<Vec<u8>>) -> Self {
113        Self { writer, rx }
114    }
115
116    /// Start the writer loop in a blocking thread.
117    ///
118    /// This method spawns a blocking task that receives data from
119    /// the channel and writes it to the PTY. It returns when:
120    /// - The channel is closed (sender dropped)
121    /// - An unrecoverable error occurs
122    pub async fn run(self) {
123        let mut writer = self.writer;
124        let mut rx = self.rx;
125
126        let result = tokio::task::spawn_blocking(move || {
127            // We need to receive in a blocking way inside spawn_blocking
128            // Use a small wrapper to bridge async recv
129            while let Some(data) = {
130                // This is a bit awkward, but we need to block on the receive
131                // We'll use a small runtime just for this
132                tokio::runtime::Handle::try_current()
133                    .ok()
134                    .and_then(|h| h.block_on(async { rx.recv().await }))
135                    .or_else(|| {
136                        // Fallback: try blocking receive
137                        rx.blocking_recv()
138                    })
139            } {
140                trace!("PTY writer: writing {} bytes", data.len());
141                if let Err(e) = writer.write_all(&data) {
142                    if e.kind() == std::io::ErrorKind::BrokenPipe {
143                        debug!("PTY writer: broken pipe");
144                        break;
145                    }
146                    error!("PTY writer error: {}", e);
147                    break;
148                }
149                if let Err(e) = writer.flush() {
150                    error!("PTY writer flush error: {}", e);
151                    break;
152                }
153            }
154            debug!("PTY writer: channel closed");
155        })
156        .await;
157
158        if let Err(e) = result {
159            error!("PTY writer task panicked: {}", e);
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::io::Cursor;
168    use std::time::Duration;
169
170    #[tokio::test]
171    async fn test_async_reader_basic() {
172        // Create a reader with some test data
173        let data = b"Hello, World!\nTest line 2\n";
174        let cursor = Cursor::new(data.to_vec());
175
176        let (tx, mut rx) = mpsc::channel(32);
177        let reader = AsyncPtyReader::new(cursor, tx);
178
179        // Run reader in background
180        let handle = tokio::spawn(reader.run());
181
182        // Collect all received data
183        let mut received = Vec::new();
184        while let Ok(Some(chunk)) =
185            tokio::time::timeout(Duration::from_millis(100), rx.recv()).await
186        {
187            received.extend(chunk);
188        }
189
190        // Wait for reader to finish
191        let _ = tokio::time::timeout(Duration::from_millis(100), handle).await;
192
193        assert_eq!(received, data);
194    }
195
196    #[tokio::test]
197    async fn test_async_reader_empty() {
198        let cursor = Cursor::new(Vec::new());
199        let (tx, mut rx) = mpsc::channel(32);
200        let reader = AsyncPtyReader::new(cursor, tx);
201
202        let handle = tokio::spawn(reader.run());
203
204        // Should complete quickly with no data
205        let result = tokio::time::timeout(Duration::from_millis(100), rx.recv()).await;
206        assert!(result.is_ok());
207        assert!(result.unwrap().is_none()); // Channel closed, no data
208
209        let _ = handle.await;
210    }
211
212    #[tokio::test]
213    async fn test_async_writer_basic() {
214        let buffer = Vec::new();
215        let cursor = Cursor::new(buffer);
216
217        let (tx, rx) = mpsc::channel(32);
218        let writer = AsyncPtyWriter::new(cursor, rx);
219
220        // Send some data
221        tx.send(b"Hello".to_vec()).await.unwrap();
222        tx.send(b", World!".to_vec()).await.unwrap();
223        drop(tx); // Close the channel
224
225        // Run writer
226        let handle = tokio::spawn(writer.run());
227        let _ = tokio::time::timeout(Duration::from_millis(500), handle).await;
228
229        // Note: We can't easily check the output here because Cursor moves
230        // This test mainly verifies no panics/deadlocks
231    }
232
233    #[tokio::test]
234    async fn test_channel_creation() {
235        // Test that we can create channels for PTY communication
236        let (reader_tx, mut reader_rx) = mpsc::channel::<Vec<u8>>(32);
237        let (writer_tx, _writer_rx) = mpsc::channel::<Vec<u8>>(32);
238
239        // Verify channels work
240        reader_tx.send(b"test".to_vec()).await.unwrap();
241        let received = reader_rx.recv().await.unwrap();
242        assert_eq!(received, b"test");
243
244        writer_tx.send(b"input".to_vec()).await.unwrap();
245    }
246
247    #[tokio::test]
248    async fn test_reader_channel_closed() {
249        let data = b"Some data that won't be fully read";
250        let cursor = Cursor::new(data.to_vec());
251
252        let (tx, rx) = mpsc::channel(1); // Small buffer
253        let reader = AsyncPtyReader::new(cursor, tx);
254
255        // Drop receiver immediately
256        drop(rx);
257
258        // Reader should handle closed channel gracefully
259        let handle = tokio::spawn(reader.run());
260        let result = tokio::time::timeout(Duration::from_millis(100), handle).await;
261        assert!(result.is_ok()); // Should complete without hanging
262    }
263}