Skip to main content

vfa_tui/subprocess/
mod.rs

1pub mod executor;
2pub mod signal;
3pub mod stream;
4
5use std::time::{Duration, Instant};
6
7use tokio::process::Child;
8use tokio::sync::mpsc;
9
10#[allow(unused_imports)]
11pub use executor::SubprocessExecutor;
12#[allow(unused_imports)]
13pub use stream::{OutputLine, OutputStream};
14
15/// Handle to a running subprocess with streaming output capture.
16pub struct SubprocessHandle {
17    child: Child,
18    stdout_rx: mpsc::Receiver<OutputLine>,
19    stderr_rx: mpsc::Receiver<OutputLine>,
20    start_time: Instant,
21    timeout: Duration,
22    finished: bool,
23    exit_code: Option<i32>,
24}
25
26impl SubprocessHandle {
27    pub(crate) fn new(
28        child: Child,
29        stdout_rx: mpsc::Receiver<OutputLine>,
30        stderr_rx: mpsc::Receiver<OutputLine>,
31        timeout: Duration,
32    ) -> Self {
33        Self {
34            child,
35            stdout_rx,
36            stderr_rx,
37            start_time: Instant::now(),
38            timeout,
39            finished: false,
40            exit_code: None,
41        }
42    }
43
44    /// Cancel the subprocess by sending SIGTERM, then SIGKILL if needed.
45    pub async fn cancel(&mut self) -> anyhow::Result<()> {
46        if !self.finished {
47            signal::graceful_kill(&mut self.child).await?;
48            self.finished = true;
49        }
50        Ok(())
51    }
52
53    /// Try to receive the next stdout line without blocking.
54    pub fn try_recv_stdout(&mut self) -> Option<OutputLine> {
55        self.stdout_rx.try_recv().ok()
56    }
57
58    /// Try to receive the next stderr line without blocking.
59    pub fn try_recv_stderr(&mut self) -> Option<OutputLine> {
60        self.stderr_rx.try_recv().ok()
61    }
62
63    /// Returns true if the subprocess is still running.
64    pub fn is_running(&self) -> bool {
65        !self.finished
66    }
67
68    /// Returns the exit code if the subprocess has finished.
69    pub fn exit_code(&self) -> Option<i32> {
70        self.exit_code
71    }
72
73    /// Check if the subprocess has exceeded its timeout (synchronous check).
74    /// This does not kill the process; it only checks the elapsed time.
75    pub fn is_timed_out(&self) -> bool {
76        !self.finished && self.start_time.elapsed() > self.timeout
77    }
78
79    /// Non-blocking check if the subprocess has exited.
80    /// If it has, sets `finished` and `exit_code` and returns true.
81    /// This allows synchronous tick loops to detect completion.
82    pub fn try_poll_exit(&mut self) -> bool {
83        if self.finished {
84            return true;
85        }
86        match self.child.try_wait() {
87            Ok(Some(status)) => {
88                self.finished = true;
89                self.exit_code = Some(status.code().unwrap_or(-1));
90                true
91            }
92            _ => false,
93        }
94    }
95
96    /// Check if the subprocess has exceeded its timeout.
97    /// Returns true if timed out (and kills the process).
98    pub async fn check_timeout(&mut self) -> bool {
99        if self.finished {
100            return false;
101        }
102        if self.start_time.elapsed() > self.timeout {
103            self.cancel().await.ok();
104            self.finished = true;
105            true
106        } else {
107            false
108        }
109    }
110
111    /// Wait for the subprocess to complete and return its exit code.
112    pub async fn wait(&mut self) -> anyhow::Result<i32> {
113        if let Some(code) = self.exit_code {
114            return Ok(code);
115        }
116
117        let status = self.child.wait().await?;
118        self.finished = true;
119        let code = status.code().unwrap_or(-1);
120        self.exit_code = Some(code);
121        Ok(code)
122    }
123}