use crate::services::async_bridge::AsyncMessage;
use std::process::Stdio;
use std::sync::mpsc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
pub async fn spawn_plugin_process(
process_id: u64,
command: String,
args: Vec<String>,
cwd: Option<String>,
sender: mpsc::Sender<AsyncMessage>,
) {
let mut cmd = Command::new(&command);
cmd.args(&args);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
if let Some(ref dir) = cwd {
cmd.current_dir(dir);
}
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => {
let _ = sender.send(AsyncMessage::PluginProcessOutput {
process_id,
stdout: String::new(),
stderr: format!("Failed to spawn process: {}", e),
exit_code: -1,
});
return;
}
};
let stdout_handle = child.stdout.take();
let stderr_handle = child.stderr.take();
let stdout_future = async {
if let Some(stdout) = stdout_handle {
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
let mut output = String::new();
while let Ok(Some(line)) = lines.next_line().await {
output.push_str(&line);
output.push('\n');
}
output
} else {
String::new()
}
};
let stderr_future = async {
if let Some(stderr) = stderr_handle {
let reader = BufReader::new(stderr);
let mut lines = reader.lines();
let mut output = String::new();
while let Ok(Some(line)) = lines.next_line().await {
output.push_str(&line);
output.push('\n');
}
output
} else {
String::new()
}
};
let (stdout, stderr) = tokio::join!(stdout_future, stderr_future);
let exit_code = match child.wait().await {
Ok(status) => status.code().unwrap_or(-1),
Err(_) => -1,
};
let _ = sender.send(AsyncMessage::PluginProcessOutput {
process_id,
stdout,
stderr,
exit_code,
});
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_spawn_simple_command() {
let (sender, receiver) = mpsc::channel();
spawn_plugin_process(
1,
"echo".to_string(),
vec!["hello".to_string()],
None,
sender,
)
.await;
let msg = receiver.recv().unwrap();
match msg {
AsyncMessage::PluginProcessOutput {
process_id,
stdout,
stderr,
exit_code,
} => {
assert_eq!(process_id, 1);
assert!(stdout.contains("hello"));
assert_eq!(stderr, "");
assert_eq!(exit_code, 0);
}
_ => panic!("Expected PluginProcessOutput"),
}
}
#[tokio::test]
async fn test_spawn_with_args() {
let (sender, receiver) = mpsc::channel();
spawn_plugin_process(
2,
"printf".to_string(),
vec![
"%s %s".to_string(),
"hello".to_string(),
"world".to_string(),
],
None,
sender,
)
.await;
let msg = receiver.recv().unwrap();
match msg {
AsyncMessage::PluginProcessOutput {
process_id,
stdout,
exit_code,
..
} => {
assert_eq!(process_id, 2);
assert!(stdout.contains("hello world"));
assert_eq!(exit_code, 0);
}
_ => panic!("Expected PluginProcessOutput"),
}
}
#[tokio::test]
async fn test_spawn_nonexistent_command() {
let (sender, receiver) = mpsc::channel();
spawn_plugin_process(
3,
"this_command_does_not_exist_12345".to_string(),
vec![],
None,
sender,
)
.await;
let msg = receiver.recv().unwrap();
match msg {
AsyncMessage::PluginProcessOutput {
process_id,
stdout,
stderr,
exit_code,
} => {
assert_eq!(process_id, 3);
assert_eq!(stdout, "");
assert!(stderr.contains("Failed to spawn"));
assert_eq!(exit_code, -1);
}
_ => panic!("Expected PluginProcessOutput"),
}
}
#[tokio::test]
async fn test_spawn_failing_command() {
let (sender, receiver) = mpsc::channel();
spawn_plugin_process(
4,
"sh".to_string(),
vec!["-c".to_string(), "exit 42".to_string()],
None,
sender,
)
.await;
let msg = receiver.recv().unwrap();
match msg {
AsyncMessage::PluginProcessOutput {
process_id,
exit_code,
..
} => {
assert_eq!(process_id, 4);
assert_eq!(exit_code, 42);
}
_ => panic!("Expected PluginProcessOutput"),
}
}
}