use log::*; use std::io::{self, Write}; use std::process::{Child, Command, Stdio}; use which::which; use crate::meta_plugin::{BaseMetaPlugin, MetaPlugin, MetaPluginResponse, MetaPluginType}; /// External program execution meta plugin. /// /// This plugin executes a specified external command during item save operations, /// capturing its output as metadata. It supports piping input data to the command's stdin /// and processing stdout. Useful for dynamic metadata generation via shell commands. /// /// # Examples /// /// Configured via options like `command: "date"`, the plugin runs `date` and captures output as metadata. pub struct MetaPluginExec { pub program: String, pub args: Vec, pub supported: bool, pub split_whitespace: bool, process: Option, writer: Option>, result: Option, base: BaseMetaPlugin, } // Manual Debug implementation because Box doesn't implement Debug /// Custom Debug implementation for MetaPluginExec. /// /// Obfuscates the writer field since Box does not 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("base", &self.base) .finish() } } impl MetaPluginExec { /// Creates a new MetaPluginExec instance. /// /// Validates the program availability using `which` and initializes outputs and options. /// The meta_name determines the default output key for captured command output. /// /// # Arguments /// /// * `program` - The executable name or path to run. /// * `args` - Slice of arguments to pass to the program. /// * `meta_name` - Name for the metadata output key. /// * `split_whitespace` - If true, takes the first whitespace-separated word from output; otherwise, trims full output. /// * `_options` - Optional configuration options (currently unused beyond passing through). /// * `outputs` - Optional output mappings to override defaults. /// /// # Returns /// /// * `MetaPluginExec` - New plugin instance, with `supported` set based on program availability. /// /// # Examples /// /// ``` /// # use keep::meta_plugin::MetaPluginExec; /// let plugin = MetaPluginExec::new("date", &[], "date_output".to_string(), false, None, None); /// ``` pub fn new( program: &str, args: &[String], meta_name: String, split_whitespace: bool, _options: Option>, outputs: Option>, ) -> 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(mut child) => { let stdin = child.stdin.take().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() && 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(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 { &self.base.outputs } fn outputs_mut(&mut self) -> &mut std::collections::HashMap { &mut self.base.outputs } fn options(&self) -> &std::collections::HashMap { &self.base.options } fn options_mut(&mut self) -> &mut std::collections::HashMap { &mut self.base.options } fn default_outputs(&self) -> Vec { 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, )) }); }