52 lines
1.6 KiB
Rust
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(())
|
|
}
|