1use std::fmt;
23use std::io;
24use std::sync::Arc;
25use std::sync::Mutex as StdMutex;
26use std::sync::atomic::{AtomicBool, Ordering};
27
28use bytes::Bytes;
29use tokio::sync::{broadcast, mpsc, oneshot};
30use tokio::task::{AbortHandle, JoinHandle};
31
32pub(crate) fn async_drop<F, Fut>(f: F)
42where
43 F: FnOnce() -> Fut + Send + 'static,
44 Fut: Future<Output = ()> + Send + 'static,
45{
46 let handle = std::thread::spawn(move || {
47 let rt = match tokio::runtime::Runtime::new() {
48 Ok(rt) => rt,
49 Err(_) => return,
50 };
51 rt.block_on(f());
52 });
53 let _ = handle.join();
54}
55
56pub trait ChildTerminator: Send + Sync {
60 fn kill(&mut self) -> io::Result<()>;
62}
63
64pub struct PtyHandles {
69 pub _slave: Option<Box<dyn Send>>,
71 pub _master: Box<dyn Send>,
73}
74
75impl fmt::Debug for PtyHandles {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.debug_struct("PtyHandles").finish()
78 }
79}
80
81pub struct ProcessHandle {
89 writer_tx: mpsc::Sender<Vec<u8>>,
90 output_tx: broadcast::Sender<Bytes>,
91 killer: StdMutex<Option<Box<dyn ChildTerminator>>>,
92 reader_handle: StdMutex<Option<JoinHandle<()>>>,
93 reader_abort_handles: StdMutex<Vec<AbortHandle>>,
94 writer_handle: StdMutex<Option<JoinHandle<()>>>,
95 wait_handle: StdMutex<Option<JoinHandle<()>>>,
96 exit_status: Arc<AtomicBool>,
97 exit_code: Arc<StdMutex<Option<i32>>>,
98 _pty_handles: StdMutex<Option<PtyHandles>>,
100}
101
102impl fmt::Debug for ProcessHandle {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.debug_struct("ProcessHandle")
105 .field("has_exited", &self.has_exited())
106 .field("exit_code", &self.exit_code())
107 .finish()
108 }
109}
110
111impl ProcessHandle {
112 #[allow(clippy::too_many_arguments)]
114 pub(crate) fn new(
115 writer_tx: mpsc::Sender<Vec<u8>>,
116 output_tx: broadcast::Sender<Bytes>,
117 initial_output_rx: broadcast::Receiver<Bytes>,
118 killer: Box<dyn ChildTerminator>,
119 reader_handle: JoinHandle<()>,
120 reader_abort_handles: Vec<AbortHandle>,
121 writer_handle: JoinHandle<()>,
122 wait_handle: JoinHandle<()>,
123 exit_status: Arc<AtomicBool>,
124 exit_code: Arc<StdMutex<Option<i32>>>,
125 pty_handles: Option<PtyHandles>,
126 ) -> (Self, broadcast::Receiver<Bytes>) {
127 (
128 Self {
129 writer_tx,
130 output_tx,
131 killer: StdMutex::new(Some(killer)),
132 reader_handle: StdMutex::new(Some(reader_handle)),
133 reader_abort_handles: StdMutex::new(reader_abort_handles),
134 writer_handle: StdMutex::new(Some(writer_handle)),
135 wait_handle: StdMutex::new(Some(wait_handle)),
136 exit_status,
137 exit_code,
138 _pty_handles: StdMutex::new(pty_handles),
139 },
140 initial_output_rx,
141 )
142 }
143
144 #[inline]
152 pub fn writer_sender(&self) -> mpsc::Sender<Vec<u8>> {
153 self.writer_tx.clone()
154 }
155
156 #[inline]
161 pub fn output_receiver(&self) -> broadcast::Receiver<Bytes> {
162 self.output_tx.subscribe()
163 }
164
165 #[inline]
167 pub fn has_exited(&self) -> bool {
168 self.exit_status.load(Ordering::SeqCst)
169 }
170
171 #[inline]
173 pub fn exit_code(&self) -> Option<i32> {
174 *self.exit_code.lock().unwrap_or_else(|e| e.into_inner())
175 }
176
177 #[inline]
179 pub fn is_output_drained(&self) -> bool {
180 self.reader_handle
181 .lock()
182 .ok()
183 .and_then(|guard| guard.as_ref().map(JoinHandle::is_finished))
184 .unwrap_or(true)
185 }
186
187 pub fn terminate(&self) {
191 self.terminate_internal();
192 }
193
194 fn terminate_internal(&self) {
196 if let Ok(mut killer_opt) = self.killer.lock()
198 && let Some(mut killer) = killer_opt.take()
199 {
200 let _ = killer.kill();
201 }
202
203 self.abort_tasks();
204 }
205
206 fn abort_tasks(&self) {
208 if let Ok(mut h) = self.reader_handle.lock()
210 && let Some(handle) = h.take()
211 {
212 handle.abort();
213 }
214
215 if let Ok(mut handles) = self.reader_abort_handles.lock() {
217 for handle in handles.drain(..) {
218 handle.abort();
219 }
220 }
221
222 if let Ok(mut h) = self.writer_handle.lock()
224 && let Some(handle) = h.take()
225 {
226 handle.abort();
227 }
228
229 if let Ok(mut h) = self.wait_handle.lock()
231 && let Some(handle) = h.take()
232 {
233 handle.abort();
234 }
235 }
236
237 #[inline]
239 pub fn is_running(&self) -> bool {
240 !self.has_exited() && !self.is_writer_closed()
241 }
242
243 pub async fn write(&self, bytes: impl Into<Vec<u8>>) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
247 self.writer_tx.send(bytes.into()).await
248 }
249
250 #[inline]
252 pub fn is_writer_closed(&self) -> bool {
253 self.writer_tx.is_closed()
254 }
255}
256
257impl Drop for ProcessHandle {
258 fn drop(&mut self) {
259 let killer = self.killer.lock().ok().and_then(|mut g| g.take());
267 let mut reader_handle = self.reader_handle.lock().ok().and_then(|mut g| g.take());
268 let reader_abort_handles = self
269 .reader_abort_handles
270 .lock()
271 .ok()
272 .map(|mut g| g.drain(..).collect::<Vec<_>>());
273 let mut writer_handle = self.writer_handle.lock().ok().and_then(|mut g| g.take());
274 let mut wait_handle = self.wait_handle.lock().ok().and_then(|mut g| g.take());
275
276 async_drop(move || async move {
277 if let Some(mut killer) = killer {
278 let _ = killer.kill();
279 }
280 if let Some(handle) = reader_handle.take() {
281 handle.abort();
282 }
283 if let Some(handle) = writer_handle.take() {
284 handle.abort();
285 }
286 if let Some(handle) = wait_handle.take() {
287 handle.abort();
288 }
289 if let Some(handles) = reader_abort_handles {
290 for handle in handles {
291 handle.abort();
292 }
293 }
294 });
295 }
296}
297
298#[derive(Debug)]
302pub struct SpawnedProcess {
303 pub session: ProcessHandle,
305 pub output_rx: broadcast::Receiver<Bytes>,
307 pub exit_rx: oneshot::Receiver<i32>,
309}
310
311impl SpawnedProcess {
312 pub async fn wait_with_output(self, timeout_ms: u64) -> (Vec<u8>, i32) {
316 collect_output_until_exit(self.output_rx, self.exit_rx, timeout_ms).await
317 }
318}
319
320pub async fn collect_output_until_exit(
324 mut output_rx: broadcast::Receiver<Bytes>,
325 exit_rx: oneshot::Receiver<i32>,
326 timeout_ms: u64,
327) -> (Vec<u8>, i32) {
328 let mut collected = Vec::new();
329 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
330 tokio::pin!(exit_rx);
331
332 loop {
333 tokio::select! {
334 res = output_rx.recv() => {
335 if let Ok(chunk) = res {
336 collected.extend_from_slice(&chunk);
337 }
338 }
339 res = &mut exit_rx => {
340 let code = res.unwrap_or(-1);
341 let quiet = tokio::time::Duration::from_millis(50);
343 let max_deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(500);
344
345 while tokio::time::Instant::now() < max_deadline {
346 match tokio::time::timeout(quiet, output_rx.recv()).await {
347 Ok(Ok(chunk)) => collected.extend_from_slice(&chunk),
348 Ok(Err(broadcast::error::RecvError::Lagged(count))) => {
349 eprintln!("[vtcode] output stream lagged ({count} dropped)");
350 continue;
351 }
352 Ok(Err(broadcast::error::RecvError::Closed)) => break,
353 Err(_) => break, }
355 }
356 return (collected, code);
357 }
358 _ = tokio::time::sleep_until(deadline) => {
359 return (collected, -1);
360 }
361 }
362 }
363}
364
365pub type ExecCommandSession = ProcessHandle;
367
368pub type SpawnedPty = SpawnedProcess;
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 struct NoopTerminator;
376 impl ChildTerminator for NoopTerminator {
377 fn kill(&mut self) -> io::Result<()> {
378 Ok(())
379 }
380 }
381
382 #[tokio::test]
383 async fn test_process_handle_debug() {
384 let exit_status = Arc::new(AtomicBool::new(false));
386 let exit_code = Arc::new(StdMutex::new(None));
387
388 let (writer_tx, _) = mpsc::channel(1);
389 let (output_tx, initial_rx) = broadcast::channel(1);
390
391 let (handle, _) = ProcessHandle::new(
392 writer_tx,
393 output_tx,
394 initial_rx,
395 Box::new(NoopTerminator),
396 tokio::spawn(async {}),
397 vec![],
398 tokio::spawn(async {}),
399 tokio::spawn(async {}),
400 exit_status,
401 exit_code,
402 None,
403 );
404
405 let debug_str = format!("{handle:?}");
406 assert!(debug_str.contains("ProcessHandle"));
407 }
408
409 #[tokio::test]
410 async fn test_has_exited() {
411 let exit_status = Arc::new(AtomicBool::new(false));
412 let exit_code = Arc::new(StdMutex::new(None));
413
414 let (writer_tx, _) = mpsc::channel(1);
415 let (output_tx, initial_rx) = broadcast::channel(1);
416
417 let (handle, _) = ProcessHandle::new(
418 writer_tx,
419 output_tx,
420 initial_rx,
421 Box::new(NoopTerminator),
422 tokio::spawn(async {}),
423 vec![],
424 tokio::spawn(async {}),
425 tokio::spawn(async {}),
426 Arc::clone(&exit_status),
427 exit_code,
428 None,
429 );
430
431 assert!(!handle.has_exited());
432 exit_status.store(true, Ordering::SeqCst);
433 assert!(handle.has_exited());
434 }
435}