feat: implement MetaPluginExec for external command execution
Co-authored-by: aider (openai/andrew/openrouter/sonoma-sky-alpha) <aider@aider.chat>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
use log::*;
|
use log::*;
|
||||||
use std::io::Write;
|
use std::io::{self, Write};
|
||||||
use std::process::{Command, Stdio, Child};
|
use std::process::{Command, Stdio, Child};
|
||||||
use which::which;
|
use which::which;
|
||||||
|
|
||||||
@@ -67,4 +67,224 @@ impl MetaPluginExec {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let plugin
|
/// let plugin = MetaPluginExec::new("date", &[], "date_output", false, None, None);
|
||||||
|
/// ```
|
||||||
|
pub fn new(
|
||||||
|
program: &str,
|
||||||
|
args: &[String],
|
||||||
|
meta_name: String,
|
||||||
|
split_whitespace: bool,
|
||||||
|
_options: Option<std::collections::HashMap<String, serde_yaml::Value>>,
|
||||||
|
outputs: Option<std::collections::HashMap<String, serde_yaml::Value>>,
|
||||||
|
) -> MetaPluginExec {
|
||||||
|
let supported = which(program).is_ok();
|
||||||
|
|
||||||
|
let mut base = BaseMetaPlugin::new();
|
||||||
|
|
||||||
|
// Set default output
|
||||||
|
let default_outputs = &[meta_name.as_str()];
|
||||||
|
base.initialize_plugin(default_outputs, _options, outputs);
|
||||||
|
|
||||||
|
MetaPluginExec {
|
||||||
|
program: program.to_string(),
|
||||||
|
args: args.to_vec(),
|
||||||
|
supported,
|
||||||
|
split_whitespace,
|
||||||
|
process: None,
|
||||||
|
writer: None,
|
||||||
|
result: None,
|
||||||
|
base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts the external process if not already running.
|
||||||
|
///
|
||||||
|
/// Spawns the command with piped stdin/stdout and stores the child process and writer.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `MetaPluginResponse` - Empty response, initializes the process.
|
||||||
|
fn start_process(&mut self) -> MetaPluginResponse {
|
||||||
|
if self.process.is_some() {
|
||||||
|
return MetaPluginResponse {
|
||||||
|
metadata: Vec::new(),
|
||||||
|
is_finalized: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.supported {
|
||||||
|
debug!("META: Exec plugin: program '{}' not supported", self.program);
|
||||||
|
return MetaPluginResponse {
|
||||||
|
metadata: Vec::new(),
|
||||||
|
is_finalized: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&self.program);
|
||||||
|
cmd.args(&self.args)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
|
||||||
|
match cmd.spawn() {
|
||||||
|
Ok(child) => {
|
||||||
|
let stdin = child.stdin.unwrap();
|
||||||
|
self.writer = Some(Box::new(stdin));
|
||||||
|
self.process = Some(child);
|
||||||
|
debug!("META: Exec plugin: started process for '{}'", self.program);
|
||||||
|
MetaPluginResponse {
|
||||||
|
metadata: Vec::new(),
|
||||||
|
is_finalized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("META: Exec plugin: failed to start '{}': {}", self.program, e);
|
||||||
|
MetaPluginResponse {
|
||||||
|
metadata: Vec::new(),
|
||||||
|
is_finalized: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MetaPlugin for MetaPluginExec {
|
||||||
|
fn meta_type(&self) -> MetaPluginType {
|
||||||
|
MetaPluginType::Exec
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_supported(&self) -> bool {
|
||||||
|
self.supported
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_internal(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize(&mut self) -> MetaPluginResponse {
|
||||||
|
self.start_process()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, data: &[u8]) -> MetaPluginResponse {
|
||||||
|
if let Some(writer) = self.writer.as_mut() {
|
||||||
|
if let Err(e) = writer.write_all(data) {
|
||||||
|
error!("META: Exec plugin: failed to write to stdin: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MetaPluginResponse {
|
||||||
|
metadata: Vec::new(),
|
||||||
|
is_finalized: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finalize(&mut self) -> MetaPluginResponse {
|
||||||
|
let mut metadata = Vec::new();
|
||||||
|
|
||||||
|
// Close stdin if writer exists
|
||||||
|
drop(self.writer.take());
|
||||||
|
|
||||||
|
// Wait for process to complete and capture output
|
||||||
|
if let Some(mut child) = self.process.take() {
|
||||||
|
match child.wait_with_output() {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let result = if self.split_whitespace {
|
||||||
|
stdout.split_whitespace().next().unwrap_or(&stdout).to_string()
|
||||||
|
} else {
|
||||||
|
stdout.trim().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.result = Some(result.clone());
|
||||||
|
|
||||||
|
if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs(
|
||||||
|
self.base.outputs().keys().next().unwrap_or(&"exec".to_string()),
|
||||||
|
serde_yaml::Value::String(result),
|
||||||
|
self.base.outputs(),
|
||||||
|
) {
|
||||||
|
metadata.push(meta_data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
error!("META: Exec plugin: command failed: {}", stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("META: Exec plugin: failed to wait on process: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MetaPluginResponse {
|
||||||
|
metadata,
|
||||||
|
is_finalized: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn program_info(&self) -> Option<(&str, Vec<&str>)> {
|
||||||
|
let args_str: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect();
|
||||||
|
Some((&self.program, args_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outputs(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
|
||||||
|
&self.base.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outputs_mut(&mut self) -> &mut std::collections::HashMap<String, serde_yaml::Value> {
|
||||||
|
&mut self.base.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> &std::collections::HashMap<String, serde_yaml::Value> {
|
||||||
|
&self.base.options
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options_mut(&mut self) -> &mut std::collections::HashMap<String, serde_yaml::Value> {
|
||||||
|
&mut self.base.options
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_outputs(&self) -> Vec<String> {
|
||||||
|
vec!["exec".to_string()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::meta_plugin::register_meta_plugin;
|
||||||
|
|
||||||
|
// Register the plugin at module initialization time
|
||||||
|
#[ctor::ctor]
|
||||||
|
fn register_exec_plugin() {
|
||||||
|
register_meta_plugin(MetaPluginType::Exec, |options, outputs| {
|
||||||
|
// Parse command from options for registration
|
||||||
|
let mut program_name = String::new();
|
||||||
|
let mut args = Vec::new();
|
||||||
|
let mut meta_name = "exec".to_string();
|
||||||
|
let mut split_whitespace = false;
|
||||||
|
|
||||||
|
if let Some(opts) = &options {
|
||||||
|
if let Some(command_value) = opts.get("command")
|
||||||
|
&& let Some(command_str) = command_value.as_str() {
|
||||||
|
let parts: Vec<&str> = command_str.split_whitespace().collect();
|
||||||
|
if !parts.is_empty() {
|
||||||
|
program_name = parts[0].to_string();
|
||||||
|
args = parts[1..].iter().map(|s| s.to_string()).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(split_value) = opts.get("split_whitespace")
|
||||||
|
&& let Some(split_bool) = split_value.as_bool() {
|
||||||
|
split_whitespace = split_bool;
|
||||||
|
}
|
||||||
|
if let Some(name_value) = opts.get("name")
|
||||||
|
&& let Some(name_str) = name_value.as_str() {
|
||||||
|
meta_name = name_str.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box::new(MetaPluginExec::new(
|
||||||
|
&program_name,
|
||||||
|
&args,
|
||||||
|
meta_name,
|
||||||
|
split_whitespace,
|
||||||
|
options,
|
||||||
|
outputs,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user