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
32const POST_EXIT_DRAIN_QUIET_MS: u64 = 50;
33const POST_EXIT_DRAIN_MAX_MS: u64 = 500;
34
35pub(crate) fn async_drop<F, Fut>(f: F)
45where
46 F: FnOnce() -> Fut + Send + 'static,
47 Fut: Future<Output = ()> + Send + 'static,
48{
49 let handle = std::thread::spawn(move || {
50 let rt = match tokio::runtime::Runtime::new() {
51 Ok(rt) => rt,
52 Err(_) => return,
53 };
54 rt.block_on(f());
55 });
56 let _ = handle.join();
57}
58
59pub trait ChildTerminator: Send + Sync {
63 fn kill(&mut self) -> io::Result<()>;
65}
66
67pub struct PtyHandles {
72 pub _slave: Option<Box<dyn Send>>,
74 pub _master: Box<dyn Send>,
76}
77
78impl fmt::Debug for PtyHandles {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.debug_struct("PtyHandles").finish()
81 }
82}
83
84pub struct ProcessHandle {
92 writer_tx: mpsc::Sender<Vec<u8>>,
93 output_tx: broadcast::Sender<Bytes>,
94 killer: StdMutex<Option<Box<dyn ChildTerminator>>>,
95 reader_handle: StdMutex<Option<JoinHandle<()>>>,
96 reader_abort_handles: StdMutex<Vec<AbortHandle>>,
97 writer_handle: StdMutex<Option<JoinHandle<()>>>,
98 wait_handle: StdMutex<Option<JoinHandle<()>>>,
99 exit_status: Arc<AtomicBool>,
100 exit_code: Arc<StdMutex<Option<i32>>>,
101 _pty_handles: StdMutex<Option<PtyHandles>>,
103}
104
105impl fmt::Debug for ProcessHandle {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 f.debug_struct("ProcessHandle")
108 .field("has_exited", &self.has_exited())
109 .field("exit_code", &self.exit_code())
110 .finish()
111 }
112}
113
114impl ProcessHandle {
115 #[allow(clippy::too_many_arguments)]
117 pub(crate) fn new(
118 writer_tx: mpsc::Sender<Vec<u8>>,
119 output_tx: broadcast::Sender<Bytes>,
120 initial_output_rx: broadcast::Receiver<Bytes>,
121 killer: Box<dyn ChildTerminator>,
122 reader_handle: JoinHandle<()>,
123 reader_abort_handles: Vec<AbortHandle>,
124 writer_handle: JoinHandle<()>,
125 wait_handle: JoinHandle<()>,
126 exit_status: Arc<AtomicBool>,
127 exit_code: Arc<StdMutex<Option<i32>>>,
128 pty_handles: Option<PtyHandles>,
129 ) -> (Self, broadcast::Receiver<Bytes>) {
130 (
131 Self {
132 writer_tx,
133 output_tx,
134 killer: StdMutex::new(Some(killer)),
135 reader_handle: StdMutex::new(Some(reader_handle)),
136 reader_abort_handles: StdMutex::new(reader_abort_handles),
137 writer_handle: StdMutex::new(Some(writer_handle)),
138 wait_handle: StdMutex::new(Some(wait_handle)),
139 exit_status,
140 exit_code,
141 _pty_handles: StdMutex::new(pty_handles),
142 },
143 initial_output_rx,
144 )
145 }
146
147 #[inline]
155 pub fn writer_sender(&self) -> mpsc::Sender<Vec<u8>> {
156 self.writer_tx.clone()
157 }
158
159 #[inline]
164 pub fn output_receiver(&self) -> broadcast::Receiver<Bytes> {
165 self.output_tx.subscribe()
166 }
167
168 #[inline]
170 pub fn has_exited(&self) -> bool {
171 self.exit_status.load(Ordering::SeqCst)
172 }
173
174 #[inline]
176 pub fn exit_code(&self) -> Option<i32> {
177 *self.exit_code.lock().unwrap_or_else(|e| e.into_inner())
178 }
179
180 #[inline]
182 pub fn is_output_drained(&self) -> bool {
183 self.reader_handle
184 .lock()
185 .ok()
186 .and_then(|guard| guard.as_ref().map(JoinHandle::is_finished))
187 .unwrap_or(true)
188 }
189
190 pub fn terminate(&self) {
194 self.terminate_internal();
195 }
196
197 fn terminate_internal(&self) {
199 if let Ok(mut killer_opt) = self.killer.lock()
201 && let Some(mut killer) = killer_opt.take()
202 {
203 let _ = killer.kill();
204 }
205
206 self.abort_tasks();
207 }
208
209 fn abort_tasks(&self) {
211 if let Ok(mut h) = self.reader_handle.lock()
213 && let Some(handle) = h.take()
214 {
215 handle.abort();
216 }
217
218 if let Ok(mut handles) = self.reader_abort_handles.lock() {
220 for handle in handles.drain(..) {
221 handle.abort();
222 }
223 }
224
225 if let Ok(mut h) = self.writer_handle.lock()
227 && let Some(handle) = h.take()
228 {
229 handle.abort();
230 }
231
232 if let Ok(mut h) = self.wait_handle.lock()
234 && let Some(handle) = h.take()
235 {
236 handle.abort();
237 }
238 }
239
240 #[inline]
242 pub fn is_running(&self) -> bool {
243 !self.has_exited() && !self.is_writer_closed()
244 }
245
246 pub async fn write(&self, bytes: impl Into<Vec<u8>>) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
250 self.writer_tx.send(bytes.into()).await
251 }
252
253 #[inline]
255 pub fn is_writer_closed(&self) -> bool {
256 self.writer_tx.is_closed()
257 }
258}
259
260impl Drop for ProcessHandle {
261 fn drop(&mut self) {
262 let killer = self.killer.lock().ok().and_then(|mut g| g.take());
270 let mut reader_handle = self.reader_handle.lock().ok().and_then(|mut g| g.take());
271 let reader_abort_handles = self
272 .reader_abort_handles
273 .lock()
274 .ok()
275 .map(|mut g| g.drain(..).collect::<Vec<_>>());
276 let mut writer_handle = self.writer_handle.lock().ok().and_then(|mut g| g.take());
277 let mut wait_handle = self.wait_handle.lock().ok().and_then(|mut g| g.take());
278
279 async_drop(move || async move {
280 if let Some(mut killer) = killer {
281 let _ = killer.kill();
282 }
283 if let Some(handle) = reader_handle.take() {
284 handle.abort();
285 }
286 if let Some(handle) = writer_handle.take() {
287 handle.abort();
288 }
289 if let Some(handle) = wait_handle.take() {
290 handle.abort();
291 }
292 if let Some(handles) = reader_abort_handles {
293 for handle in handles {
294 handle.abort();
295 }
296 }
297 });
298 }
299}
300
301#[derive(Debug)]
305pub struct SpawnedProcess {
306 pub session: ProcessHandle,
308 pub output_rx: broadcast::Receiver<Bytes>,
310 pub reliable_output_rx: mpsc::Receiver<Bytes>,
314 pub(crate) reliable_output_enabled: bool,
316 pub exit_rx: oneshot::Receiver<i32>,
318}
319
320impl SpawnedProcess {
321 pub async fn wait_with_output(self, timeout_ms: u64) -> (Vec<u8>, i32) {
325 if self.reliable_output_enabled {
326 collect_reliable_output_until_exit(self.reliable_output_rx, self.exit_rx, timeout_ms).await
327 } else {
328 collect_output_until_exit(self.output_rx, self.exit_rx, timeout_ms).await
329 }
330 }
331}
332
333async fn collect_reliable_output_until_exit(
335 mut output_rx: mpsc::Receiver<Bytes>,
336 exit_rx: oneshot::Receiver<i32>,
337 timeout_ms: u64,
338) -> (Vec<u8>, i32) {
339 let mut collected = Vec::new();
340 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
341 tokio::pin!(exit_rx);
342
343 loop {
344 tokio::select! {
345 chunk = output_rx.recv() => {
346 if let Some(chunk) = chunk {
347 collected.extend_from_slice(&chunk);
348 } else {
349 return (collected, exit_rx.await.unwrap_or(-1));
350 }
351 }
352 res = &mut exit_rx => {
353 let code = res.unwrap_or(-1);
354 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
359 let max_deadline = tokio::time::Instant::now()
360 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
361 while tokio::time::Instant::now() < max_deadline {
362 match tokio::time::timeout(quiet, output_rx.recv()).await {
363 Ok(Some(chunk)) => collected.extend_from_slice(&chunk),
364 Ok(None) | Err(_) => break,
365 }
366 }
367 return (collected, code);
368 }
369 _ = tokio::time::sleep_until(deadline) => {
370 return (collected, -1);
371 }
372 }
373 }
374}
375
376pub async fn collect_output_until_exit(
380 mut output_rx: broadcast::Receiver<Bytes>,
381 exit_rx: oneshot::Receiver<i32>,
382 timeout_ms: u64,
383) -> (Vec<u8>, i32) {
384 let mut collected = Vec::new();
385 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
386 tokio::pin!(exit_rx);
387
388 loop {
389 tokio::select! {
390 res = output_rx.recv() => {
391 if let Ok(chunk) = res {
392 collected.extend_from_slice(&chunk);
393 }
394 }
395 res = &mut exit_rx => {
396 let code = res.unwrap_or(-1);
397 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
399 let max_deadline = tokio::time::Instant::now()
400 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
401
402 while tokio::time::Instant::now() < max_deadline {
403 match tokio::time::timeout(quiet, output_rx.recv()).await {
404 Ok(Ok(chunk)) => collected.extend_from_slice(&chunk),
405 Ok(Err(broadcast::error::RecvError::Lagged(count))) => {
406 eprintln!("[vtcode] output stream lagged ({count} dropped)");
407 continue;
408 }
409 Ok(Err(broadcast::error::RecvError::Closed)) => break,
410 Err(_) => break, }
412 }
413 return (collected, code);
414 }
415 _ = tokio::time::sleep_until(deadline) => {
416 return (collected, -1);
417 }
418 }
419 }
420}
421
422pub type ExecCommandSession = ProcessHandle;
424
425pub type SpawnedPty = SpawnedProcess;
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 struct NoopTerminator;
433 impl ChildTerminator for NoopTerminator {
434 fn kill(&mut self) -> io::Result<()> {
435 Ok(())
436 }
437 }
438
439 #[tokio::test]
440 async fn test_process_handle_debug() {
441 let exit_status = Arc::new(AtomicBool::new(false));
443 let exit_code = Arc::new(StdMutex::new(None));
444
445 let (writer_tx, _) = mpsc::channel(1);
446 let (output_tx, initial_rx) = broadcast::channel(1);
447
448 let (handle, _) = ProcessHandle::new(
449 writer_tx,
450 output_tx,
451 initial_rx,
452 Box::new(NoopTerminator),
453 tokio::spawn(async {}),
454 vec![],
455 tokio::spawn(async {}),
456 tokio::spawn(async {}),
457 exit_status,
458 exit_code,
459 None,
460 );
461
462 let debug_str = format!("{handle:?}");
463 assert!(debug_str.contains("ProcessHandle"));
464 }
465
466 #[tokio::test]
467 async fn test_has_exited() {
468 let exit_status = Arc::new(AtomicBool::new(false));
469 let exit_code = Arc::new(StdMutex::new(None));
470
471 let (writer_tx, _) = mpsc::channel(1);
472 let (output_tx, initial_rx) = broadcast::channel(1);
473
474 let (handle, _) = ProcessHandle::new(
475 writer_tx,
476 output_tx,
477 initial_rx,
478 Box::new(NoopTerminator),
479 tokio::spawn(async {}),
480 vec![],
481 tokio::spawn(async {}),
482 tokio::spawn(async {}),
483 Arc::clone(&exit_status),
484 exit_code,
485 None,
486 );
487
488 assert!(!handle.has_exited());
489 exit_status.store(true, Ordering::SeqCst);
490 assert!(handle.has_exited());
491 }
492}