Files
PostfixAdminRS/src/hooks.rs
T
2026-11-03 16:52:28 +01:00

52 lines
1.6 KiB
Rust

use std::{collections::HashMap, process::Stdio, time::Duration};
use tokio::{io::AsyncWriteExt, process::Command, time::timeout};
use crate::error::{AppError, AppResult};
pub async fn run(command: &[String], vars: &[(&str, &str)], stdin: Option<&[u8]>) -> AppResult<()> {
if command.is_empty() {
return Ok(());
}
let vars: HashMap<&str, &str> = vars.iter().copied().collect();
let expand = |value: &str| {
let mut out = value.to_string();
for (key, replacement) in &vars {
out = out.replace(&format!("{{{key}}}"), replacement);
}
out
};
let program = expand(&command[0]);
if program.is_empty() {
return Err(AppError::config("hook program cannot be empty"));
}
let mut cmd = Command::new(program);
for arg in command.iter().skip(1) {
cmd.arg(expand(arg));
}
cmd.kill_on_drop(true)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if stdin.is_some() {
cmd.stdin(Stdio::piped());
} else {
cmd.stdin(Stdio::null());
}
let mut child = cmd.spawn()?;
if let (Some(data), Some(mut pipe)) = (stdin, child.stdin.take()) {
pipe.write_all(data).await?;
}
let output = timeout(Duration::from_secs(30), child.wait_with_output())
.await
.map_err(|_| AppError::config("hook timed out after 30 seconds"))??;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AppError::config(format!(
"hook failed with {}: {}",
output.status,
stderr.trim()
)));
}
Ok(())
}