rustenium_core/
process.rs1use 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 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 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, 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 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, 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(); }
145 }
146}