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