Skip to main content

ej_io/
process.rs

1//! Low-level async process management utilities.
2
3use std::{
4    ffi::OsStr,
5    io,
6    process::{ExitStatus, Stdio},
7    sync::{
8        Arc,
9        atomic::{AtomicBool, Ordering},
10    },
11    time::Duration,
12};
13
14use tokio::process::{Child, Command};
15
16/// Errors that can occur during process operations.
17#[derive(Debug)]
18pub enum ProcessError {
19    /// Failed to wait for child process.
20    WaitChildFail,
21    /// Failed to spawn the process.
22    SpawnProcessFail(io::Error),
23    /// Process was terminated.
24    Quit,
25}
26
27/// Current status of a running process.
28pub enum ProcessStatus {
29    /// Process has completed with exit status.
30    Done(ExitStatus),
31    /// Process is still running.
32    Running,
33}
34/// Spawn a new async process with piped stdout and stderr.
35///
36/// Launches a subprocess with the given command and arguments using tokio.
37/// Both stdout and stderr are piped and can be accessed via the returned Child.
38///
39/// # Arguments
40///
41/// * `cmd` - Command to execute
42/// * `args` - Command line arguments
43///
44/// # Returns
45///
46/// Returns a `Result<Child, io::Error>` - the spawned tokio process or an error.
47///
48/// # Examples
49///
50/// ```rust
51/// use ej_io::process::spawn_process;
52///
53/// #[tokio::main]
54/// async fn main() {
55///     let mut child = spawn_process("echo", vec!["Hello".to_string()]).unwrap();
56///     let output = child.stdout.take().unwrap();
57/// }
58/// ```
59pub fn spawn_process(cmd: &str, args: Vec<String>) -> Result<Child, io::Error> {
60    Command::new(OsStr::new(&cmd))
61        .args(args)
62        .stdout(Stdio::piped())
63        .stderr(Stdio::piped())
64        .spawn()
65}
66/// Asynchronously check process status without blocking.
67///
68/// Polls the process status in a non-blocking manner using tokio. Includes a small async sleep
69/// to prevent excessive CPU usage when called in a loop.
70///
71/// Note: This function may never return `ProcessStatus::Done` if the process
72/// is blocked waiting for stdin. Use `stop_child` and `capture_exit_status`
73/// to handle such cases.
74///
75/// # Arguments
76///
77/// * `child` - Mutable reference to the child process
78///
79/// # Returns
80///
81/// Returns a `Result<ProcessStatus, ProcessError>` indicating the current process state.
82///
83/// # Examples
84///
85/// ```rust
86/// use ej_io::process::{spawn_process, get_process_status, ProcessStatus};
87///
88/// #[tokio::main]
89/// async fn main() {
90///     let mut child = spawn_process("sleep", vec!["1".to_string()]).unwrap();
91///
92///     loop {
93///         match get_process_status(&mut child).await.unwrap() {
94///             ProcessStatus::Done(exit_status) => {
95///                 println!("Process finished with: {:?}", exit_status);
96///                 break;
97///             }
98///             ProcessStatus::Running => {
99///                 println!("Still running...");
100///             }
101///         }
102///     }
103/// }
104/// ```
105pub async fn get_process_status(child: &mut Child) -> Result<ProcessStatus, ProcessError> {
106    match child.try_wait() {
107        Ok(status) => match status {
108            Some(exit_status) => Ok(ProcessStatus::Done(exit_status)),
109            None => {
110                tokio::time::sleep(Duration::from_millis(10)).await;
111                Ok(ProcessStatus::Running)
112            }
113        },
114        Err(_) => return Err(ProcessError::WaitChildFail),
115    }
116}
117
118/// Asynchronously terminate a child process.
119///
120/// Sends a kill signal to the child process using tokio.
121///
122/// # Arguments
123///
124/// * `child` - Mutable reference to the child process
125///
126/// # Returns
127///
128/// Returns a `Result<(), io::Error>` indicating success or failure.
129///
130/// # Examples
131///
132/// ```rust
133/// use ej_io::process::{spawn_process, stop_child};
134///
135/// #[tokio::main]
136/// async fn main() {
137///     let mut child = spawn_process("sleep", vec!["60".to_string()]).unwrap();
138///     stop_child(&mut child).await.unwrap();
139/// }
140/// ```
141pub async fn stop_child(child: &mut Child) -> Result<(), io::Error> {
142    child.kill().await
143}
144/// Asynchronously capture the exit status of a child process.
145///
146/// Waits for the child process to complete and returns its exit status using tokio.
147/// This will close the stdin pipe, which can unblock processes waiting for input.
148///
149/// # Arguments
150///
151/// * `child` - Mutable reference to the child process
152///
153/// # Returns
154///
155/// Returns a `Result<ExitStatus, io::Error>` with the process exit status.
156///
157/// # Examples
158///
159/// ```rust
160/// use ej_io::process::{spawn_process, capture_exit_status};
161///
162/// #[tokio::main]
163/// async fn main() {
164///     let mut child = spawn_process("echo", vec!["done".to_string()]).unwrap();
165///     let exit_status = capture_exit_status(&mut child).await.unwrap();
166///     assert!(exit_status.success());
167/// }
168/// ```
169pub async fn capture_exit_status(child: &mut Child) -> Result<ExitStatus, io::Error> {
170    child.wait().await
171}
172
173/// Asynchronously wait for a child process with cancellation support.
174///
175/// Waits for the child process to complete while periodically checking
176/// if it should be cancelled via the atomic boolean flag. Uses tokio for async operation.
177///
178/// # Arguments
179///
180/// * `child` - Mutable reference to the child process
181/// * `should_stop` - Atomic flag to signal process termination
182///
183/// # Returns
184///
185/// Returns a `Result<ExitStatus, ProcessError>` with the process exit status or error.
186///
187/// # Examples
188///
189/// ```rust
190/// use ej_io::process::{spawn_process, wait_child};
191/// use std::sync::{Arc, atomic::AtomicBool};
192///
193/// #[tokio::main]
194/// async fn main() {
195///     let mut child = spawn_process("sleep", vec!["1".to_string()]).unwrap();
196///     let should_stop = Arc::new(AtomicBool::new(false));
197///
198///     let exit_status = wait_child(&mut child, should_stop).await.unwrap();
199///     assert!(exit_status.success());
200/// }
201/// ```
202pub async fn wait_child(
203    child: &mut Child,
204    should_stop: Arc<AtomicBool>,
205) -> Result<ExitStatus, ProcessError> {
206    loop {
207        if should_stop.load(Ordering::Relaxed) {
208            let _ = stop_child(child);
209            return Err(ProcessError::Quit);
210        }
211        match get_process_status(child).await {
212            Ok(status) => match status {
213                ProcessStatus::Done(exit_status) => return Ok(exit_status),
214                ProcessStatus::Running => {
215                    tokio::time::sleep(Duration::from_millis(10));
216                }
217            },
218            Err(_) => {
219                return Err(ProcessError::WaitChildFail);
220            }
221        }
222    }
223}