1use std::fmt;
13use std::io;
14use std::sync::Arc;
15use std::sync::Mutex as StdMutex;
16use std::sync::atomic::{AtomicBool, Ordering};
17
18use bytes::Bytes;
19use tokio::sync::{broadcast, mpsc, oneshot};
20use tokio::task::{AbortHandle, JoinHandle};
21
22pub trait ChildTerminator: Send + Sync {
26 fn kill(&mut self) -> io::Result<()>;
28}
29
30pub struct PtyHandles {
35 pub _slave: Option<Box<dyn Send>>,
37 pub _master: Box<dyn Send>,
39}
40
41impl fmt::Debug for PtyHandles {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.debug_struct("PtyHandles").finish()
44 }
45}
46
47pub struct ProcessHandle {
55 writer_tx: mpsc::Sender<Vec<u8>>,
56 output_tx: broadcast::Sender<Bytes>,
57 killer: StdMutex<Option<Box<dyn ChildTerminator>>>,
58 reader_handle: StdMutex<Option<JoinHandle<()>>>,
59 reader_abort_handles: StdMutex<Vec<AbortHandle>>,
60 writer_handle: StdMutex<Option<JoinHandle<()>>>,
61 wait_handle: StdMutex<Option<JoinHandle<()>>>,
62 exit_status: Arc<AtomicBool>,
63 exit_code: Arc<StdMutex<Option<i32>>>,
64 _pty_handles: StdMutex<Option<PtyHandles>>,
66}
67
68impl fmt::Debug for ProcessHandle {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.debug_struct("ProcessHandle")
71 .field("has_exited", &self.has_exited())
72 .field("exit_code", &self.exit_code())
73 .finish()
74 }
75}
76
77impl ProcessHandle {
78 #[allow(clippy::too_many_arguments)]
80 pub fn new(
81 writer_tx: mpsc::Sender<Vec<u8>>,
82 output_tx: broadcast::Sender<Bytes>,
83 initial_output_rx: broadcast::Receiver<Bytes>,
84 killer: Box<dyn ChildTerminator>,
85 reader_handle: JoinHandle<()>,
86 reader_abort_handles: Vec<AbortHandle>,
87 writer_handle: JoinHandle<()>,
88 wait_handle: JoinHandle<()>,
89 exit_status: Arc<AtomicBool>,
90 exit_code: Arc<StdMutex<Option<i32>>>,
91 pty_handles: Option<PtyHandles>,
92 ) -> (Self, broadcast::Receiver<Bytes>) {
93 (
94 Self {
95 writer_tx,
96 output_tx,
97 killer: StdMutex::new(Some(killer)),
98 reader_handle: StdMutex::new(Some(reader_handle)),
99 reader_abort_handles: StdMutex::new(reader_abort_handles),
100 writer_handle: StdMutex::new(Some(writer_handle)),
101 wait_handle: StdMutex::new(Some(wait_handle)),
102 exit_status,
103 exit_code,
104 _pty_handles: StdMutex::new(pty_handles),
105 },
106 initial_output_rx,
107 )
108 }
109
110 #[inline]
118 pub fn writer_sender(&self) -> mpsc::Sender<Vec<u8>> {
119 self.writer_tx.clone()
120 }
121
122 #[inline]
127 pub fn output_receiver(&self) -> broadcast::Receiver<Bytes> {
128 self.output_tx.subscribe()
129 }
130
131 #[inline]
133 pub fn has_exited(&self) -> bool {
134 self.exit_status.load(Ordering::SeqCst)
135 }
136
137 #[inline]
139 pub fn exit_code(&self) -> Option<i32> {
140 self.exit_code.lock().ok().and_then(|guard| *guard)
141 }
142
143 #[inline]
145 pub fn is_output_drained(&self) -> bool {
146 self.reader_handle
147 .lock()
148 .ok()
149 .and_then(|guard| guard.as_ref().map(JoinHandle::is_finished))
150 .unwrap_or(true)
151 }
152
153 pub fn terminate(&self) {
157 self.terminate_internal();
158 }
159
160 fn terminate_internal(&self) {
162 if let Ok(mut killer_opt) = self.killer.lock()
164 && let Some(mut killer) = killer_opt.take()
165 {
166 let _ = killer.kill();
167 }
168
169 self.abort_tasks();
170 }
171
172 fn abort_tasks(&self) {
174 if let Ok(mut h) = self.reader_handle.lock()
176 && let Some(handle) = h.take()
177 {
178 handle.abort();
179 }
180
181 if let Ok(mut handles) = self.reader_abort_handles.lock() {
183 for handle in handles.drain(..) {
184 handle.abort();
185 }
186 }
187
188 if let Ok(mut h) = self.writer_handle.lock()
190 && let Some(handle) = h.take()
191 {
192 handle.abort();
193 }
194
195 if let Ok(mut h) = self.wait_handle.lock()
197 && let Some(handle) = h.take()
198 {
199 handle.abort();
200 }
201 }
202
203 #[inline]
205 pub fn is_running(&self) -> bool {
206 !self.has_exited() && !self.is_writer_closed()
207 }
208
209 pub async fn write(
213 &self,
214 bytes: impl Into<Vec<u8>>,
215 ) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
216 self.writer_tx.send(bytes.into()).await
217 }
218
219 #[inline]
221 pub fn is_writer_closed(&self) -> bool {
222 self.writer_tx.is_closed()
223 }
224}
225
226impl Drop for ProcessHandle {
227 fn drop(&mut self) {
228 self.terminate_internal();
229 }
230}
231
232#[derive(Debug)]
236pub struct SpawnedProcess {
237 pub session: ProcessHandle,
239 pub output_rx: broadcast::Receiver<Bytes>,
241 pub exit_rx: oneshot::Receiver<i32>,
243}
244
245impl SpawnedProcess {
246 pub async fn wait_with_output(self, timeout_ms: u64) -> (Vec<u8>, i32) {
250 collect_output_until_exit(self.output_rx, self.exit_rx, timeout_ms).await
251 }
252}
253
254pub async fn collect_output_until_exit(
258 mut output_rx: broadcast::Receiver<Bytes>,
259 exit_rx: oneshot::Receiver<i32>,
260 timeout_ms: u64,
261) -> (Vec<u8>, i32) {
262 let mut collected = Vec::new();
263 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
264 tokio::pin!(exit_rx);
265
266 loop {
267 tokio::select! {
268 res = output_rx.recv() => {
269 if let Ok(chunk) = res {
270 collected.extend_from_slice(&chunk);
271 }
272 }
273 res = &mut exit_rx => {
274 let code = res.unwrap_or(-1);
275 let quiet = tokio::time::Duration::from_millis(50);
277 let max_deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(500);
278
279 while tokio::time::Instant::now() < max_deadline {
280 match tokio::time::timeout(quiet, output_rx.recv()).await {
281 Ok(Ok(chunk)) => collected.extend_from_slice(&chunk),
282 Ok(Err(broadcast::error::RecvError::Lagged(count))) => {
283 eprintln!("[vtcode] output stream lagged ({count} dropped)");
284 continue;
285 }
286 Ok(Err(broadcast::error::RecvError::Closed)) => break,
287 Err(_) => break, }
289 }
290 return (collected, code);
291 }
292 _ = tokio::time::sleep_until(deadline) => {
293 return (collected, -1);
294 }
295 }
296 }
297}
298
299pub type ExecCommandSession = ProcessHandle;
301
302pub type SpawnedPty = SpawnedProcess;
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 struct NoopTerminator;
310 impl ChildTerminator for NoopTerminator {
311 fn kill(&mut self) -> io::Result<()> {
312 Ok(())
313 }
314 }
315
316 #[tokio::test]
317 async fn test_process_handle_debug() {
318 let exit_status = Arc::new(AtomicBool::new(false));
320 let exit_code = Arc::new(StdMutex::new(None));
321
322 let (writer_tx, _) = mpsc::channel(1);
323 let (output_tx, initial_rx) = broadcast::channel(1);
324
325 let (handle, _) = ProcessHandle::new(
326 writer_tx,
327 output_tx,
328 initial_rx,
329 Box::new(NoopTerminator),
330 tokio::spawn(async {}),
331 vec![],
332 tokio::spawn(async {}),
333 tokio::spawn(async {}),
334 exit_status,
335 exit_code,
336 None,
337 );
338
339 let debug_str = format!("{handle:?}");
340 assert!(debug_str.contains("ProcessHandle"));
341 }
342
343 #[tokio::test]
344 async fn test_has_exited() {
345 let exit_status = Arc::new(AtomicBool::new(false));
346 let exit_code = Arc::new(StdMutex::new(None));
347
348 let (writer_tx, _) = mpsc::channel(1);
349 let (output_tx, initial_rx) = broadcast::channel(1);
350
351 let (handle, _) = ProcessHandle::new(
352 writer_tx,
353 output_tx,
354 initial_rx,
355 Box::new(NoopTerminator),
356 tokio::spawn(async {}),
357 vec![],
358 tokio::spawn(async {}),
359 tokio::spawn(async {}),
360 Arc::clone(&exit_status),
361 exit_code,
362 None,
363 );
364
365 assert!(!handle.has_exited());
366 exit_status.store(true, Ordering::SeqCst);
367 assert!(handle.has_exited());
368 }
369}