1use std::fmt;
13use std::io;
14use std::sync::Arc;
15use std::sync::Mutex as StdMutex;
16use std::sync::atomic::{AtomicBool, Ordering};
17
18use bytes::Bytes;
19use tokio::sync::{broadcast, mpsc, oneshot};
20use tokio::task::{AbortHandle, JoinHandle};
21
22const POST_EXIT_DRAIN_QUIET_MS: u64 = 50;
23const POST_EXIT_DRAIN_MAX_MS: u64 = 500;
24
25pub trait ChildTerminator: Send + Sync {
29 fn kill(&mut self) -> io::Result<()>;
31}
32
33pub trait PtyHandle: Send {}
53
54impl<T: Send> PtyHandle for T {}
55
56pub struct PtyHandles {
61 pub _slave: Option<Box<dyn PtyHandle>>,
63 pub _master: Box<dyn PtyHandle>,
65}
66
67impl fmt::Debug for PtyHandles {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.debug_struct("PtyHandles").finish()
70 }
71}
72
73pub struct ProcessHandle {
81 writer_tx: mpsc::Sender<Vec<u8>>,
82 output_tx: broadcast::Sender<Bytes>,
83 killer: StdMutex<Option<Box<dyn ChildTerminator>>>,
84 reader_handle: StdMutex<Option<JoinHandle<()>>>,
85 reader_abort_handles: StdMutex<Vec<AbortHandle>>,
86 writer_handle: StdMutex<Option<JoinHandle<()>>>,
87 wait_handle: StdMutex<Option<JoinHandle<()>>>,
88 exit_status: Arc<AtomicBool>,
89 exit_code: Arc<StdMutex<Option<i32>>>,
90 _pty_handles: StdMutex<Option<PtyHandles>>,
92}
93
94impl fmt::Debug for ProcessHandle {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.debug_struct("ProcessHandle")
97 .field("has_exited", &self.has_exited())
98 .field("exit_code", &self.exit_code())
99 .finish()
100 }
101}
102
103impl ProcessHandle {
104 #[allow(
106 clippy::too_many_arguments,
107 reason = "Intentional compatibility, platform, or test-only suppression."
108 )]
109 pub(crate) fn new(
110 writer_tx: mpsc::Sender<Vec<u8>>,
111 output_tx: broadcast::Sender<Bytes>,
112 initial_output_rx: broadcast::Receiver<Bytes>,
113 killer: Box<dyn ChildTerminator>,
114 reader_handle: JoinHandle<()>,
115 reader_abort_handles: Vec<AbortHandle>,
116 writer_handle: JoinHandle<()>,
117 wait_handle: JoinHandle<()>,
118 exit_status: Arc<AtomicBool>,
119 exit_code: Arc<StdMutex<Option<i32>>>,
120 pty_handles: Option<PtyHandles>,
121 ) -> (Self, broadcast::Receiver<Bytes>) {
122 (
123 Self {
124 writer_tx,
125 output_tx,
126 killer: StdMutex::new(Some(killer)),
127 reader_handle: StdMutex::new(Some(reader_handle)),
128 reader_abort_handles: StdMutex::new(reader_abort_handles),
129 writer_handle: StdMutex::new(Some(writer_handle)),
130 wait_handle: StdMutex::new(Some(wait_handle)),
131 exit_status,
132 exit_code,
133 _pty_handles: StdMutex::new(pty_handles),
134 },
135 initial_output_rx,
136 )
137 }
138
139 #[inline]
147 pub fn writer_sender(&self) -> mpsc::Sender<Vec<u8>> {
148 self.writer_tx.clone()
149 }
150
151 #[inline]
156 pub fn output_receiver(&self) -> broadcast::Receiver<Bytes> {
157 self.output_tx.subscribe()
158 }
159
160 #[inline]
162 pub fn has_exited(&self) -> bool {
163 self.exit_status.load(Ordering::SeqCst)
164 }
165
166 #[inline]
168 pub fn exit_code(&self) -> Option<i32> {
169 *self.exit_code.lock().unwrap_or_else(|e| e.into_inner())
170 }
171
172 #[inline]
174 pub fn is_output_drained(&self) -> bool {
175 self.reader_handle
176 .lock()
177 .ok()
178 .and_then(|guard| guard.as_ref().map(JoinHandle::is_finished))
179 .unwrap_or(true)
180 }
181
182 pub fn terminate(&self) {
186 self.terminate_internal();
187 }
188
189 pub fn terminate_process(&self) {
195 if let Ok(mut killer_opt) = self.killer.lock()
196 && let Some(mut killer) = killer_opt.take()
197 {
198 let _ = killer.kill();
199 }
200 }
201
202 fn terminate_internal(&self) {
204 if let Ok(mut killer_opt) = self.killer.lock()
206 && let Some(mut killer) = killer_opt.take()
207 {
208 let _ = killer.kill();
209 }
210
211 self.abort_tasks();
212 }
213
214 fn abort_tasks(&self) {
216 if let Ok(mut h) = self.reader_handle.lock()
218 && let Some(handle) = h.take()
219 {
220 handle.abort();
221 }
222
223 if let Ok(mut handles) = self.reader_abort_handles.lock() {
225 for handle in handles.drain(..) {
226 handle.abort();
227 }
228 }
229
230 if let Ok(mut h) = self.writer_handle.lock()
232 && let Some(handle) = h.take()
233 {
234 handle.abort();
235 }
236
237 if let Ok(mut h) = self.wait_handle.lock()
239 && let Some(handle) = h.take()
240 {
241 handle.abort();
242 }
243 }
244
245 #[inline]
247 pub fn is_running(&self) -> bool {
248 !self.has_exited() && !self.is_writer_closed()
249 }
250
251 pub async fn write(&self, bytes: impl Into<Vec<u8>>) -> Result<(), mpsc::error::SendError<Vec<u8>>> {
255 self.writer_tx.send(bytes.into()).await
256 }
257
258 #[inline]
260 pub fn is_writer_closed(&self) -> bool {
261 self.writer_tx.is_closed()
262 }
263}
264
265impl Drop for ProcessHandle {
266 fn drop(&mut self) {
267 self.terminate_internal();
277 }
278}
279
280#[derive(Debug)]
284pub struct SpawnedProcess {
285 pub session: ProcessHandle,
287 pub process_id: u32,
289 pub output_rx: broadcast::Receiver<Bytes>,
291 pub reliable_output_rx: mpsc::Receiver<Bytes>,
295 pub(crate) reliable_output_enabled: bool,
297 pub exit_rx: oneshot::Receiver<i32>,
299}
300
301impl SpawnedProcess {
302 pub async fn wait_with_output(self, timeout_ms: u64) -> (Vec<u8>, i32) {
306 if self.reliable_output_enabled {
307 collect_reliable_output_until_exit(self.reliable_output_rx, self.exit_rx, timeout_ms).await
308 } else {
309 collect_output_until_exit(self.output_rx, self.exit_rx, timeout_ms).await
310 }
311 }
312}
313
314async fn collect_reliable_output_until_exit(
316 mut output_rx: mpsc::Receiver<Bytes>,
317 exit_rx: oneshot::Receiver<i32>,
318 timeout_ms: u64,
319) -> (Vec<u8>, i32) {
320 let mut collected = Vec::new();
321 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
322 tokio::pin!(exit_rx);
323
324 loop {
325 tokio::select! {
326 chunk = output_rx.recv() => {
327 if let Some(chunk) = chunk {
328 collected.extend_from_slice(&chunk);
329 } else {
330 return (collected, exit_rx.await.unwrap_or(-1));
331 }
332 }
333 res = &mut exit_rx => {
334 let code = res.unwrap_or(-1);
335 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
340 let max_deadline = tokio::time::Instant::now()
341 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
342 while tokio::time::Instant::now() < max_deadline {
343 match tokio::time::timeout(quiet, output_rx.recv()).await {
344 Ok(Some(chunk)) => collected.extend_from_slice(&chunk),
345 Ok(None) | Err(_) => break,
346 }
347 }
348 return (collected, code);
349 }
350 _ = tokio::time::sleep_until(deadline) => {
351 return (collected, -1);
352 }
353 }
354 }
355}
356
357pub async fn collect_output_until_exit(
361 mut output_rx: broadcast::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 res = output_rx.recv() => {
372 if let Ok(chunk) = res {
373 collected.extend_from_slice(&chunk);
374 }
375 }
376 res = &mut exit_rx => {
377 let code = res.unwrap_or(-1);
378 let quiet = tokio::time::Duration::from_millis(POST_EXIT_DRAIN_QUIET_MS);
380 let max_deadline = tokio::time::Instant::now()
381 + tokio::time::Duration::from_millis(POST_EXIT_DRAIN_MAX_MS);
382
383 while tokio::time::Instant::now() < max_deadline {
384 match tokio::time::timeout(quiet, output_rx.recv()).await {
385 Ok(Ok(chunk)) => collected.extend_from_slice(&chunk),
386 Ok(Err(broadcast::error::RecvError::Lagged(count))) => {
387 eprintln!("[vtcode] output stream lagged ({count} dropped)");
388 continue;
389 }
390 Ok(Err(broadcast::error::RecvError::Closed)) => break,
391 Err(_) => break, }
393 }
394 return (collected, code);
395 }
396 _ = tokio::time::sleep_until(deadline) => {
397 return (collected, -1);
398 }
399 }
400 }
401}
402
403pub type ExecCommandSession = ProcessHandle;
405
406pub type SpawnedPty = SpawnedProcess;
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 struct NoopTerminator;
414 impl ChildTerminator for NoopTerminator {
415 fn kill(&mut self) -> io::Result<()> {
416 Ok(())
417 }
418 }
419
420 #[tokio::test]
421 async fn test_process_handle_debug() {
422 let exit_status = Arc::new(AtomicBool::new(false));
424 let exit_code = Arc::new(StdMutex::new(None));
425
426 let (writer_tx, _) = mpsc::channel(1);
427 let (output_tx, initial_rx) = broadcast::channel(1);
428
429 let (handle, _) = ProcessHandle::new(
430 writer_tx,
431 output_tx,
432 initial_rx,
433 Box::new(NoopTerminator),
434 tokio::spawn(async {}),
435 vec![],
436 tokio::spawn(async {}),
437 tokio::spawn(async {}),
438 exit_status,
439 exit_code,
440 None,
441 );
442
443 let debug_str = format!("{handle:?}");
444 assert!(debug_str.contains("ProcessHandle"));
445 }
446
447 #[tokio::test]
448 async fn test_has_exited() {
449 let exit_status = Arc::new(AtomicBool::new(false));
450 let exit_code = Arc::new(StdMutex::new(None));
451
452 let (writer_tx, _) = mpsc::channel(1);
453 let (output_tx, initial_rx) = broadcast::channel(1);
454
455 let (handle, _) = ProcessHandle::new(
456 writer_tx,
457 output_tx,
458 initial_rx,
459 Box::new(NoopTerminator),
460 tokio::spawn(async {}),
461 vec![],
462 tokio::spawn(async {}),
463 tokio::spawn(async {}),
464 Arc::clone(&exit_status),
465 exit_code,
466 None,
467 );
468
469 assert!(!handle.has_exited());
470 exit_status.store(true, Ordering::SeqCst);
471 assert!(handle.has_exited());
472 }
473
474 struct RecordingTerminator(Arc<AtomicBool>);
475 impl ChildTerminator for RecordingTerminator {
476 fn kill(&mut self) -> io::Result<()> {
477 self.0.store(true, Ordering::SeqCst);
478 Ok(())
479 }
480 }
481
482 #[tokio::test]
487 async fn drop_kills_child_synchronously_without_bridging_runtime() {
488 let killed = Arc::new(AtomicBool::new(false));
489 let exit_status = Arc::new(AtomicBool::new(false));
490 let exit_code = Arc::new(StdMutex::new(None));
491 let (writer_tx, _) = mpsc::channel(1);
492 let (output_tx, initial_rx) = broadcast::channel(1);
493
494 let (handle, _) = ProcessHandle::new(
496 writer_tx,
497 output_tx,
498 initial_rx,
499 Box::new(RecordingTerminator(Arc::clone(&killed))),
500 tokio::spawn(std::future::pending()),
501 vec![],
502 tokio::spawn(std::future::pending()),
503 tokio::spawn(std::future::pending()),
504 exit_status,
505 exit_code,
506 None,
507 );
508
509 let started = std::time::Instant::now();
510 drop(handle);
511 assert!(killed.load(Ordering::SeqCst), "Drop must kill the child");
512 assert!(started.elapsed() < std::time::Duration::from_millis(250), "Drop must not block on async cleanup");
513 }
514}