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 fn terminate_internal(&self) {
225 if let Ok(mut killer_opt) = self.killer.lock()
227 && let Some(mut killer) = killer_opt.take()
228 {
229 let _ = killer.kill();
230 }
231
232 self.abort_tasks();
233 }
234
235 fn abort_tasks(&self) {
237 if let Ok(mut h) = self.reader_handle.lock()
239 && let Some(handle) = h.take()
240 {
241 handle.abort();
242 }
243
244 if let Ok(mut handles) = self.reader_abort_handles.lock() {
246 for handle in handles.drain(..) {
247 handle.abort();
248 }
249 }
250
251 if let Ok(mut h) = self.writer_handle.lock()
253 && let Some(handle) = h.take()
254 {
255 handle.abort();
256 }
257
258 if let Ok(mut h) = self.wait_handle.lock()
260 && let Some(handle) = h.take()
261 {
262 handle.abort();
263 }
264 }
265
266 #[inline]
268 pub fn is_running(&self) -> bool {
269 !self.has_exited() && !self.is_writer_closed()
270 }
271
272 pub async fn write(&self, bytes: impl Into<Vec<u8>>) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
276 self.writer_tx.send(bytes.into()).await
277 }
278
279 #[inline]
281 pub fn is_writer_closed(&self) -> bool {
282 self.writer_tx.is_closed()
283 }
284}
285
286impl Drop for ProcessHandle {
287 fn drop(&mut self) {
288 let killer = self.killer.lock().ok().and_then(|mut g| g.take());
296 let mut reader_handle = self.reader_handle.lock().ok().and_then(|mut g| g.take());
297 let reader_abort_handles = self
298 .reader_abort_handles
299 .lock()
300 .ok()
301 .map(|mut g| g.drain(..).collect::<Vec<_>>());
302 let mut writer_handle = self.writer_handle.lock().ok().and_then(|mut g| g.take());
303 let mut wait_handle = self.wait_handle.lock().ok().and_then(|mut g| g.take());
304
305 async_drop(move || async move {
306 if let Some(mut killer) = killer {
307 let _ = killer.kill();
308 }
309 if let Some(handle) = reader_handle.take() {
310 handle.abort();
311 }
312 if let Some(handle) = writer_handle.take() {
313 handle.abort();
314 }
315 if let Some(handle) = wait_handle.take() {
316 handle.abort();
317 }
318 if let Some(handles) = reader_abort_handles {
319 for handle in handles {
320 handle.abort();
321 }
322 }
323 });
324 }
325}
326
327#[derive(Debug)]
331pub struct SpawnedProcess {
332 pub session: ProcessHandle,
334 pub output_rx: broadcast::Receiver<Bytes>,
336 pub reliable_output_rx: mpsc::Receiver<Bytes>,
340 pub(crate) reliable_output_enabled: bool,
342 pub exit_rx: oneshot::Receiver<i32>,
344}
345
346impl SpawnedProcess {
347 pub async fn wait_with_output(self, timeout_ms: u64) -> (Vec<u8>, i32) {
351 if self.reliable_output_enabled {
352 collect_reliable_output_until_exit(self.reliable_output_rx, self.exit_rx, timeout_ms).await
353 } else {
354 collect_output_until_exit(self.output_rx, self.exit_rx, timeout_ms).await
355 }
356 }
357}
358
359async fn collect_reliable_output_until_exit(
361 mut output_rx: mpsc::Receiver<Bytes>,
362 exit_rx: oneshot::Receiver<i32>,
363 timeout_ms: u64,
364) -> (Vec<u8>, i32) {
365 let mut collected = Vec::new();
366 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
367 tokio::pin!(exit_rx);
368
369 loop {
370 tokio::select! {
371 chunk = output_rx.recv() => {
372 if let Some(chunk) = chunk {
373 collected.extend_from_slice(&chunk);
374 } else {
375 return (collected, exit_rx.await.unwrap_or(-1));
376 }
377 }
378 res = &mut exit_rx => {
379 let code = res.unwrap_or(-1);
380 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
385 let max_deadline = tokio::time::Instant::now()
386 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
387 while tokio::time::Instant::now() < max_deadline {
388 match tokio::time::timeout(quiet, output_rx.recv()).await {
389 Ok(Some(chunk)) => collected.extend_from_slice(&chunk),
390 Ok(None) | Err(_) => break,
391 }
392 }
393 return (collected, code);
394 }
395 _ = tokio::time::sleep_until(deadline) => {
396 return (collected, -1);
397 }
398 }
399 }
400}
401
402pub async fn collect_output_until_exit(
406 mut output_rx: broadcast::Receiver<Bytes>,
407 exit_rx: oneshot::Receiver<i32>,
408 timeout_ms: u64,
409) -> (Vec<u8>, i32) {
410 let mut collected = Vec::new();
411 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
412 tokio::pin!(exit_rx);
413
414 loop {
415 tokio::select! {
416 res = output_rx.recv() => {
417 if let Ok(chunk) = res {
418 collected.extend_from_slice(&chunk);
419 }
420 }
421 res = &mut exit_rx => {
422 let code = res.unwrap_or(-1);
423 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
425 let max_deadline = tokio::time::Instant::now()
426 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
427
428 while tokio::time::Instant::now() < max_deadline {
429 match tokio::time::timeout(quiet, output_rx.recv()).await {
430 Ok(Ok(chunk)) => collected.extend_from_slice(&chunk),
431 Ok(Err(broadcast::error::RecvError::Lagged(count))) => {
432 eprintln!("[vtcode] output stream lagged ({count} dropped)");
433 continue;
434 }
435 Ok(Err(broadcast::error::RecvError::Closed)) => break,
436 Err(_) => break, }
438 }
439 return (collected, code);
440 }
441 _ = tokio::time::sleep_until(deadline) => {
442 return (collected, -1);
443 }
444 }
445 }
446}
447
448pub type ExecCommandSession = ProcessHandle;
450
451pub type SpawnedPty = SpawnedProcess;
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 struct NoopTerminator;
459 impl ChildTerminator for NoopTerminator {
460 fn kill(&mut self) -> io::Result<()> {
461 Ok(())
462 }
463 }
464
465 #[tokio::test]
466 async fn test_process_handle_debug() {
467 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 exit_status,
484 exit_code,
485 None,
486 );
487
488 let debug_str = format!("{handle:?}");
489 assert!(debug_str.contains("ProcessHandle"));
490 }
491
492 #[tokio::test]
493 async fn test_has_exited() {
494 let exit_status = Arc::new(AtomicBool::new(false));
495 let exit_code = Arc::new(StdMutex::new(None));
496
497 let (writer_tx, _) = mpsc::channel(1);
498 let (output_tx, initial_rx) = broadcast::channel(1);
499
500 let (handle, _) = ProcessHandle::new(
501 writer_tx,
502 output_tx,
503 initial_rx,
504 Box::new(NoopTerminator),
505 tokio::spawn(async {}),
506 vec![],
507 tokio::spawn(async {}),
508 tokio::spawn(async {}),
509 Arc::clone(&exit_status),
510 exit_code,
511 None,
512 );
513
514 assert!(!handle.has_exited());
515 exit_status.store(true, Ordering::SeqCst);
516 assert!(handle.has_exited());
517 }
518}