ej_io/
process.rs

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