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 trait PtyHandle: Send {}
87
88impl<T: Send> PtyHandle for T {}
89
90pub struct PtyHandles {
95 pub _slave: Option<Box<dyn PtyHandle>>,
97 pub _master: Box<dyn PtyHandle>,
99}
100
101impl fmt::Debug for PtyHandles {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.debug_struct("PtyHandles").finish()
104 }
105}
106
107pub struct ProcessHandle {
115 writer_tx: mpsc::Sender<Vec<u8>>,
116 output_tx: broadcast::Sender<Bytes>,
117 killer: StdMutex<Option<Box<dyn ChildTerminator>>>,
118 reader_handle: StdMutex<Option<JoinHandle<()>>>,
119 reader_abort_handles: StdMutex<Vec<AbortHandle>>,
120 writer_handle: StdMutex<Option<JoinHandle<()>>>,
121 wait_handle: StdMutex<Option<JoinHandle<()>>>,
122 exit_status: Arc<AtomicBool>,
123 exit_code: Arc<StdMutex<Option<i32>>>,
124 _pty_handles: StdMutex<Option<PtyHandles>>,
126}
127
128impl fmt::Debug for ProcessHandle {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 f.debug_struct("ProcessHandle")
131 .field("has_exited", &self.has_exited())
132 .field("exit_code", &self.exit_code())
133 .finish()
134 }
135}
136
137impl ProcessHandle {
138 #[allow(
140 clippy::too_many_arguments,
141 reason = "Intentional compatibility, platform, or test-only suppression."
142 )]
143 pub(crate) fn new(
144 writer_tx: mpsc::Sender<Vec<u8>>,
145 output_tx: broadcast::Sender<Bytes>,
146 initial_output_rx: broadcast::Receiver<Bytes>,
147 killer: Box<dyn ChildTerminator>,
148 reader_handle: JoinHandle<()>,
149 reader_abort_handles: Vec<AbortHandle>,
150 writer_handle: JoinHandle<()>,
151 wait_handle: JoinHandle<()>,
152 exit_status: Arc<AtomicBool>,
153 exit_code: Arc<StdMutex<Option<i32>>>,
154 pty_handles: Option<PtyHandles>,
155 ) -> (Self, broadcast::Receiver<Bytes>) {
156 (
157 Self {
158 writer_tx,
159 output_tx,
160 killer: StdMutex::new(Some(killer)),
161 reader_handle: StdMutex::new(Some(reader_handle)),
162 reader_abort_handles: StdMutex::new(reader_abort_handles),
163 writer_handle: StdMutex::new(Some(writer_handle)),
164 wait_handle: StdMutex::new(Some(wait_handle)),
165 exit_status,
166 exit_code,
167 _pty_handles: StdMutex::new(pty_handles),
168 },
169 initial_output_rx,
170 )
171 }
172
173 #[inline]
181 pub fn writer_sender(&self) -> mpsc::Sender<Vec<u8>> {
182 self.writer_tx.clone()
183 }
184
185 #[inline]
190 pub fn output_receiver(&self) -> broadcast::Receiver<Bytes> {
191 self.output_tx.subscribe()
192 }
193
194 #[inline]
196 pub fn has_exited(&self) -> bool {
197 self.exit_status.load(Ordering::SeqCst)
198 }
199
200 #[inline]
202 pub fn exit_code(&self) -> Option<i32> {
203 *self.exit_code.lock().unwrap_or_else(|e| e.into_inner())
204 }
205
206 #[inline]
208 pub fn is_output_drained(&self) -> bool {
209 self.reader_handle
210 .lock()
211 .ok()
212 .and_then(|guard| guard.as_ref().map(JoinHandle::is_finished))
213 .unwrap_or(true)
214 }
215
216 pub fn terminate(&self) {
220 self.terminate_internal();
221 }
222
223 pub fn terminate_process(&self) {
229 if let Ok(mut killer_opt) = self.killer.lock()
230 && let Some(mut killer) = killer_opt.take()
231 {
232 let _ = killer.kill();
233 }
234 }
235
236 fn terminate_internal(&self) {
238 if let Ok(mut killer_opt) = self.killer.lock()
240 && let Some(mut killer) = killer_opt.take()
241 {
242 let _ = killer.kill();
243 }
244
245 self.abort_tasks();
246 }
247
248 fn abort_tasks(&self) {
250 if let Ok(mut h) = self.reader_handle.lock()
252 && let Some(handle) = h.take()
253 {
254 handle.abort();
255 }
256
257 if let Ok(mut handles) = self.reader_abort_handles.lock() {
259 for handle in handles.drain(..) {
260 handle.abort();
261 }
262 }
263
264 if let Ok(mut h) = self.writer_handle.lock()
266 && let Some(handle) = h.take()
267 {
268 handle.abort();
269 }
270
271 if let Ok(mut h) = self.wait_handle.lock()
273 && let Some(handle) = h.take()
274 {
275 handle.abort();
276 }
277 }
278
279 #[inline]
281 pub fn is_running(&self) -> bool {
282 !self.has_exited() && !self.is_writer_closed()
283 }
284
285 pub async fn write(&self, bytes: impl Into<Vec<u8>>) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
289 self.writer_tx.send(bytes.into()).await
290 }
291
292 #[inline]
294 pub fn is_writer_closed(&self) -> bool {
295 self.writer_tx.is_closed()
296 }
297}
298
299impl Drop for ProcessHandle {
300 fn drop(&mut self) {
301 let killer = self.killer.lock().ok().and_then(|mut g| g.take());
309 let mut reader_handle = self.reader_handle.lock().ok().and_then(|mut g| g.take());
310 let reader_abort_handles = self
311 .reader_abort_handles
312 .lock()
313 .ok()
314 .map(|mut g| g.drain(..).collect::<Vec<_>>());
315 let mut writer_handle = self.writer_handle.lock().ok().and_then(|mut g| g.take());
316 let mut wait_handle = self.wait_handle.lock().ok().and_then(|mut g| g.take());
317
318 async_drop(move || async move {
319 if let Some(mut killer) = killer {
320 let _ = killer.kill();
321 }
322 if let Some(handle) = reader_handle.take() {
323 handle.abort();
324 }
325 if let Some(handle) = writer_handle.take() {
326 handle.abort();
327 }
328 if let Some(handle) = wait_handle.take() {
329 handle.abort();
330 }
331 if let Some(handles) = reader_abort_handles {
332 for handle in handles {
333 handle.abort();
334 }
335 }
336 });
337 }
338}
339
340#[derive(Debug)]
344pub struct SpawnedProcess {
345 pub session: ProcessHandle,
347 pub process_id: u32,
349 pub output_rx: broadcast::Receiver<Bytes>,
351 pub reliable_output_rx: mpsc::Receiver<Bytes>,
355 pub(crate) reliable_output_enabled: bool,
357 pub exit_rx: oneshot::Receiver<i32>,
359}
360
361impl SpawnedProcess {
362 pub async fn wait_with_output(self, timeout_ms: u64) -> (Vec<u8>, i32) {
366 if self.reliable_output_enabled {
367 collect_reliable_output_until_exit(self.reliable_output_rx, self.exit_rx, timeout_ms).await
368 } else {
369 collect_output_until_exit(self.output_rx, self.exit_rx, timeout_ms).await
370 }
371 }
372}
373
374async fn collect_reliable_output_until_exit(
376 mut output_rx: mpsc::Receiver<Bytes>,
377 exit_rx: oneshot::Receiver<i32>,
378 timeout_ms: u64,
379) -> (Vec<u8>, i32) {
380 let mut collected = Vec::new();
381 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
382 tokio::pin!(exit_rx);
383
384 loop {
385 tokio::select! {
386 chunk = output_rx.recv() => {
387 if let Some(chunk) = chunk {
388 collected.extend_from_slice(&chunk);
389 } else {
390 return (collected, exit_rx.await.unwrap_or(-1));
391 }
392 }
393 res = &mut exit_rx => {
394 let code = res.unwrap_or(-1);
395 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
400 let max_deadline = tokio::time::Instant::now()
401 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
402 while tokio::time::Instant::now() < max_deadline {
403 match tokio::time::timeout(quiet, output_rx.recv()).await {
404 Ok(Some(chunk)) => collected.extend_from_slice(&chunk),
405 Ok(None) | Err(_) => break,
406 }
407 }
408 return (collected, code);
409 }
410 _ = tokio::time::sleep_until(deadline) => {
411 return (collected, -1);
412 }
413 }
414 }
415}
416
417pub async fn collect_output_until_exit(
421 mut output_rx: broadcast::Receiver<Bytes>,
422 exit_rx: oneshot::Receiver<i32>,
423 timeout_ms: u64,
424) -> (Vec<u8>, i32) {
425 let mut collected = Vec::new();
426 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
427 tokio::pin!(exit_rx);
428
429 loop {
430 tokio::select! {
431 res = output_rx.recv() => {
432 if let Ok(chunk) = res {
433 collected.extend_from_slice(&chunk);
434 }
435 }
436 res = &mut exit_rx => {
437 let code = res.unwrap_or(-1);
438 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
440 let max_deadline = tokio::time::Instant::now()
441 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
442
443 while tokio::time::Instant::now() < max_deadline {
444 match tokio::time::timeout(quiet, output_rx.recv()).await {
445 Ok(Ok(chunk)) => collected.extend_from_slice(&chunk),
446 Ok(Err(broadcast::error::RecvError::Lagged(count))) => {
447 eprintln!("[vtcode] output stream lagged ({count} dropped)");
448 continue;
449 }
450 Ok(Err(broadcast::error::RecvError::Closed)) => break,
451 Err(_) => break, }
453 }
454 return (collected, code);
455 }
456 _ = tokio::time::sleep_until(deadline) => {
457 return (collected, -1);
458 }
459 }
460 }
461}
462
463pub type ExecCommandSession = ProcessHandle;
465
466pub type SpawnedPty = SpawnedProcess;
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 struct NoopTerminator;
474 impl ChildTerminator for NoopTerminator {
475 fn kill(&mut self) -> io::Result<()> {
476 Ok(())
477 }
478 }
479
480 #[tokio::test]
481 async fn test_process_handle_debug() {
482 let exit_status = Arc::new(AtomicBool::new(false));
484 let exit_code = Arc::new(StdMutex::new(None));
485
486 let (writer_tx, _) = mpsc::channel(1);
487 let (output_tx, initial_rx) = broadcast::channel(1);
488
489 let (handle, _) = ProcessHandle::new(
490 writer_tx,
491 output_tx,
492 initial_rx,
493 Box::new(NoopTerminator),
494 tokio::spawn(async {}),
495 vec![],
496 tokio::spawn(async {}),
497 tokio::spawn(async {}),
498 exit_status,
499 exit_code,
500 None,
501 );
502
503 let debug_str = format!("{handle:?}");
504 assert!(debug_str.contains("ProcessHandle"));
505 }
506
507 #[tokio::test]
508 async fn test_has_exited() {
509 let exit_status = Arc::new(AtomicBool::new(false));
510 let exit_code = Arc::new(StdMutex::new(None));
511
512 let (writer_tx, _) = mpsc::channel(1);
513 let (output_tx, initial_rx) = broadcast::channel(1);
514
515 let (handle, _) = ProcessHandle::new(
516 writer_tx,
517 output_tx,
518 initial_rx,
519 Box::new(NoopTerminator),
520 tokio::spawn(async {}),
521 vec![],
522 tokio::spawn(async {}),
523 tokio::spawn(async {}),
524 Arc::clone(&exit_status),
525 exit_code,
526 None,
527 );
528
529 assert!(!handle.has_exited());
530 exit_status.store(true, Ordering::SeqCst);
531 assert!(handle.has_exited());
532 }
533}