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}