Skip to main content

rustenium_core/
process.rs

1use std::process::Stdio;
2
3use regex::Regex;
4use tokio::io::{AsyncBufReadExt, BufReader};
5use tokio::process::{Child, Command};
6use tokio::time::{timeout, Duration};
7
8#[derive(Debug)]
9pub struct Process {
10    child: Option<Child>,
11}
12
13impl Process {
14    pub fn create<S, I>(exe_path: S, args: I) -> Process
15    where
16        S: AsRef<str>,
17        I: IntoIterator<Item = String>,
18    {
19        let exe = exe_path.as_ref();
20        let args_vec: Vec<String> = args.into_iter().collect();
21
22        // Log the full command being executed with details
23        tracing::info!(
24            "Starting process with executable: '{}' and arguments: {:?}",
25            exe,
26            args_vec
27        );
28        tracing::info!(
29            "Full command: {} {}",
30            exe,
31            args_vec.join(" ")
32        );
33
34        let mut child = Command::new(exe)
35            .args(args_vec)
36            .stdout(Stdio::piped())
37            .stderr(Stdio::piped())
38            .kill_on_drop(true)
39            .spawn()
40            .expect("Failed to start process");
41
42        // Capture and log stdout
43        if let Some(stdout) = child.stdout.take() {
44            let exe_name = exe.to_string();
45            tokio::spawn(async move {
46                let mut reader = BufReader::new(stdout);
47                let mut line = String::new();
48                loop {
49                    line.clear();
50                    match reader.read_line(&mut line).await {
51                        Ok(0) => break, // EOF
52                        Ok(_) => {
53                            tracing::debug!("[{} stdout] {}", exe_name, line.trim());
54                        }
55                        Err(e) => {
56                            tracing::error!("[{} stdout] Error reading: {}", exe_name, e);
57                            break;
58                        }
59                    }
60                }
61            });
62        }
63
64        // Capture and log stderr
65        if let Some(stderr) = child.stderr.take() {
66            let exe_name = exe.to_string();
67            tokio::spawn(async move {
68                let mut reader = BufReader::new(stderr);
69                let mut line = String::new();
70                loop {
71                    line.clear();
72                    match reader.read_line(&mut line).await {
73                        Ok(0) => break, // EOF
74                        Ok(_) => {
75                            tracing::debug!("[{} stderr] {}", exe_name, line.trim());
76                        }
77                        Err(e) => {
78                            tracing::error!("[{} stderr] Error reading: {}", exe_name, e);
79                            break;
80                        }
81                    }
82                }
83            });
84        }
85
86        let child = Some(child);
87
88        return Self { child };
89    }
90
91    #[deprecated]
92    pub async fn wait_for_pattern(&mut self, pattern: &str, timeout_secs: Option<u64>) -> String {
93        let timeout_secs = timeout_secs.unwrap_or(20);
94        let regex = Regex::new(pattern).expect("Invalid regex pattern");
95        let child = self.child.as_mut().unwrap();
96
97        let stdout = child.stdout.as_mut().expect("Failed to access stdout");
98        let stderr = child.stderr.as_mut().expect("Failed to access stderr");
99
100        let mut stdout_lines = BufReader::new(stdout).lines();
101        let mut stderr_lines = BufReader::new(stderr).lines();
102
103        let check_line = |_label: &str, line: Result<Option<String>, _>| -> Option<String> {
104            if let Ok(Some(line)) = line {
105                if let Some(captures) = regex.captures(&line) {
106                    if let Some(url) = captures.get(1) {
107                        return Some(url.as_str().into());
108                    }
109                }
110            }
111            None
112        };
113
114        let timeout_duration = Duration::from_secs(timeout_secs);
115
116        let timeout_result = timeout(timeout_duration, async {
117            loop {
118                tokio::select! {
119                    stdout_line = stdout_lines.next_line() => {
120                        if let Some(line) = check_line("stdout", stdout_line) {
121                            return Some(line);
122                        }
123                    },
124                    stderr_line = stderr_lines.next_line() => {
125                        if let Some(line) = check_line("stderr", stderr_line) {
126                            return  Some(line);
127                        }
128                    }
129                }
130            }
131        })
132        .await;
133
134        match timeout_result {
135            Ok(Some(matched)) => matched,
136            Ok(None) => panic!("Found a pattern but None"),
137            Err(_) => panic!("Timeout reached without finding pattern"),
138        }
139    }
140}
141
142impl Process {
143    pub fn kill(&mut self) -> Result<(), crate::error::ProcessKillError> {
144        if let Some(mut child) = self.child.take() {
145            if let Some(pid) = child.id() {
146                let pid_str = pid.to_string();
147                #[cfg(unix)]
148                {
149                    let _ = std::process::Command::new("pkill")
150                        .args(["-9", "-P", &pid_str])
151                        .output();
152                }
153                #[cfg(windows)]
154                {
155                    let _ = std::process::Command::new("taskkill")
156                        .args(["/F", "/T", "/PID", &pid_str])
157                        .output();
158                }
159            }
160            child.start_kill().map_err(|_| crate::error::ProcessKillError)?;
161            Ok(())
162        } else {
163            Err(crate::error::ProcessKillError)
164        }
165    }
166}
167
168impl Drop for Process {
169    fn drop(&mut self) {
170        let _ = self.kill();
171    }
172}