diff --git a/src/filter_plugin/exec.rs b/src/filter_plugin/exec.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/meta_plugin/exec.rs b/src/meta_plugin/exec.rs new file mode 100644 index 0000000..01228bf --- /dev/null +++ b/src/meta_plugin/exec.rs @@ -0,0 +1,227 @@ +use log::*; +use std::io::Write; +use std::process::{Command, Stdio, Child}; +use which::which; + +use crate::meta_plugin::{MetaPlugin, MetaPluginResponse, MetaPluginType}; + +pub struct MetaPluginExec { + pub program: String, + pub args: Vec, + pub supported: bool, + pub split_whitespace: bool, + process: Option, + writer: Option>, + result: Option, + outputs: std::collections::HashMap, + options: std::collections::HashMap, +} + +// Manual Debug implementation because Box doesn't implement Debug +impl std::fmt::Debug for MetaPluginExec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MetaPluginExec") + .field("program", &self.program) + .field("args", &self.args) + .field("supported", &self.supported) + .field("split_whitespace", &self.split_whitespace) + .field("process", &self.process) + .field("writer", &self.writer.as_ref().map(|_| "Box")) + .field("result", &self.result) + .field("outputs", &self.outputs) + .field("options", &self.options) + .finish() + } +} + + +impl MetaPluginExec { + pub fn new( + program: &str, + args: Vec<&str>, + meta_name: String, + split_whitespace: bool, + _options: Option>, + outputs: Option>, + ) -> MetaPluginExec { + let program_path = which(program); + let supported = program_path.is_ok(); + + // Start with default outputs + let mut final_outputs = std::collections::HashMap::new(); + final_outputs.insert(meta_name.clone(), serde_yaml::Value::String(meta_name.clone())); + if let Some(outs) = outputs { + for (key, value) in outs { + final_outputs.insert(key, value); + } + } + + // Start with default options + let mut final_options = std::collections::HashMap::new(); + if let Some(opts) = _options { + for (key, value) in opts { + final_options.insert(key, value); + } + } + + MetaPluginExec { + 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, + process: None, + writer: None, + result: None, + outputs: final_outputs, + options: final_options, + } + } + +} + +impl MetaPlugin for MetaPluginExec { + fn is_supported(&self) -> bool { + self.supported + } + + fn is_internal(&self) -> bool { + false + } + + fn initialize(&mut self) -> MetaPluginResponse { + debug!("META: Initializing program plugin: {:?}", self); + + let program = self.program.clone(); + let args = self.args.clone(); + + debug!("META: Executing command: {:?} {:?}", program, args); + + let mut process = match Command::new(program.clone()) + .args(args.clone()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(process) => process, + Err(e) => { + debug!("META: Failed to spawn process: {}", e); + return MetaPluginResponse { + metadata: Vec::new(), + is_finalized: true, + }; + } + }; + + let stdin = process.stdin.take().unwrap(); + self.writer = Some(Box::new(stdin)); + self.process = Some(process); + + MetaPluginResponse { + metadata: Vec::new(), + is_finalized: false, + } + } + + fn finalize(&mut self) -> MetaPluginResponse { + debug!("META: Finalizing program plugin"); + let mut metadata = Vec::new(); + + if let Some(process) = self.process.take() { + // Close stdin to signal end of input + drop(self.writer.take()); + + // Wait for the process to complete + let output = match process.wait_with_output() { + Ok(output) => output, + Err(e) => { + debug!("META: Failed to get process output: {}", e); + return MetaPluginResponse { + metadata: Vec::new(), + is_finalized: true, + }; + } + }; + + if output.status.success() { + // Process the output + let output_str = String::from_utf8_lossy(&output.stdout); + let result = if self.split_whitespace { + output_str.split_whitespace().next().unwrap_or("").to_string() + } else { + output_str.trim().to_string() + }; + + if !result.is_empty() { + debug!("META: Program output: {}", result); + self.result = Some(result.clone()); + + // Use process_metadata_outputs to handle output mapping + if let Some(meta_data) = crate::meta_plugin::process_metadata_outputs( + &self.meta_type().to_string(), + serde_yaml::Value::String(result), + &self.outputs + ) { + metadata.push(meta_data); + } + } + } else { + debug!("META: Program failed with status: {:?}", output.status); + let stderr = String::from_utf8_lossy(&output.stderr); + if !stderr.is_empty() { + debug!("META: Program stderr: {}", stderr); + } + } + } + + MetaPluginResponse { + metadata, + is_finalized: true, + } + } + + fn update(&mut self, data: &[u8]) -> MetaPluginResponse { + if let Some(ref mut writer) = self.writer { + if let Err(e) = writer.write_all(data) { + debug!("META: Failed to write to process stdin: {}", e); + } + } + MetaPluginResponse { + metadata: Vec::new(), + is_finalized: false, + } + } + + fn meta_type(&self) -> MetaPluginType { + MetaPluginType::Exec + } + + fn program_info(&self) -> Option<(&str, Vec<&str>)> { + if self.supported { + Some((&self.program, self.args.iter().map(|s| s.as_str()).collect())) + } else { + None + } + } + + fn outputs(&self) -> &std::collections::HashMap { + &self.outputs + } + + fn outputs_mut(&mut self) -> &mut std::collections::HashMap { + &mut self.outputs + } + + fn default_outputs(&self) -> Vec { + vec![self.meta_type().to_string()] + } + + + fn options(&self) -> &std::collections::HashMap { + &self.options + } + + fn options_mut(&mut self) -> &mut std::collections::HashMap { + &mut self.options + } +}