Files
keep/src/meta_plugin/exec.rs
Andrew Phillips 9b7cbd5244 fix: resolve doctest failures, database bugs, and remove dead code
- Fix all 96 doctest failures across 20 files by adding hidden imports and
  proper test setup (68 pass, 33 intentionally ignored)
- Fix set_item_tags: wrap in transaction and replace item.id.unwrap() with
  proper error handling
- Fix get_items_matching: replace N+1 per-item meta queries with batch
  get_meta_for_items() call
- Fix get_item_matching: apply meta filtering instead of ignoring the parameter
- Remove duplicate doc comment in store_meta
- Remove dead code files: plugin.rs, plugins.rs, binary_detection.rs
  (never declared as modules)
- Apply cargo fmt formatting fixes
- Add keep.db to .gitignore
2026-03-12 11:58:44 -03:00

308 lines
10 KiB
Rust

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<String>,
pub supported: bool,
pub split_whitespace: bool,
process: Option<Child>,
writer: Option<Box<dyn Write>>,
result: Option<String>,
base: BaseMetaPlugin,
}
// Manual Debug implementation because Box<dyn Write> doesn't implement Debug
/// Custom Debug implementation for MetaPluginExec.
///
/// Obfuscates the writer field since Box<dyn Write> 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<dyn Write>"))
.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<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(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<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,
))
});
}