use std::path::Path;
use std::time::Duration;
use tokio::process::Command;
use tracing::{debug, info, warn};
use crate::runtime::gates::types::{GateDef, GateResult, VerificationConfig, SKIPPED_GATE_COMMAND};
use crate::wire::protocol::scrub_secret_patterns;
pub async fn run_gates(config: &VerificationConfig, dir: &Path) -> Vec<GateResult> {
run_gates_with_evidence(config, dir, None).await
}
pub async fn run_gates_with_evidence(
config: &VerificationConfig,
dir: &Path,
output_dir: Option<&Path>,
) -> Vec<GateResult> {
let mut results = Vec::with_capacity(config.gates.len());
for (index, gate) in config.gates.iter().enumerate() {
let start = std::time::Instant::now();
info!(gate = %gate.name, command = %gate.command, "Running gate");
debug!(gate = %gate.name, args = %scrub_secret_patterns(&gate.args.join(" ")), "Running gate args");
if gate.command == SKIPPED_GATE_COMMAND {
let skipped_message = "Skipped by gate config".to_string();
results.push(GateResult {
name: gate.name.clone(),
passed: true,
stdout: String::new(),
stderr: skipped_message.clone(),
duration_ms: start.elapsed().as_millis() as u64,
required: gate.required,
command_line: "<skipped by config>".to_string(),
exit_code: None,
timed_out: false,
stdout_summary: None,
stderr_summary: Some(skipped_message),
output_path: None,
timeout_secs: gate.timeout_secs,
});
continue;
}
let command_line = render_command_line(&gate.command, &gate.args);
let mut cmd = Command::new(&gate.command);
cmd.args(&gate.args)
.current_dir(dir)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let timeout = if gate.timeout_secs > 0 {
Duration::from_secs(gate.timeout_secs)
} else {
Duration::from_secs(60)
};
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => {
if gate.timeout_secs > 0 {
warn!(gate = %gate.name, error = %e, "Failed to run gate command");
} else {
warn!(gate = %gate.name, error = %e, "Failed to spawn gate command");
}
let prefix = if gate.timeout_secs > 0 {
"Run error"
} else {
"Spawn error"
};
results.push(make_gate_error(
gate,
&command_line,
start,
&format!("{prefix}: {e}"),
));
continue;
}
};
let mut stdout = child.stdout.take().expect("stdout piped by spawn");
let mut stderr = child.stderr.take().expect("stderr piped by spawn");
let read_stdout = tokio::spawn(async move {
let mut buf = Vec::new();
let _ = tokio::io::AsyncReadExt::read_to_end(&mut stdout, &mut buf).await;
buf
});
let read_stderr = tokio::spawn(async move {
let mut buf = Vec::new();
let _ = tokio::io::AsyncReadExt::read_to_end(&mut stderr, &mut buf).await;
buf
});
let (status, stdout, stderr) = match tokio::time::timeout(timeout, child.wait()).await {
Ok(Ok(status)) => {
let stdout = read_stdout.await.unwrap_or_default();
let stderr = read_stderr.await.unwrap_or_default();
(status, stdout, stderr)
}
Ok(Err(e)) => {
read_stdout.abort();
read_stderr.abort();
if gate.timeout_secs > 0 {
warn!(gate = %gate.name, error = %e, "Failed to run gate command");
} else {
warn!(gate = %gate.name, error = %e, "Failed to spawn gate command");
}
let prefix = if gate.timeout_secs > 0 {
"Run error"
} else {
"Spawn error"
};
results.push(make_gate_error(
gate,
&command_line,
start,
&format!("{prefix}: {e}"),
));
continue;
}
Err(_) => {
read_stdout.abort();
read_stderr.abort();
let _ = child.start_kill();
let _ = child.wait().await;
let timeout_message = if gate.timeout_secs > 0 {
format!("Timed out after {}s", gate.timeout_secs)
} else {
"Timed out after 60s (default)".to_string()
};
warn!(gate = %gate.name, timeout = gate.timeout_secs, "Gate timed out");
results.push(make_gate_timeout(
gate,
&command_line,
start,
timeout_message,
));
continue;
}
};
let output = std::process::Output {
status,
stdout,
stderr,
};
let stdout = scrub_secret_patterns(&String::from_utf8_lossy(&output.stdout)).into_owned();
let stderr = scrub_secret_patterns(&String::from_utf8_lossy(&output.stderr)).into_owned();
let passed = output.status.success();
let exit_code = output.status.code();
let duration_ms = start.elapsed().as_millis() as u64;
let stdout_summary = summarize_output(&stdout);
let stderr_summary = summarize_output(&stderr);
let output_path = if let Some(dir) = output_dir {
write_full_output_artifact(dir, &gate.name, index, &stdout, &stderr).await
} else {
None
};
info!(
gate = %gate.name,
passed,
duration_ms,
"Gate complete"
);
results.push(GateResult {
name: gate.name.clone(),
passed,
stdout,
stderr,
duration_ms,
required: gate.required,
command_line,
exit_code,
timed_out: false,
stdout_summary,
stderr_summary,
output_path,
timeout_secs: gate.timeout_secs,
});
}
results
}
fn make_gate_error(
gate: &GateDef,
command_line: &str,
start: std::time::Instant,
message: &str,
) -> GateResult {
GateResult {
name: gate.name.clone(),
passed: false,
stdout: String::new(),
stderr: message.to_string(),
duration_ms: start.elapsed().as_millis() as u64,
required: gate.required,
command_line: command_line.to_string(),
exit_code: None,
timed_out: false,
stdout_summary: None,
stderr_summary: Some(message.to_string()),
output_path: None,
timeout_secs: gate.timeout_secs,
}
}
fn make_gate_timeout(
gate: &GateDef,
command_line: &str,
start: std::time::Instant,
message: String,
) -> GateResult {
GateResult {
name: gate.name.clone(),
passed: false,
stdout: String::new(),
stderr: message.clone(),
duration_ms: start.elapsed().as_millis() as u64,
required: gate.required,
command_line: command_line.to_string(),
exit_code: None,
timed_out: true,
stdout_summary: None,
stderr_summary: Some(message),
output_path: None,
timeout_secs: gate.timeout_secs,
}
}
fn render_command_line(command: &str, args: &[String]) -> String {
if args.is_empty() {
command.to_string()
} else {
format!("{command} {}", args.join(" "))
}
}
fn summarize_output(text: &str) -> Option<String> {
let mut lines: Vec<String> = text
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.take(3)
.map(|line| {
let mut out = line.to_string();
if out.chars().count() > 240 {
out = format!("{}...", out.chars().take(240).collect::<String>());
}
out
})
.collect();
if lines.is_empty() {
return None;
}
if text.lines().count() > 3 {
lines.push("...".to_string());
}
Some(lines.join("\n"))
}
async fn write_full_output_artifact(
output_dir: &Path,
gate_name: &str,
gate_index: usize,
stdout: &str,
stderr: &str,
) -> Option<String> {
if tokio::fs::create_dir_all(output_dir).await.is_err() {
return None;
}
let safe_name = gate_name
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect::<String>();
let file_name = format!("gate-{:02}-{}.log", gate_index + 1, safe_name);
let path = output_dir.join(file_name);
let body = format!(
"[stdout]\n{}\n\n[stderr]\n{}\n",
stdout.trim_end(),
stderr.trim_end()
);
if tokio::fs::write(&path, body).await.is_ok() {
Some(path.to_string_lossy().to_string())
} else {
None
}
}