use super::*;
pub(crate) fn free_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.expect("the OS must have a free loopback port")
.local_addr()
.expect("a bound listener has an address")
.port()
}
pub(crate) fn scrape_metrics(addr: std::net::SocketAddr) -> std::io::Result<String> {
use std::io::{Read as _, Write as _};
let mut stream = std::net::TcpStream::connect(addr)?;
stream.set_read_timeout(Some(METRICS_SCRAPE_READ_TIMEOUT))?;
stream.write_all(b"GET /metrics HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n")?;
let mut body = String::new();
stream.read_to_string(&mut body)?;
Ok(body)
}
pub(crate) fn poll_metrics(addr: std::net::SocketAddr) -> String {
let start = Instant::now();
loop {
if let Ok(body) = scrape_metrics(addr) {
return body;
}
if start.elapsed() >= METRICS_SCRAPE_DEADLINE {
return String::new();
}
std::thread::sleep(METRICS_SCRAPE_POLL_INTERVAL);
}
}
#[cfg(unix)]
pub(crate) fn poll_metrics_containing(addr: std::net::SocketAddr, needle: &str) -> String {
poll_metrics_containing_all(addr, &[needle])
}
#[cfg(unix)]
pub(crate) fn poll_metrics_containing_all(addr: std::net::SocketAddr, needles: &[&str]) -> String {
let start = Instant::now();
let mut last = String::new();
loop {
if let Ok(body) = scrape_metrics(addr) {
if needles.iter().all(|needle| body.contains(needle)) {
return body;
}
last = body;
}
if start.elapsed() >= METRICS_SCRAPE_DEADLINE {
return last;
}
std::thread::sleep(METRICS_SCRAPE_POLL_INTERVAL);
}
}
#[cfg(unix)]
pub(crate) fn poll_metrics_without(addr: std::net::SocketAddr, needle: &str) -> String {
let start = Instant::now();
let mut last = String::new();
loop {
if let Ok(body) = scrape_metrics(addr) {
if body.contains("HTTP/1.1 200") && !body.contains(needle) {
return body;
}
last = body;
}
if start.elapsed() >= METRICS_SCRAPE_DEADLINE {
return last;
}
std::thread::sleep(METRICS_SCRAPE_POLL_INTERVAL);
}
}
pub(crate) fn http_get(
addr: std::net::SocketAddr,
path: &str,
headers: &[(&str, &str)],
) -> std::io::Result<(u16, String)> {
use std::io::{Read as _, Write as _};
let mut stream = std::net::TcpStream::connect(addr)?;
stream.set_read_timeout(Some(SERVE_HTTP_READ_TIMEOUT))?;
let mut request = format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\n");
for (name, value) in headers {
request.push_str(&format!("{name}: {value}\r\n"));
}
request.push_str("\r\n");
stream.write_all(request.as_bytes())?;
let mut raw = String::new();
stream.read_to_string(&mut raw)?;
let status = raw
.split_whitespace()
.nth(1)
.and_then(|code| code.parse().ok())
.unwrap_or(0);
let body = raw
.split_once("\r\n\r\n")
.map_or("", |(_, body)| body)
.to_string();
Ok((status, body))
}
pub(crate) fn poll_http_get(
addr: std::net::SocketAddr,
path: &str,
headers: &[(&str, &str)],
) -> (u16, String) {
let start = Instant::now();
loop {
if let Ok(answer) = http_get(addr, path, headers) {
return answer;
}
if start.elapsed() >= SERVE_HTTP_DEADLINE {
return (0, String::new());
}
std::thread::sleep(SERVE_HTTP_POLL_INTERVAL);
}
}
#[cfg(unix)]
pub(crate) fn wait_for_dog_pid(home: &Path, name: &str) -> nix::unistd::Pid {
let flock = poll_flock_listing(home, FLOCK_DEADLINE, |data| {
data.as_array().is_some_and(|entries| {
entries
.iter()
.any(|e| e["name"] == name && !e["pid"].is_null())
})
});
let dog = flock
.as_array()
.and_then(|entries| entries.iter().find(|e| e["name"] == name))
.unwrap_or_else(|| panic!("no entry named {name} in `shep flock`: {flock}"));
let pid = dog["pid"]
.as_i64()
.unwrap_or_else(|| panic!("dog {name} has no pid after {FLOCK_DEADLINE:?}: {dog}"));
nix::unistd::Pid::from_raw(i32::try_from(pid).expect("a real OS pid fits i32"))
}
pub(crate) fn bleats_no_follow_until_written(home: &Path, args: &[&str]) -> Output {
bleats_no_follow_until(home, args, |stdout| !stdout.is_empty())
}
pub(crate) fn bleats_no_follow_until_contains(
home: &Path,
args: &[&str],
needles: &[&str],
) -> Output {
bleats_no_follow_until(home, args, |stdout| {
needles.iter().all(|needle| stdout.contains(needle))
})
}
pub(crate) fn bleats_no_follow_until(
home: &Path,
args: &[&str],
done: impl Fn(&str) -> bool,
) -> Output {
let start = Instant::now();
loop {
let output = shep(home)
.arg("bleats")
.arg("--no-follow")
.args(args)
.output()
.unwrap();
if done(&String::from_utf8_lossy(&output.stdout)) || start.elapsed() >= BLEATS_DEADLINE {
return output;
}
std::thread::sleep(BLEATS_POLL_INTERVAL);
}
}
pub(crate) fn poll_flock_data(
home: &Path,
deadline: Duration,
done: impl Fn(&serde_json::Value) -> bool,
) -> serde_json::Value {
poll_flock_until(home, deadline, false, false, done)
}
pub(crate) fn poll_flock_listing(
home: &Path,
deadline: Duration,
done: impl Fn(&serde_json::Value) -> bool,
) -> serde_json::Value {
poll_flock_until(home, deadline, false, true, done)
}
#[cfg(unix)]
pub(crate) fn poll_flock_data_across_a_handover(
home: &Path,
deadline: Duration,
done: impl Fn(&serde_json::Value) -> bool,
) -> serde_json::Value {
poll_flock_until(home, deadline, true, false, done)
}
pub(crate) fn poll_flock_until(
home: &Path,
deadline: Duration,
tolerate_one_drop: bool,
with_dogs: bool,
done: impl Fn(&serde_json::Value) -> bool,
) -> serde_json::Value {
let start = Instant::now();
let mut tolerance = tolerate_one_drop;
loop {
let output = shep(home)
.arg("--format")
.arg("json")
.arg("flock")
.output()
.unwrap();
if tolerance && !output.status.success() && closed_by_a_handover(&output) {
tolerance = false;
std::thread::sleep(FLOCK_POLL_INTERVAL);
continue;
}
assert_success(&output);
let envelope: serde_json::Value = serde_json::from_slice(&output.stdout)
.unwrap_or_else(|e| panic!("flock stdout was not JSON: {e}"));
let data = if with_dogs {
envelope["data"].clone()
} else {
serde_json::Value::Array(sheep_rows(&envelope["data"]))
};
if done(&data) || start.elapsed() >= deadline {
return data;
}
std::thread::sleep(FLOCK_POLL_INTERVAL);
}
}
pub(crate) fn closed_by_a_handover(output: &Output) -> bool {
let stderr = String::from_utf8_lossy(&output.stderr);
stderr.contains(DROPPED_REPLY) || stderr.contains(DROPPED_HANDSHAKE)
}
pub(crate) fn sheep_rows(data: &serde_json::Value) -> Vec<serde_json::Value> {
data.as_array()
.into_iter()
.flatten()
.filter(|row| row["dog"].is_null())
.cloned()
.collect()
}
pub(crate) fn poll_flock(
home: &Path,
done: impl Fn(&serde_json::Value) -> bool,
) -> serde_json::Value {
let first = |data: &serde_json::Value| {
sheep_rows(data)
.into_iter()
.next()
.unwrap_or(serde_json::Value::Null)
};
first(&poll_flock_data(home, FLOCK_DEADLINE, |data| {
done(&first(data))
}))
}
pub(crate) fn poll_describe_lambs(home: &Path, name: &str, deadline: Duration) -> Output {
let start = Instant::now();
loop {
let output = shep(home)
.arg("--format")
.arg("json")
.arg("describe")
.arg(name)
.output()
.unwrap();
assert_success(&output);
let envelope: serde_json::Value = serde_json::from_slice(&output.stdout)
.unwrap_or_else(|e| panic!("describe stdout was not JSON: {e}"));
let has_lamb = envelope["data"][0]["lambs"]
.as_array()
.is_some_and(|lambs| !lambs.is_empty());
if has_lamb || start.elapsed() >= deadline {
return output;
}
std::thread::sleep(FLOCK_POLL_INTERVAL);
}
}
pub(crate) fn sheep_named<'a>(data: &'a serde_json::Value, name: &str) -> &'a serde_json::Value {
data.as_array()
.unwrap_or_else(|| panic!("flock data must be an array: {data}"))
.iter()
.find(|info| info["name"] == name)
.unwrap_or_else(|| panic!("no sheep named {name} in the flock: {data}"))
}