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