use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use super::protocol::{ScriptCommand, ScriptEvent};
pub struct ScriptProcess {
child: Option<Child>,
stdin_writer: Option<std::process::ChildStdin>,
command_buffer: Arc<Mutex<Vec<ScriptCommand>>>,
error_buffer: Arc<Mutex<Vec<String>>>,
_stdout_thread: Option<JoinHandle<()>>,
_stderr_thread: Option<JoinHandle<()>>,
}
impl ScriptProcess {
pub fn spawn(
command: &str,
args: &[&str],
env_vars: &HashMap<String, String>,
) -> Result<Self, String> {
let mut cmd = Command::new(command);
cmd.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.envs(env_vars);
let mut child = cmd
.spawn()
.map_err(|e| format!("Failed to spawn '{}': {}", command, e))?;
let stdin_writer = child.stdin.take();
let stdout = child
.stdout
.take()
.ok_or_else(|| "Failed to capture stdout".to_string())?;
let stderr = child
.stderr
.take()
.ok_or_else(|| "Failed to capture stderr".to_string())?;
let command_buffer: Arc<Mutex<Vec<ScriptCommand>>> = Arc::new(Mutex::new(Vec::new()));
let error_buffer: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let cmd_buf = Arc::clone(&command_buffer);
let stdout_thread = std::thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(text) => {
if text.is_empty() {
continue;
}
match serde_json::from_str::<ScriptCommand>(&text) {
Ok(cmd) => {
let mut buf = cmd_buf.lock().unwrap_or_else(|e| {
log::warn!("command_buffer mutex poisoned, recovering");
e.into_inner()
});
buf.push(cmd);
}
Err(e) => {
log::warn!(
"ScriptProcess: failed to parse stdout line as ScriptCommand: {}: {:?}",
e,
text
);
}
}
}
Err(e) => {
log::warn!("ScriptProcess: error reading stdout: {}", e);
break;
}
}
}
});
let err_buf = Arc::clone(&error_buffer);
let stderr_thread = std::thread::spawn(move || {
let reader = BufReader::new(stderr);
for line in reader.lines() {
match line {
Ok(text) => {
if text.is_empty() {
continue;
}
let mut buf = err_buf.lock().unwrap_or_else(|e| {
log::warn!("error_buffer mutex poisoned, recovering");
e.into_inner()
});
buf.push(text);
}
Err(e) => {
log::warn!("ScriptProcess: error reading stderr: {}", e);
break;
}
}
}
});
Ok(Self {
child: Some(child),
stdin_writer,
command_buffer,
error_buffer,
_stdout_thread: Some(stdout_thread),
_stderr_thread: Some(stderr_thread),
})
}
pub fn is_running(&mut self) -> bool {
match self.child.as_mut() {
Some(child) => match child.try_wait() {
Ok(Some(_status)) => false, Ok(None) => true, Err(_) => false, },
None => false,
}
}
pub fn send_event(&mut self, event: &ScriptEvent) -> Result<(), String> {
let stdin = self
.stdin_writer
.as_mut()
.ok_or_else(|| "stdin writer is not available".to_string())?;
let json = serde_json::to_string(event)
.map_err(|e| format!("Failed to serialize event: {}", e))?;
writeln!(stdin, "{}", json).map_err(|e| format!("Failed to write to stdin: {}", e))?;
stdin
.flush()
.map_err(|e| format!("Failed to flush stdin: {}", e))?;
Ok(())
}
pub fn read_commands(&self) -> Vec<ScriptCommand> {
let mut buf = self.command_buffer.lock().unwrap_or_else(|e| {
log::warn!("command_buffer mutex poisoned, recovering");
e.into_inner()
});
buf.drain(..).collect()
}
pub fn read_errors(&self) -> Vec<String> {
let mut buf = self.error_buffer.lock().unwrap_or_else(|e| {
log::warn!("error_buffer mutex poisoned, recovering");
e.into_inner()
});
buf.drain(..).collect()
}
pub fn stop(&mut self) {
self.stdin_writer.take();
if let Some(ref mut child) = self.child {
let _ = child.kill();
let _ = child.wait();
}
self.child.take();
}
}
impl Drop for ScriptProcess {
fn drop(&mut self) {
self.stop();
}
}