xprocess/
lib.rs

1use std::ffi::OsStr;
2use std::io::Read;
3use std::os::unix::process::CommandExt;
4use std::process::{Child, Command, Stdio};
5
6use anyhow::{Result, bail};
7
8/// Reference of a system process spawned by [`Process::spawn`]
9///
10/// # Example
11///
12/// ```ignore
13/// use xprocess::Process;
14///
15/// fn main() {
16///    let process = Process::spawn("sleep").expect("Failed to spawn process");
17///    println!("Spawned process with PID: {}", process.pid());
18///    process.kill().expect("Failed to kill process");
19/// }
20/// ```
21///
22pub struct Process {
23    pid: u32,
24    child: Option<Child>,
25}
26
27impl Process {
28    pub fn spawn<S: AsRef<OsStr>>(cmd: S) -> Result<Self> {
29        let mut command = Self::build_command::<S, _, S>(cmd, []);
30        Self::spawn_child_process(&mut command)
31    }
32
33    pub fn spawn_with_args<S, I, T>(cmd: S, args: I) -> Result<Self>
34    where
35        T: AsRef<OsStr>,
36        I: IntoIterator<Item = T>,
37        S: AsRef<OsStr>,
38    {
39        let mut command = Self::build_command(cmd, args);
40        Self::spawn_child_process(&mut command)
41    }
42
43    fn build_command<S, I, T>(cmd: S, args: I) -> Command
44    where
45        T: AsRef<OsStr>,
46        I: IntoIterator<Item = T>,
47        S: AsRef<OsStr>,
48    {
49        let mut command = Command::new(cmd);
50        command.args(args);
51        command
52    }
53
54    fn spawn_child_process(cmd: &mut Command) -> Result<Self> {
55        let mut child = cmd
56            .stdin(Stdio::null())
57            .stdout(Stdio::piped())
58            .stderr(Stdio::piped());
59
60        unsafe {
61            child = child.pre_exec(|| {
62                // Create a new session to detach the process
63                libc::setsid();
64                Ok(())
65            });
66        }
67
68        let child_process = child.spawn()?;
69        let pid = child_process.id();
70
71        Ok(Self {
72            pid,
73            child: Some(child_process),
74        })
75    }
76
77    /// Retrieves PID for the spawned process
78    pub fn pid(&self) -> u32 {
79        self.pid
80    }
81
82    /// Reads and returns the stdout of the process
83    ///
84    /// This method reads all available output from stdout and returns it as a String.
85    /// The method will block until the process closes its stdout stream.
86    ///
87    /// **Important:** This method consumes the stdout handle. Subsequent calls will return
88    /// an empty String.
89    ///
90    /// **Note:** For processes that produce output and then continue running, consider
91    /// waiting for the process to finish or close stdout before calling this method,
92    /// otherwise it may block indefinitely.
93    ///
94    /// # Example
95    ///
96    /// ```ignore
97    /// let mut process = Process::spawn_with_args("echo", ["hello"]).expect("Failed to spawn");
98    /// // Wait for the process to finish writing
99    /// std::thread::sleep(std::time::Duration::from_millis(100));
100    /// let output = process.stdout().expect("Failed to read stdout");
101    /// assert_eq!(output.trim(), "hello");
102    /// ```
103    pub fn stdout(&mut self) -> Result<String> {
104        if let Some(ref mut child) = self.child
105            && let Some(ref mut stdout) = child.stdout
106        {
107            let mut output = String::new();
108            stdout.read_to_string(&mut output)?;
109            return Ok(output);
110        }
111        Ok(String::new())
112    }
113
114    /// Reads and returns the stderr of the process
115    ///
116    /// This method reads all available output from stderr and returns it as a String.
117    /// The method will block until the process closes its stderr stream.
118    ///
119    /// **Important:** This method consumes the stderr handle. Subsequent calls will return
120    /// an empty String.
121    ///
122    /// **Note:** For processes that produce output and then continue running, consider
123    /// waiting for the process to finish or close stderr before calling this method,
124    /// otherwise it may block indefinitely.
125    ///
126    /// # Example
127    ///
128    /// ```ignore
129    /// let mut process = Process::spawn_with_args("ls", ["/nonexistent"]).expect("Failed to spawn");
130    /// // Wait for the process to finish writing
131    /// std::thread::sleep(std::time::Duration::from_millis(100));
132    /// let error = process.stderr().expect("Failed to read stderr");
133    /// assert!(error.contains("No such file or directory"));
134    /// ```
135    pub fn stderr(&mut self) -> Result<String> {
136        if let Some(ref mut child) = self.child
137            && let Some(ref mut stderr) = child.stderr
138        {
139            let mut output = String::new();
140            stderr.read_to_string(&mut output)?;
141            return Ok(output);
142        }
143        Ok(String::new())
144    }
145
146    /// Kills the process referenced by this instance of [`Process`]
147    pub fn kill(&self) -> Result<()> {
148        match Command::new("kill").arg(self.pid().to_string()).status() {
149            Ok(status) => {
150                if status.success() {
151                    return Ok(());
152                }
153
154                bail!("Failed to kill process with PID: {}", self.pid());
155            }
156            Err(e) => {
157                bail!("Error executing kill command: {}", e);
158            }
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use std::thread;
166    use std::time::Duration;
167
168    use super::*;
169
170    #[test]
171    fn spawn_process() {
172        let process = Process::spawn("sleep").expect("Failed to spawn process");
173        assert!(process.pid() > 0);
174        thread::sleep(Duration::from_millis(100));
175        let result = process.kill();
176        assert!(result.is_ok(), "Failed to kill the process");
177    }
178
179    #[test]
180    fn spawn_process_with_args() {
181        let process = Process::spawn_with_args("sleep", ["1"]).expect("Failed to spawn process");
182        assert!(process.pid() > 0);
183        thread::sleep(Duration::from_millis(100));
184        let result = process.kill();
185        assert!(result.is_ok(), "Failed to kill the process");
186    }
187
188    #[test]
189    fn spawn_process_with_args_different_types() {
190        let process = Process::spawn_with_args("sleep", [String::from("1")])
191            .expect("Failed to spawn process");
192        assert!(process.pid() > 0);
193        thread::sleep(Duration::from_millis(100));
194        let result = process.kill();
195        assert!(result.is_ok(), "Failed to kill the process");
196    }
197
198    #[test]
199    fn capture_stdout() {
200        let mut process =
201            Process::spawn_with_args("echo", ["hello world"]).expect("Failed to spawn process");
202        thread::sleep(Duration::from_millis(100));
203        let stdout = process.stdout().expect("Failed to read stdout");
204        assert_eq!(stdout.trim(), "hello world");
205        process.kill().expect("Failed to kill process");
206    }
207
208    #[test]
209    fn capture_stderr() {
210        let mut process = Process::spawn_with_args("sh", ["-c", "echo error message >&2"])
211            .expect("Failed to spawn process");
212        thread::sleep(Duration::from_millis(100));
213        let stderr = process.stderr().expect("Failed to read stderr");
214        assert_eq!(stderr.trim(), "error message");
215        process.kill().expect("Failed to kill process");
216    }
217
218    #[test]
219    fn capture_both_stdout_and_stderr() {
220        let mut process =
221            Process::spawn_with_args("sh", ["-c", "echo stdout message; echo stderr message >&2"])
222                .expect("Failed to spawn process");
223        thread::sleep(Duration::from_millis(100));
224        let stdout = process.stdout().expect("Failed to read stdout");
225        let stderr = process.stderr().expect("Failed to read stderr");
226        assert_eq!(stdout.trim(), "stdout message");
227        assert_eq!(stderr.trim(), "stderr message");
228        process.kill().expect("Failed to kill process");
229    }
230
231    #[test]
232    fn capture_empty_stdout() {
233        let mut process = Process::spawn_with_args("true", Vec::<String>::new())
234            .expect("Failed to spawn process");
235        thread::sleep(Duration::from_millis(100));
236        let stdout = process.stdout().expect("Failed to read stdout");
237        assert_eq!(stdout, "");
238        process.kill().ok(); // Process might already be finished
239    }
240}