use log::*; use std::io::Write; use std::process::{Command, Stdio, Child}; use which::which; use crate::meta_plugin::{MetaPlugin, MetaPluginResponse}; pub struct MetaPluginProgram { pub program: String, pub args: Vec, pub supported: bool, pub meta_name: String, pub split_whitespace: bool, process: Option, writer: Option>, item_id: Option, result: Option, outputs: std::collections::HashMap, options: std::collections::HashMap, } impl std::fmt::Debug for MetaPluginProgram { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MetaPluginProgram") .field("program", &self.program) .field("args", &self.args) .field("supported", &self.supported) .field("meta_name", &self.meta_name) .field("split_whitespace", &self.split_whitespace) .field("process", &self.process) .field("writer", &"Box") .field("outputs", &self.outputs) .field("options", &self.options) .finish() } } impl MetaPluginProgram { pub fn new( program: &str, args: Vec<&str>, meta_name: String, split_whitespace: bool, _options: Option>, outputs: Option>, ) -> MetaPluginProgram { 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); } } MetaPluginProgram { 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, meta_name, split_whitespace, process: None, writer: None, item_id: None, result: None, outputs: final_outputs, options: final_options, } } pub fn new_simple(program: &str, args: Vec<&str>, meta_name: String, split_whitespace: bool) -> MetaPluginProgram { Self::new(program, args, meta_name, split_whitespace, None, None) } } impl MetaPlugin for MetaPluginProgram { 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); // Create metadata to be returned - clone before moving into self.result let result_clone = result.clone(); self.result = Some(result_clone); metadata.push(crate::meta_plugin::MetaData { name: self.meta_name.clone(), value: result, }); } } 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_name(&self) -> String { self.meta_name.clone() } 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_name.clone()] } fn default_options(&self) -> std::collections::HashMap { std::collections::HashMap::new() } fn options(&self) -> &std::collections::HashMap { &self.options } fn options_mut(&mut self) -> &mut std::collections::HashMap { &mut self.options } }