use std::net::TcpStream;
use std::path::PathBuf;
use std::time::Duration;
use crate::connector::{ServiceInfo, ServiceStatus};
pub(super) fn binary_on_path(binary: &str) -> bool {
which::which(binary).is_ok()
}
pub(super) fn read_addr_file(path: &std::path::Path) -> Option<String> {
let raw = std::fs::read_to_string(path).ok()?;
let trimmed = raw.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
pub(super) fn tcp_probe(addr: &str) -> bool {
let Ok(socket_addr) = addr.parse() else {
return false;
};
TcpStream::connect_timeout(&socket_addr, Duration::from_millis(300))
.map(|s| {
drop(s);
true
})
.unwrap_or(false)
}
pub(super) fn fetch_health_version(addr: &str) -> Option<String> {
use std::io::{Read, Write};
let mut stream =
TcpStream::connect_timeout(&addr.parse().ok()?, Duration::from_millis(800)).ok()?;
stream
.set_read_timeout(Some(Duration::from_millis(800)))
.ok()?;
let request = format!("GET /health HTTP/1.0\r\nHost: {addr}\r\n\r\n");
stream.write_all(request.as_bytes()).ok()?;
let mut buf = Vec::new();
stream.read_to_end(&mut buf).ok()?;
let raw = String::from_utf8_lossy(&buf);
let body_start = raw.find("\r\n\r\n").map(|i| i + 4)?;
let body = &raw[body_start..];
let json: serde_json::Value = serde_json::from_str(body).ok()?;
json.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
pub(super) fn detect_service(
id: &'static str,
display_name: &'static str,
binary: &str,
addr_file: PathBuf,
) -> ServiceInfo {
if !binary_on_path(binary) {
return ServiceInfo {
id: id.to_string(),
display_name: display_name.to_string(),
status: ServiceStatus::Absent,
version: None,
url: None,
hint: None,
};
}
if let Some(addr) = read_addr_file(&addr_file)
&& tcp_probe(&addr)
{
let base_url = format!("http://{addr}");
let version = fetch_health_version(&addr);
return ServiceInfo {
id: id.to_string(),
display_name: display_name.to_string(),
status: ServiceStatus::Running,
version,
url: Some(base_url),
hint: None,
};
}
ServiceInfo {
id: id.to_string(),
display_name: display_name.to_string(),
status: ServiceStatus::Available,
version: None,
url: None,
hint: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_read_addr_file_returns_trimmed_content() {
let tmp = TempDir::new().expect("tempdir");
let path = tmp.path().join("http_addr");
fs::write(&path, " 127.0.0.1:9999\n").expect("write");
assert_eq!(read_addr_file(&path), Some("127.0.0.1:9999".to_string()));
}
#[test]
fn test_read_addr_file_absent_returns_none() {
let tmp = TempDir::new().expect("tempdir");
assert_eq!(read_addr_file(&tmp.path().join("no_file")), None);
}
#[test]
fn test_read_addr_file_empty_returns_none() {
let tmp = TempDir::new().expect("tempdir");
let path = tmp.path().join("http_addr");
fs::write(&path, " \n").expect("write");
assert_eq!(read_addr_file(&path), None);
}
#[test]
fn test_tcp_probe_unreachable() {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("bind free port");
let addr = listener.local_addr().expect("local_addr").to_string();
drop(listener);
assert!(!tcp_probe(&addr), "closed port must return false");
}
#[test]
fn test_tcp_probe_reachable() {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("bind free port");
let addr = listener.local_addr().expect("local_addr").to_string();
assert!(tcp_probe(&addr), "listening port must return true");
drop(listener);
}
#[test]
fn test_tcp_probe_malformed_addr() {
assert!(!tcp_probe("not-an-addr"));
}
}