par_term_scripting/
process.rs1use std::collections::HashMap;
8use std::io::{BufRead, BufReader, Write};
9use std::process::{Child, Command, Stdio};
10use std::sync::{Arc, Mutex};
11use std::thread::JoinHandle;
12
13use super::protocol::{ScriptCommand, ScriptEvent};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ScriptStatus {
23 Running,
25 Exited { success: bool },
27}
28
29pub struct ScriptProcess {
35 child: Option<Child>,
37 stdin_writer: Option<std::process::ChildStdin>,
39 command_buffer: Arc<Mutex<Vec<ScriptCommand>>>,
41 error_buffer: Arc<Mutex<Vec<String>>>,
43 _stdout_thread: Option<JoinHandle<()>>,
45 _stderr_thread: Option<JoinHandle<()>>,
47 exit: Option<ScriptStatus>,
49}
50
51impl ScriptProcess {
52 pub fn spawn(
65 command: &str,
66 args: &[&str],
67 env_vars: &HashMap<String, String>,
68 ) -> Result<Self, String> {
69 let mut cmd = Command::new(command);
70 cmd.args(args)
71 .stdin(Stdio::piped())
72 .stdout(Stdio::piped())
73 .stderr(Stdio::piped())
74 .envs(env_vars);
75
76 let mut child = cmd
77 .spawn()
78 .map_err(|e| format!("Failed to spawn '{}': {}", command, e))?;
79
80 let stdin_writer = child.stdin.take();
81 let stdout = child
82 .stdout
83 .take()
84 .ok_or_else(|| "Failed to capture stdout".to_string())?;
85 let stderr = child
86 .stderr
87 .take()
88 .ok_or_else(|| "Failed to capture stderr".to_string())?;
89
90 let command_buffer: Arc<Mutex<Vec<ScriptCommand>>> = Arc::new(Mutex::new(Vec::new()));
91 let error_buffer: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
92
93 let cmd_buf = Arc::clone(&command_buffer);
95 let stdout_thread = std::thread::spawn(move || {
96 let reader = BufReader::new(stdout);
97 for line in reader.lines() {
98 match line {
99 Ok(text) => {
100 if text.is_empty() {
101 continue;
102 }
103 match serde_json::from_str::<ScriptCommand>(&text) {
104 Ok(cmd) => {
105 let mut buf = cmd_buf.lock().unwrap_or_else(|e| {
106 log::warn!("command_buffer mutex poisoned, recovering");
107 e.into_inner()
108 });
109 buf.push(cmd);
110 }
111 Err(e) => {
112 log::warn!(
113 "ScriptProcess: failed to parse stdout line as ScriptCommand: {}: {:?}",
114 e,
115 text
116 );
117 }
118 }
119 }
120 Err(e) => {
121 log::warn!("ScriptProcess: error reading stdout: {}", e);
122 break;
123 }
124 }
125 }
126 });
127
128 let err_buf = Arc::clone(&error_buffer);
130 let stderr_thread = std::thread::spawn(move || {
131 let reader = BufReader::new(stderr);
132 for line in reader.lines() {
133 match line {
134 Ok(text) => {
135 if text.is_empty() {
136 continue;
137 }
138 let mut buf = err_buf.lock().unwrap_or_else(|e| {
139 log::warn!("error_buffer mutex poisoned, recovering");
140 e.into_inner()
141 });
142 buf.push(text);
143 }
144 Err(e) => {
145 log::warn!("ScriptProcess: error reading stderr: {}", e);
146 break;
147 }
148 }
149 }
150 });
151
152 Ok(Self {
153 child: Some(child),
154 stdin_writer,
155 command_buffer,
156 error_buffer,
157 _stdout_thread: Some(stdout_thread),
158 _stderr_thread: Some(stderr_thread),
159 exit: None,
160 })
161 }
162
163 pub fn poll_status(&mut self) -> ScriptStatus {
170 if let Some(status) = self.exit {
171 return status;
172 }
173 let polled = match self.child.as_mut() {
176 Some(child) => match child.try_wait() {
177 Ok(Some(s)) => Some(ScriptStatus::Exited {
178 success: s.success(),
179 }),
180 Ok(None) => None,
181 Err(_) => Some(ScriptStatus::Exited { success: false }),
182 },
183 None => Some(ScriptStatus::Exited { success: true }),
186 };
187 match polled {
188 Some(status) => {
189 self.stdin_writer.take();
192 self.child.take();
193 self.exit = Some(status);
194 status
195 }
196 None => ScriptStatus::Running,
197 }
198 }
199
200 pub fn is_running(&mut self) -> bool {
204 matches!(self.poll_status(), ScriptStatus::Running)
205 }
206
207 pub fn send_event(&mut self, event: &ScriptEvent) -> Result<(), String> {
212 let stdin = self
213 .stdin_writer
214 .as_mut()
215 .ok_or_else(|| "stdin writer is not available".to_string())?;
216
217 let json = serde_json::to_string(event)
218 .map_err(|e| format!("Failed to serialize event: {}", e))?;
219
220 writeln!(stdin, "{}", json).map_err(|e| format!("Failed to write to stdin: {}", e))?;
221
222 stdin
223 .flush()
224 .map_err(|e| format!("Failed to flush stdin: {}", e))?;
225
226 Ok(())
227 }
228
229 pub fn read_commands(&self) -> Vec<ScriptCommand> {
234 let mut buf = self.command_buffer.lock().unwrap_or_else(|e| {
235 log::warn!("command_buffer mutex poisoned, recovering");
236 e.into_inner()
237 });
238 buf.drain(..).collect()
239 }
240
241 pub fn read_errors(&self) -> Vec<String> {
246 let mut buf = self.error_buffer.lock().unwrap_or_else(|e| {
247 log::warn!("error_buffer mutex poisoned, recovering");
248 e.into_inner()
249 });
250 buf.drain(..).collect()
251 }
252
253 pub fn stop(&mut self) {
258 self.stdin_writer.take();
260
261 if let Some(ref mut child) = self.child {
262 let _ = child.kill();
264 let _ = child.wait();
266 }
267 self.child.take();
268 }
269}
270
271impl Drop for ScriptProcess {
272 fn drop(&mut self) {
273 self.stop();
274 }
275}
276
277#[cfg(all(test, unix))]
278mod tests {
279 use super::*;
280 use std::collections::HashMap;
281 use std::time::Duration;
282
283 fn no_env() -> HashMap<String, String> {
284 HashMap::new()
285 }
286
287 #[test]
288 fn poll_status_running_for_live_process() {
289 let mut p = ScriptProcess::spawn("sleep", &["2"], &no_env()).expect("spawn sleep");
290 assert_eq!(p.poll_status(), ScriptStatus::Running);
291 p.stop();
292 }
293
294 #[test]
295 fn poll_status_exited_clean_for_exit_zero() {
296 let mut p = ScriptProcess::spawn("true", &[], &no_env()).expect("spawn true");
297 std::thread::sleep(Duration::from_millis(50));
298 assert_eq!(p.poll_status(), ScriptStatus::Exited { success: true });
299 assert_eq!(p.poll_status(), ScriptStatus::Exited { success: true });
301 }
302
303 #[test]
304 fn poll_status_exited_failure_for_nonzero_exit() {
305 let mut p = ScriptProcess::spawn("false", &[], &no_env()).expect("spawn false");
306 std::thread::sleep(Duration::from_millis(50));
307 assert_eq!(p.poll_status(), ScriptStatus::Exited { success: false });
308 }
309
310 #[test]
311 fn is_running_false_after_exit() {
312 let mut p = ScriptProcess::spawn("true", &[], &no_env()).expect("spawn true");
313 std::thread::sleep(Duration::from_millis(50));
314 assert!(!p.is_running());
315 }
316}