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(
117 clippy::too_many_arguments,
118 reason = "Intentional compatibility, platform, or test-only suppression."
119 )]
120 pub(crate) fn new(
121 writer_tx: mpsc::Sender<Vec<u8>>,
122 output_tx: broadcast::Sender<Bytes>,
123 initial_output_rx: broadcast::Receiver<Bytes>,
124 killer: Box<dyn ChildTerminator>,
125 reader_handle: JoinHandle<()>,
126 reader_abort_handles: Vec<AbortHandle>,
127 writer_handle: JoinHandle<()>,
128 wait_handle: JoinHandle<()>,
129 exit_status: Arc<AtomicBool>,
130 exit_code: Arc<StdMutex<Option<i32>>>,
131 pty_handles: Option<PtyHandles>,
132 ) -> (Self, broadcast::Receiver<Bytes>) {
133 (
134 Self {
135 writer_tx,
136 output_tx,
137 killer: StdMutex::new(Some(killer)),
138 reader_handle: StdMutex::new(Some(reader_handle)),
139 reader_abort_handles: StdMutex::new(reader_abort_handles),
140 writer_handle: StdMutex::new(Some(writer_handle)),
141 wait_handle: StdMutex::new(Some(wait_handle)),
142 exit_status,
143 exit_code,
144 _pty_handles: StdMutex::new(pty_handles),
145 },
146 initial_output_rx,
147 )
148 }
149
150 #[inline]
158 pub fn writer_sender(&self) -> mpsc::Sender<Vec<u8>> {
159 self.writer_tx.clone()
160 }
161
162 #[inline]
167 pub fn output_receiver(&self) -> broadcast::Receiver<Bytes> {
168 self.output_tx.subscribe()
169 }
170
171 #[inline]
173 pub fn has_exited(&self) -> bool {
174 self.exit_status.load(Ordering::SeqCst)
175 }
176
177 #[inline]
179 pub fn exit_code(&self) -> Option<i32> {
180 *self.exit_code.lock().unwrap_or_else(|e| e.into_inner())
181 }
182
183 #[inline]
185 pub fn is_output_drained(&self) -> bool {
186 self.reader_handle
187 .lock()
188 .ok()
189 .and_then(|guard| guard.as_ref().map(JoinHandle::is_finished))
190 .unwrap_or(true)
191 }
192
193 pub fn terminate(&self) {
197 self.terminate_internal();
198 }
199
200 fn terminate_internal(&self) {
202 if let Ok(mut killer_opt) = self.killer.lock()
204 && let Some(mut killer) = killer_opt.take()
205 {
206 let _ = killer.kill();
207 }
208
209 self.abort_tasks();
210 }
211
212 fn abort_tasks(&self) {
214 if let Ok(mut h) = self.reader_handle.lock()
216 && let Some(handle) = h.take()
217 {
218 handle.abort();
219 }
220
221 if let Ok(mut handles) = self.reader_abort_handles.lock() {
223 for handle in handles.drain(..) {
224 handle.abort();
225 }
226 }
227
228 if let Ok(mut h) = self.writer_handle.lock()
230 && let Some(handle) = h.take()
231 {
232 handle.abort();
233 }
234
235 if let Ok(mut h) = self.wait_handle.lock()
237 && let Some(handle) = h.take()
238 {
239 handle.abort();
240 }
241 }
242
243 #[inline]
245 pub fn is_running(&self) -> bool {
246 !self.has_exited() && !self.is_writer_closed()
247 }
248
249 pub async fn write(&self, bytes: impl Into<Vec<u8>>) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
253 self.writer_tx.send(bytes.into()).await
254 }
255
256 #[inline]
258 pub fn is_writer_closed(&self) -> bool {
259 self.writer_tx.is_closed()
260 }
261}
262
263impl Drop for ProcessHandle {
264 fn drop(&mut self) {
265 let killer = self.killer.lock().ok().and_then(|mut g| g.take());
273 let mut reader_handle = self.reader_handle.lock().ok().and_then(|mut g| g.take());
274 let reader_abort_handles = self
275 .reader_abort_handles
276 .lock()
277 .ok()
278 .map(|mut g| g.drain(..).collect::<Vec<_>>());
279 let mut writer_handle = self.writer_handle.lock().ok().and_then(|mut g| g.take());
280 let mut wait_handle = self.wait_handle.lock().ok().and_then(|mut g| g.take());
281
282 async_drop(move || async move {
283 if let Some(mut killer) = killer {
284 let _ = killer.kill();
285 }
286 if let Some(handle) = reader_handle.take() {
287 handle.abort();
288 }
289 if let Some(handle) = writer_handle.take() {
290 handle.abort();
291 }
292 if let Some(handle) = wait_handle.take() {
293 handle.abort();
294 }
295 if let Some(handles) = reader_abort_handles {
296 for handle in handles {
297 handle.abort();
298 }
299 }
300 });
301 }
302}
303
304#[derive(Debug)]
308pub struct SpawnedProcess {
309 pub session: ProcessHandle,
311 pub output_rx: broadcast::Receiver<Bytes>,
313 pub reliable_output_rx: mpsc::Receiver<Bytes>,
317 pub(crate) reliable_output_enabled: bool,
319 pub exit_rx: oneshot::Receiver<i32>,
321}
322
323impl SpawnedProcess {
324 pub async fn wait_with_output(self, timeout_ms: u64) -> (Vec<u8>, i32) {
328 if self.reliable_output_enabled {
329 collect_reliable_output_until_exit(self.reliable_output_rx, self.exit_rx, timeout_ms).await
330 } else {
331 collect_output_until_exit(self.output_rx, self.exit_rx, timeout_ms).await
332 }
333 }
334}
335
336async fn collect_reliable_output_until_exit(
338 mut output_rx: mpsc::Receiver<Bytes>,
339 exit_rx: oneshot::Receiver<i32>,
340 timeout_ms: u64,
341) -> (Vec<u8>, i32) {
342 let mut collected = Vec::new();
343 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
344 tokio::pin!(exit_rx);
345
346 loop {
347 tokio::select! {
348 chunk = output_rx.recv() => {
349 if let Some(chunk) = chunk {
350 collected.extend_from_slice(&chunk);
351 } else {
352 return (collected, exit_rx.await.unwrap_or(-1));
353 }
354 }
355 res = &mut exit_rx => {
356 let code = res.unwrap_or(-1);
357 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
362 let max_deadline = tokio::time::Instant::now()
363 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
364 while tokio::time::Instant::now() < max_deadline {
365 match tokio::time::timeout(quiet, output_rx.recv()).await {
366 Ok(Some(chunk)) => collected.extend_from_slice(&chunk),
367 Ok(None) | Err(_) => break,
368 }
369 }
370 return (collected, code);
371 }
372 _ = tokio::time::sleep_until(deadline) => {
373 return (collected, -1);
374 }
375 }
376 }
377}
378
379pub async fn collect_output_until_exit(
383 mut output_rx: broadcast::Receiver<Bytes>,
384 exit_rx: oneshot::Receiver<i32>,
385 timeout_ms: u64,
386) -> (Vec<u8>, i32) {
387 let mut collected = Vec::new();
388 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
389 tokio::pin!(exit_rx);
390
391 loop {
392 tokio::select! {
393 res = output_rx.recv() => {
394 if let Ok(chunk) = res {
395 collected.extend_from_slice(&chunk);
396 }
397 }
398 res = &mut exit_rx => {
399 let code = res.unwrap_or(-1);
400 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
402 let max_deadline = tokio::time::Instant::now()
403 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
404
405 while tokio::time::Instant::now() < max_deadline {
406 match tokio::time::timeout(quiet, output_rx.recv()).await {
407 Ok(Ok(chunk)) => collected.extend_from_slice(&chunk),
408 Ok(Err(broadcast::error::RecvError::Lagged(count))) => {
409 eprintln!("[vtcode] output stream lagged ({count} dropped)");
410 continue;
411 }
412 Ok(Err(broadcast::error::RecvError::Closed)) => break,
413 Err(_) => break, }
415 }
416 return (collected, code);
417 }
418 _ = tokio::time::sleep_until(deadline) => {
419 return (collected, -1);
420 }
421 }
422 }
423}
424
425pub type ExecCommandSession = ProcessHandle;
427
428pub type SpawnedPty = SpawnedProcess;
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 struct NoopTerminator;
436 impl ChildTerminator for NoopTerminator {
437 fn kill(&mut self) -> io::Result<()> {
438 Ok(())
439 }
440 }
441
442 #[tokio::test]
443 async fn test_process_handle_debug() {
444 let exit_status = Arc::new(AtomicBool::new(false));
446 let exit_code = Arc::new(StdMutex::new(None));
447
448 let (writer_tx, _) = mpsc::channel(1);
449 let (output_tx, initial_rx) = broadcast::channel(1);
450
451 let (handle, _) = ProcessHandle::new(
452 writer_tx,
453 output_tx,
454 initial_rx,
455 Box::new(NoopTerminator),
456 tokio::spawn(async {}),
457 vec![],
458 tokio::spawn(async {}),
459 tokio::spawn(async {}),
460 exit_status,
461 exit_code,
462 None,
463 );
464
465 let debug_str = format!("{handle:?}");
466 assert!(debug_str.contains("ProcessHandle"));
467 }
468
469 #[tokio::test]
470 async fn test_has_exited() {
471 let exit_status = Arc::new(AtomicBool::new(false));
472 let exit_code = Arc::new(StdMutex::new(None));
473
474 let (writer_tx, _) = mpsc::channel(1);
475 let (output_tx, initial_rx) = broadcast::channel(1);
476
477 let (handle, _) = ProcessHandle::new(
478 writer_tx,
479 output_tx,
480 initial_rx,
481 Box::new(NoopTerminator),
482 tokio::spawn(async {}),
483 vec![],
484 tokio::spawn(async {}),
485 tokio::spawn(async {}),
486 Arc::clone(&exit_status),
487 exit_code,
488 None,
489 );
490
491 assert!(!handle.has_exited());
492 exit_status.store(true, Ordering::SeqCst);
493 assert!(handle.has_exited());
494 }
495}