use std::time::Duration;
use tokio::process::Command;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum TriggerCondition {
#[default]
NonEmpty,
ExitNonZero,
ExitZero,
Regex(String),
Always,
}
impl TriggerCondition {
pub fn parse(s: Option<&str>) -> Self {
let raw = match s.map(str::trim) {
Some(v) if !v.is_empty() => v,
_ => return Self::NonEmpty,
};
if let Some(pat) = raw
.strip_prefix("regex:")
.or_else(|| raw.strip_prefix("re:"))
{
return Self::Regex(pat.trim().to_string());
}
match raw.to_lowercase().as_str() {
"exit_non_zero" | "exitnonzero" | "non_zero" => Self::ExitNonZero,
"exit_zero" | "exitzero" | "zero" | "exit_0" => Self::ExitZero,
"always" => Self::Always,
_ => Self::NonEmpty,
}
}
pub fn should_fire(&self, result: &TriggerResult) -> bool {
match self {
Self::NonEmpty => !result.combined_output().trim().is_empty(),
Self::ExitNonZero => result.exit_code != 0,
Self::ExitZero => result.exit_code == 0,
Self::Regex(pat) => match regex::Regex::new(pat) {
Ok(re) => re.is_match(&result.combined_output()),
Err(e) => {
tracing::warn!("Invalid regex trigger condition '{pat}': {e}");
false
}
},
Self::Always => true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TriggerResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
}
impl TriggerResult {
pub fn combined_output(&self) -> String {
if self.stderr.is_empty() {
self.stdout.clone()
} else if self.stdout.is_empty() {
self.stderr.clone()
} else {
format!("{}\n{}", self.stdout, self.stderr)
}
}
}
#[derive(Debug, Clone)]
pub struct TriggerRunner {
timeout: Duration,
}
impl Default for TriggerRunner {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
}
}
}
impl TriggerRunner {
pub fn new(timeout: Duration) -> Self {
Self { timeout }
}
pub async fn run(&self, cmd: &str) -> Result<TriggerResult, String> {
let child = Command::new("/bin/sh")
.arg("-c")
.arg(cmd)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|e| format!("Failed to spawn trigger process: {e}"))?;
let output_res = tokio::time::timeout(self.timeout, child.wait_with_output()).await;
match output_res {
Ok(Ok(output)) => {
let exit_code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok(TriggerResult {
exit_code,
stdout,
stderr,
})
}
Ok(Err(e)) => Err(format!("Trigger process execution error: {e}")),
Err(_) => Err(format!(
"Trigger command timed out after {}s",
self.timeout.as_secs()
)),
}
}
}