use std::{
path::Path,
process::{Child, Command, ExitStatus, Stdio},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
time::Duration,
};
use anyhow::{Context, bail};
use super::types::AxvisorHttpProbeConfig;
const PROBE_EXIT_POLL_INTERVAL: Duration = Duration::from_millis(100);
pub(crate) fn run(
addr: &str,
config: &AxvisorHttpProbeConfig,
case_dir: &Path,
stop: Arc<AtomicBool>,
) -> anyhow::Result<()> {
let script = case_dir.join(&config.probe_script);
ensure_probe_asset(&script)?;
println!(
" host http probe: running probe asset {}",
script.display()
);
let mut child = spawn_probe_asset(&script, addr, config, case_dir)?;
match wait_probe_asset(&mut child, &stop) {
Some(status) if status.success() => Ok(()),
Some(status) => bail!(
"probe asset {} exited with code {}",
script.display(),
status.code().unwrap_or(-1)
),
None => bail!("probe asset {} was killed", script.display()),
}
}
fn ensure_probe_asset(script: &Path) -> anyhow::Result<()> {
if !script.is_file() {
bail!(
"probe asset {} does not exist; add it to the case directory (or set \
[host_http_probe] probe_script)",
script.display()
);
}
Ok(())
}
fn spawn_probe_asset(
script: &Path,
addr: &str,
config: &AxvisorHttpProbeConfig,
case_dir: &Path,
) -> anyhow::Result<Child> {
Command::new(script)
.env("AXVISOR_HTTP_BASE", format!("http://{addr}"))
.env(
"AXVISOR_HTTP_TOKEN",
config.token.clone().unwrap_or_default(),
)
.env("AXVISOR_HTTP_CASE_DIR", case_dir)
.env(
"AXVISOR_HTTP_CONNECT_TIMEOUT",
config.connect_timeout_secs.to_string(),
)
.env(
"AXVISOR_HTTP_REQUEST_TIMEOUT",
config.request_timeout_secs.to_string(),
)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("failed to spawn probe asset {}", script.display()))
}
fn wait_probe_asset(child: &mut Child, stop: &AtomicBool) -> Option<ExitStatus> {
loop {
if stop.load(Ordering::Acquire) {
let _ = child.kill();
let _ = child.wait();
return None;
}
if let Some(status) = child.try_wait().ok().flatten() {
return Some(status);
}
thread::sleep(PROBE_EXIT_POLL_INTERVAL);
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use serde::Deserialize;
use super::{super::types::DEFAULT_PROBE_SCRIPT, *};
fn test_config(probe_script: PathBuf) -> AxvisorHttpProbeConfig {
AxvisorHttpProbeConfig {
guest_port: 8080,
connect_timeout_secs: 120,
request_timeout_secs: 5,
probe_script,
token: Some("t".into()),
}
}
fn parse_probe_section(toml_body: &str) -> AxvisorHttpProbeConfig {
#[derive(Deserialize)]
struct ProbeSection {
#[serde(default)]
host_http_probe: Option<AxvisorHttpProbeConfig>,
}
toml::from_str::<ProbeSection>(toml_body)
.expect("probe section parses")
.host_http_probe
.expect("host_http_probe present")
}
#[cfg(unix)]
fn write_fixture_probe(dir: &Path, name: &str, code: i32) -> PathBuf {
use std::{fs, os::unix::fs::PermissionsExt};
let path = dir.join(name);
let script = format!(
"#!/bin/sh\nprintf '%s' \
\"$AXVISOR_HTTP_BASE|$AXVISOR_HTTP_TOKEN|$AXVISOR_HTTP_CASE_DIR\" > \
\"$AXVISOR_HTTP_CASE_DIR/env.txt\"\nexit {code}\n"
);
fs::write(&path, script).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
path
}
#[test]
fn probe_script_defaults_to_http_probe_py() {
let config = parse_probe_section("[host_http_probe]\ntoken = \"t\"\n");
assert_eq!(config.probe_script, PathBuf::from(DEFAULT_PROBE_SCRIPT));
}
#[test]
fn probe_script_is_configurable() {
let config = parse_probe_section("[host_http_probe]\nprobe_script = \"custom_probe.sh\"\n");
assert_eq!(config.probe_script, PathBuf::from("custom_probe.sh"));
}
#[cfg(unix)]
#[test]
fn run_executes_the_case_probe_asset_with_env() {
let dir = tempfile::tempdir().unwrap();
let probe = write_fixture_probe(dir.path(), "http_probe.py", 0);
let config = test_config(PathBuf::from("http_probe.py"));
let stop = Arc::new(AtomicBool::new(false));
let result = run("127.0.0.1:12345", &config, dir.path(), stop);
assert!(result.is_ok(), "probe asset should pass: {result:?}");
let recorded = std::fs::read_to_string(dir.path().join("env.txt")).unwrap();
assert_eq!(
recorded,
"http://127.0.0.1:12345|t|".to_string() + &dir.path().to_string_lossy()
);
assert!(probe.exists());
}
#[cfg(unix)]
#[test]
fn run_propagates_nonzero_probe_exit() {
let dir = tempfile::tempdir().unwrap();
write_fixture_probe(dir.path(), "http_probe.py", 1);
let config = test_config(PathBuf::from("http_probe.py"));
let stop = Arc::new(AtomicBool::new(false));
let error = run("127.0.0.1:12345", &config, dir.path(), stop).unwrap_err();
assert!(error.to_string().contains("exited with code 1"));
}
#[test]
fn run_rejects_a_missing_probe_asset() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(PathBuf::from("http_probe.py"));
let stop = Arc::new(AtomicBool::new(false));
let error = run("127.0.0.1:12345", &config, dir.path(), stop).unwrap_err();
assert!(error.to_string().contains("does not exist"));
}
#[cfg(unix)]
#[test]
fn run_kills_the_probe_asset_when_stop_is_requested() {
let dir = tempfile::tempdir().unwrap();
write_fixture_probe(dir.path(), "http_probe.py", 0);
let config = test_config(PathBuf::from("http_probe.py"));
let stop = Arc::new(AtomicBool::new(true));
let error = run("127.0.0.1:12345", &config, dir.path(), stop).unwrap_err();
assert!(error.to_string().contains("was killed"));
}
#[test]
fn http_control_plane_case_carries_a_probe_asset() {
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..");
let case_asset = workspace_root.join(
"test-suit/axvisor/normal/qemu-http-control-plane/http-control-plane/http_probe.py",
);
assert!(
case_asset.is_file(),
"http-control-plane case missing probe asset: {}",
case_asset.display()
);
let name = case_asset.file_name().and_then(|s| s.to_str()).unwrap();
assert_eq!(name, DEFAULT_PROBE_SCRIPT);
}
}