feat: add exec filter plugin for external command execution
Co-authored-by: aider (openai/andrew/openrouter/sonoma-sky-alpha) <aider@aider.chat>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
use super::{FilterPlugin, FilterOption};
|
||||
use std::io::{Result, Read, Write};
|
||||
use std::process::{Command, Stdio, Child};
|
||||
use which::which;
|
||||
use log::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecFilter {
|
||||
program: String,
|
||||
args: Vec<String>,
|
||||
supported: bool,
|
||||
split_whitespace: bool,
|
||||
child_process: Option<Child>,
|
||||
stdin_writer: Option<std::process::ChildStdin>,
|
||||
stdout_reader: Option<std::process::ChildStdout>,
|
||||
}
|
||||
|
||||
impl ExecFilter {
|
||||
pub fn new(
|
||||
program: &str,
|
||||
args: Vec<&str>,
|
||||
split_whitespace: bool,
|
||||
) -> ExecFilter {
|
||||
let program_path = which(program);
|
||||
let supported = program_path.is_ok();
|
||||
|
||||
ExecFilter {
|
||||
program: program_path.map_or_else(|| program.to_string(), |p| p.to_string_lossy().to_string()),
|
||||
args: args.iter().map(|s| s.to_string()).collect(),
|
||||
supported,
|
||||
split_whitespace,
|
||||
child_process: None,
|
||||
stdin_writer: None,
|
||||
stdout_reader: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterPlugin for ExecFilter {
|
||||
fn filter(&mut self, mut reader: Box<&mut dyn Read>, mut writer: Box<&mut dyn Write>) -> Result<()> {
|
||||
if !self.supported {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Program '{}' not found", self.program),
|
||||
));
|
||||
}
|
||||
|
||||
debug!("FILTER_EXEC: Executing command: {} {:?}", self.program, self.args);
|
||||
|
||||
let mut child = Command::new(&self.program)
|
||||
.args(&self.args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to spawn process '{}': {}", self.program, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let stdin = child.stdin.take().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to capture stdin from child process",
|
||||
)
|
||||
})?;
|
||||
let stdout = child.stdout.take().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to capture stdout from child process",
|
||||
)
|
||||
})?;
|
||||
|
||||
self.stdin_writer = Some(stdin);
|
||||
self.stdout_reader = Some(stdout);
|
||||
self.child_process = Some(child);
|
||||
|
||||
let mut stdin_writer = self.stdin_writer.as_mut().unwrap();
|
||||
let mut stdout_reader = self.stdout_reader.as_mut().unwrap();
|
||||
|
||||
// Thread to copy from input reader to child stdin
|
||||
let input_thread = std::thread::spawn(move || {
|
||||
std::io::copy(&mut *reader, &mut *stdin_writer)
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to write to process stdin: {}", e),
|
||||
)
|
||||
})?;
|
||||
// Close stdin to signal EOF to the child process
|
||||
drop(stdin_writer);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Thread to copy from child stdout to output writer
|
||||
let output_thread = std::thread::spawn(move || {
|
||||
std::io::copy(&mut *stdout_reader, &mut *writer)
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to read from process stdout: {}", e),
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Wait for both threads to complete
|
||||
input_thread.join().map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Input thread panicked: {:?}", e),
|
||||
)
|
||||
})?;
|
||||
output_thread.join().map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Output thread panicked: {:?}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Wait for the child process to finish
|
||||
if let Some(mut child) = self.child_process.take() {
|
||||
let output = child.wait_with_output()
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to wait on child process: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.is_empty() {
|
||||
warn!("FILTER_EXEC: Process stderr: {}", stderr);
|
||||
}
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Process exited with error: {:?}", output.status),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
debug!("FILTER_EXEC: Process completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn FilterPlugin> {
|
||||
Box::new(ExecFilter {
|
||||
program: self.program.clone(),
|
||||
args: self.args.clone(),
|
||||
supported: self.supported,
|
||||
split_whitespace: self.split_whitespace,
|
||||
child_process: None,
|
||||
stdin_writer: None,
|
||||
stdout_reader: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<FilterOption> {
|
||||
vec![
|
||||
FilterOption {
|
||||
name: "command".to_string(),
|
||||
default: None,
|
||||
required: true,
|
||||
},
|
||||
FilterOption {
|
||||
name: "split_whitespace".to_string(),
|
||||
default: Some(serde_json::Value::Bool(true)),
|
||||
required: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin at module initialization time
|
||||
#[ctor::ctor]
|
||||
fn register_exec_filter() {
|
||||
crate::services::filter_service::register_filter_plugin("exec", || {
|
||||
// Create a dummy instance - actual creation happens in create method
|
||||
Box::new(ExecFilter {
|
||||
program: String::new(),
|
||||
args: Vec::new(),
|
||||
supported: false,
|
||||
split_whitespace: true,
|
||||
child_process: None,
|
||||
stdin_writer: None,
|
||||
stdout_reader: None,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user