use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use super::expand::expand_vars;
use super::service::discover_newest_for_spec;
use super::spec::{CheckSpec, NewerThanSpec, RigSpec, TimeSource};
use crate::error::{Error, Result};
pub fn evaluate(rig: &RigSpec, check: &CheckSpec) -> Result<()> {
let mut set = 0;
if check.http.is_some() {
set += 1;
}
if check.file.is_some() {
set += 1;
}
if check.command.is_some() {
set += 1;
}
if check.newer_than.is_some() {
set += 1;
}
if set == 0 {
return Err(Error::validation_invalid_argument(
"check",
"Check must specify one of `http`, `file`, `command`, or `newer_than`",
None,
None,
));
}
if set > 1 {
return Err(Error::validation_invalid_argument(
"check",
"Check must specify exactly one of `http`, `file`, `command`, or `newer_than`",
None,
None,
));
}
if let Some(url) = &check.http {
return http_check(rig, url, check.expect_status.unwrap_or(200));
}
if let Some(path) = &check.file {
return file_check(rig, path, check.contains.as_deref());
}
if let Some(cmd) = &check.command {
return command_check(rig, cmd, check.expect_exit.unwrap_or(0));
}
if let Some(spec) = &check.newer_than {
return newer_than_check(rig, spec);
}
Ok(())
}
const HTTP_WAIT_READY_BUDGET: Duration = Duration::from_secs(10);
const HTTP_RETRY_INTERVAL: Duration = Duration::from_millis(200);
fn http_check(rig: &RigSpec, url: &str, expect_status: u16) -> Result<()> {
let resolved = expand_vars(rig, url);
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(5))
.build()
.map_err(|e| Error::internal_unexpected(format!("build http client: {}", e)))?;
let deadline = std::time::Instant::now() + HTTP_WAIT_READY_BUDGET;
loop {
match client.get(&resolved).send() {
Ok(response) => {
let actual = response.status().as_u16();
if actual != expect_status {
return Err(Error::validation_invalid_argument(
"check.http",
format!(
"HTTP GET {} returned {} (expected {})",
resolved, actual, expect_status
),
None,
None,
));
}
return Ok(());
}
Err(e) if e.is_connect() && std::time::Instant::now() < deadline => {
std::thread::sleep(HTTP_RETRY_INTERVAL);
}
Err(e) => {
return Err(Error::validation_invalid_argument(
"check.http",
format!("HTTP GET {} failed: {}", resolved, e),
None,
None,
));
}
}
}
}
fn file_check(rig: &RigSpec, path: &str, contains: Option<&str>) -> Result<()> {
let resolved = expand_vars(rig, path);
let p = PathBuf::from(&resolved);
if !p.exists() {
return Err(Error::validation_invalid_argument(
"check.file",
format!("File does not exist: {}", resolved),
None,
None,
));
}
if let Some(needle) = contains {
let content = std::fs::read_to_string(&p).map_err(|e| {
Error::validation_invalid_argument(
"check.file",
format!("Read {} failed: {}", resolved, e),
None,
None,
)
})?;
if !content.contains(needle) {
return Err(Error::validation_invalid_argument(
"check.file",
format!(
"File {} does not contain expected substring {:?}",
resolved, needle
),
None,
None,
));
}
}
Ok(())
}
fn command_check(rig: &RigSpec, cmd: &str, expect_exit: i32) -> Result<()> {
let resolved = expand_vars(rig, cmd);
let output = Command::new("sh")
.arg("-c")
.arg(&resolved)
.output()
.map_err(|e| {
Error::validation_invalid_argument(
"check.command",
format!("Command spawn failed: {}", e),
None,
None,
)
})?;
let actual = output.status.code().unwrap_or(-1);
if actual != expect_exit {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::validation_invalid_argument(
"check.command",
format!(
"Command `{}` exited {} (expected {}){}",
resolved,
actual,
expect_exit,
if stderr.trim().is_empty() {
String::new()
} else {
format!(": {}", stderr.trim())
}
),
None,
None,
));
}
Ok(())
}
fn newer_than_check(rig: &RigSpec, spec: &NewerThanSpec) -> Result<()> {
let left = resolve_time_source(rig, &spec.left, "left")?;
let right = resolve_time_source(rig, &spec.right, "right")?;
match (left, right) {
(None, _) => Ok(()),
(Some(_), None) => Err(Error::validation_invalid_argument(
"check.newer_than.right",
"Right-side time source is missing — cannot compare against absent reference",
None,
None,
)),
(Some(l), Some(r)) => {
if l > r {
Ok(())
} else {
Err(Error::validation_invalid_argument(
"check.newer_than",
format!(
"Left ({}) is not newer than right ({}); diff = {}s",
l,
r,
r as i64 - l as i64
),
None,
None,
))
}
}
}
}
fn resolve_time_source(rig: &RigSpec, src: &TimeSource, side: &str) -> Result<Option<u64>> {
let mut set = 0;
if src.file_mtime.is_some() {
set += 1;
}
if src.process_start.is_some() {
set += 1;
}
if set == 0 {
return Err(Error::validation_invalid_argument(
format!("check.newer_than.{}", side),
"Time source must specify one of `file_mtime` or `process_start`",
None,
None,
));
}
if set > 1 {
return Err(Error::validation_invalid_argument(
format!("check.newer_than.{}", side),
"Time source must specify exactly one of `file_mtime` or `process_start`",
None,
None,
));
}
if let Some(path) = &src.file_mtime {
let resolved = expand_vars(rig, path);
let meta = std::fs::metadata(&resolved).map_err(|e| {
Error::validation_invalid_argument(
format!("check.newer_than.{}.file_mtime", side),
format!("Stat {} failed: {}", resolved, e),
None,
None,
)
})?;
let mtime = meta
.modified()
.map_err(|e| {
Error::validation_invalid_argument(
format!("check.newer_than.{}.file_mtime", side),
format!("Read mtime of {} failed: {}", resolved, e),
None,
None,
)
})?
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| {
Error::validation_invalid_argument(
format!("check.newer_than.{}.file_mtime", side),
format!("Bad mtime on {}: {}", resolved, e),
None,
None,
)
})?
.as_secs();
return Ok(Some(mtime));
}
if let Some(disc) = &src.process_start {
let expanded = super::spec::DiscoverSpec {
pattern: expand_vars(rig, &disc.pattern),
argv_contains: disc
.argv_contains
.iter()
.map(|selector| expand_vars(rig, selector))
.collect(),
};
let proc = discover_newest_for_spec(&expanded)?;
return Ok(proc.map(|p| p.started_at_epoch));
}
Ok(None)
}
#[cfg(test)]
#[path = "../../../tests/core/rig/check_test.rs"]
mod check_test;