use std::fmt;
use std::io::{Read, Write};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream};
use std::path::PathBuf;
use std::process::{Command as ProcessCommand, Stdio};
use std::time::Duration;
use clap::{Parser, Subcommand, ValueEnum};
use crate::config::{Config, ConfigError};
#[cfg(target_os = "windows")]
use crate::service::ServiceError;
use crate::startup::StartupMethodArg;
#[derive(Parser)]
#[command(
name = "greggd",
version,
about = "Lightweight Linux, macOS, and Windows metrics daemon",
long_about = "greggd runs on designated systems and exposes a read-only JSON API \
for the gregg terminal client. It samples CPU, memory, swap, and \
load metrics on a configurable interval and serves cached immutable \
snapshots over HTTP/1."
)]
pub struct Cli {
#[arg(
long,
short = 'c',
global = true,
help = "Path to the TOML configuration file",
value_name = "PATH"
)]
pub config: Option<PathBuf>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
Run,
Stop,
#[cfg(target_os = "windows")]
Start,
#[allow(clippy::doc_markdown)]
Restart,
Startup {
#[command(subcommand)]
command: StartupCommand,
},
Croncheck,
Configprint,
Status,
Host {
address: IpAddr,
},
Port {
port: u16,
},
Version,
Update,
#[cfg(target_os = "windows")]
#[command(hide = true)]
Service,
}
#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum StartupCommand {
Install {
#[arg(long, value_enum, default_value_t = StartupMethodArg::Auto, value_name = "METHOD")]
method: StartupMethodArg,
},
Instructions {
#[arg(long, value_enum, default_value_t = StartupMethodArg::Auto, value_name = "METHOD")]
method: StartupMethodArg,
},
}
impl ValueEnum for StartupMethodArg {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Auto, Self::Systemd, Self::Launchd, Self::Cron]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(match self {
Self::Auto => clap::builder::PossibleValue::new("auto"),
Self::Systemd => clap::builder::PossibleValue::new("systemd"),
Self::Launchd => clap::builder::PossibleValue::new("launchd"),
Self::Cron => clap::builder::PossibleValue::new("cron"),
})
}
}
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitCode {
Success = 0,
ConfigError = 1,
ServiceError = 2,
RuntimeError = 3,
PermissionDenied = 4,
}
impl From<&ConfigError> for ExitCode {
fn from(e: &ConfigError) -> Self {
match e {
ConfigError::Io { source, .. }
if source.kind() == std::io::ErrorKind::PermissionDenied =>
{
Self::PermissionDenied
}
ConfigError::AtomicWrite { source, .. } => match source {
crate::config::AtomicWriteError::Io(io)
if io.kind() == std::io::ErrorKind::PermissionDenied =>
{
Self::PermissionDenied
}
_ => Self::ConfigError,
},
_ => Self::ConfigError,
}
}
}
#[cfg(target_os = "windows")]
impl From<&ServiceError> for ExitCode {
fn from(e: &ServiceError) -> Self {
match e {
ServiceError::CommandFailed { .. }
| ServiceError::ExecFailed { .. }
| ServiceError::NotAvailable { .. }
| ServiceError::StateQueryFailed { .. }
| ServiceError::Timeout { .. } => Self::ServiceError,
ServiceError::AccessDenied => Self::PermissionDenied,
}
}
}
impl From<&crate::startup::InstallError> for ExitCode {
fn from(e: &crate::startup::InstallError) -> Self {
match e {
crate::startup::InstallError::Permission { .. } => Self::PermissionDenied,
crate::startup::InstallError::Io { source, .. }
if source.kind() == std::io::ErrorKind::PermissionDenied =>
{
Self::PermissionDenied
}
crate::startup::InstallError::BinaryMissing { .. }
| crate::startup::InstallError::UnsupportedMethod { .. } => Self::ConfigError,
_ => Self::ServiceError,
}
}
}
impl From<&crate::update::UpdateError> for ExitCode {
fn from(e: &crate::update::UpdateError) -> Self {
match e {
crate::update::UpdateError::PermissionDenied { .. } => Self::PermissionDenied,
_ => Self::RuntimeError,
}
}
}
pub fn resolve_config_path(explicit: Option<&PathBuf>) -> PathBuf {
explicit.cloned().unwrap_or_else(Config::default_path)
}
pub fn load_config(path: &std::path::Path, explicit: bool) -> Result<Config, ConfigError> {
match std::fs::metadata(path) {
Ok(_) => Config::load(path),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
if explicit {
Err(ConfigError::Io {
path: path.to_path_buf(),
source: error,
})
} else {
Ok(Config::default())
}
}
Err(source) => Err(ConfigError::Io {
path: path.to_path_buf(),
source,
}),
}
}
#[derive(Debug)]
pub struct ConfigValidationError(pub Vec<crate::config::ConfigViolation>);
impl fmt::Display for ConfigValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "configuration validation failed:")?;
for v in &self.0 {
write!(f, "\n - {v}")?;
}
Ok(())
}
}
impl std::error::Error for ConfigValidationError {}
pub fn mutate_config(
path: &std::path::Path,
explicit: bool,
mutate: impl FnOnce(&mut Config),
) -> Result<(), Box<dyn std::error::Error>> {
let mut config = load_config(path, explicit)?;
mutate(&mut config);
let violations = config.validate();
if !violations.is_empty() {
return Err(Box::new(ConfigValidationError(violations)));
}
config.write_atomic(path)?;
Ok(())
}
#[must_use]
pub fn version_string() -> String {
format!("greggd {}", env!("CARGO_PKG_VERSION"))
}
#[must_use]
pub fn probe_address(address: IpAddr) -> IpAddr {
match address {
IpAddr::V4(value) if value.is_unspecified() => IpAddr::V4(Ipv4Addr::LOCALHOST),
IpAddr::V6(value) if value.is_unspecified() => IpAddr::V6(Ipv6Addr::LOCALHOST),
value => value,
}
}
#[must_use]
pub fn croncheck_target(config: &Config) -> SocketAddr {
SocketAddr::new(probe_address(config.host), config.port)
}
#[must_use]
pub fn config_address(config: &Config) -> SocketAddr {
SocketAddr::new(config.host, config.port)
}
#[must_use]
pub fn display_address(config: &Config) -> SocketAddr {
display_address_from(
config,
crate::net::local_ipv4_address,
crate::net::local_ipv6_address,
)
}
#[must_use]
#[allow(clippy::module_name_repetitions)]
pub fn display_address_from(
config: &Config,
local_ipv4: fn() -> Option<IpAddr>,
local_ipv6: fn() -> Option<IpAddr>,
) -> SocketAddr {
let host = match config.host {
IpAddr::V4(value) if value.is_unspecified() => local_ipv4().unwrap_or(config.host),
IpAddr::V6(value) if value.is_unspecified() => {
local_ipv6().or_else(local_ipv4).unwrap_or(config.host)
}
other => other,
};
SocketAddr::new(host, config.port)
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum CroncheckProbe {
Running,
Absent,
Ambiguous,
}
const CRONCHECK_TIMEOUT: Duration = Duration::from_millis(750);
const MAX_CRONCHECK_RESPONSE_BYTES: usize = 256 * 1024;
fn classify_health_response(response: &[u8]) -> Option<gregg_protocol::ReadinessState> {
let header_end = response
.windows(4)
.position(|window| window == b"\r\n\r\n")?;
let headers = &response[..header_end];
let body = &response[header_end + 4..];
let status_line = headers.split(|byte| *byte == 10).next()?;
let mut status_parts = status_line.split(|byte| *byte == 32 || *byte == 13);
let version = status_parts.next()?;
let status = status_parts
.next()
.and_then(|value| std::str::from_utf8(value).ok())
.and_then(|value| value.parse::<u16>().ok())?;
if version != b"HTTP/1.0" && version != b"HTTP/1.1" {
return None;
}
let Ok(health) = serde_json::from_slice::<gregg_protocol::v2::HealthResponseV2>(body) else {
return None;
};
match (status, health.state) {
(200, gregg_protocol::ReadinessState::Ready) => Some(gregg_protocol::ReadinessState::Ready),
(503, gregg_protocol::ReadinessState::Warming) => {
Some(gregg_protocol::ReadinessState::Warming)
}
(503, gregg_protocol::ReadinessState::Failed) => {
Some(gregg_protocol::ReadinessState::Failed)
}
_ => None,
}
}
#[derive(Debug, PartialEq, Eq)]
enum FetchOutcome {
Refused,
Failed,
Responded(Vec<u8>),
}
fn fetch_health_bytes(target: SocketAddr) -> FetchOutcome {
let mut stream = match TcpStream::connect_timeout(&target, CRONCHECK_TIMEOUT) {
Ok(stream) => stream,
Err(error) if error.kind() == std::io::ErrorKind::ConnectionRefused => {
return FetchOutcome::Refused
}
Err(_) => return FetchOutcome::Failed,
};
let _ = stream.set_read_timeout(Some(CRONCHECK_TIMEOUT));
let _ = stream.set_write_timeout(Some(CRONCHECK_TIMEOUT));
if stream
.write_all(b"GET /v2/healthz HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
.is_err()
{
return FetchOutcome::Failed;
}
let mut response = Vec::new();
let mut chunk = [0_u8; 4096];
loop {
match stream.read(&mut chunk) {
Ok(0) => break,
Ok(read) => {
if response.len().saturating_add(read) > MAX_CRONCHECK_RESPONSE_BYTES {
return FetchOutcome::Failed;
}
response.extend_from_slice(&chunk[..read]);
}
Err(_) => return FetchOutcome::Failed,
}
}
FetchOutcome::Responded(response)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthProbe {
Ready,
Warming,
Failed,
Unreachable,
NotGregg,
}
pub(crate) fn probe_health(target: SocketAddr) -> HealthProbe {
match fetch_health_bytes(target) {
FetchOutcome::Refused | FetchOutcome::Failed => HealthProbe::Unreachable,
FetchOutcome::Responded(bytes) => match classify_health_response(&bytes) {
Some(gregg_protocol::ReadinessState::Ready) => HealthProbe::Ready,
Some(gregg_protocol::ReadinessState::Warming) => HealthProbe::Warming,
Some(gregg_protocol::ReadinessState::Failed) => HealthProbe::Failed,
None => HealthProbe::NotGregg,
},
}
}
pub(crate) fn probe_greggd(target: SocketAddr) -> CroncheckProbe {
match fetch_health_bytes(target) {
FetchOutcome::Refused => CroncheckProbe::Absent,
FetchOutcome::Responded(bytes) if classify_health_response(&bytes).is_some() => {
CroncheckProbe::Running
}
FetchOutcome::Responded(_) | FetchOutcome::Failed => CroncheckProbe::Ambiguous,
}
}
pub(crate) fn build_daemon_command(
config_path: &std::path::Path,
explicit: bool,
) -> std::io::Result<ProcessCommand> {
let exe = std::env::current_exe()?;
Ok(build_daemon_command_for(&exe, config_path, explicit))
}
pub(crate) fn build_daemon_command_for(
exe: &std::path::Path,
config_path: &std::path::Path,
explicit: bool,
) -> ProcessCommand {
let mut cmd = ProcessCommand::new(exe);
cmd.arg("run");
if explicit {
cmd.arg("--config").arg(config_path);
}
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
cmd
}
pub fn dispatch(
command: &Command,
config_path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
let explicit = !matches!(
std::fs::metadata(config_path),
Err(error) if error.kind() == std::io::ErrorKind::NotFound
);
dispatch_with_config_intent(command, config_path, explicit)
}
#[allow(clippy::too_many_lines)]
pub fn dispatch_with_config_intent(
command: &Command,
config_path: &std::path::Path,
explicit: bool,
) -> Result<(), Box<dyn std::error::Error>> {
match command {
Command::Run => {
unreachable!("Command::Run is handled in main.rs")
}
Command::Stop => {
#[cfg(unix)]
{
unreachable!("Command::Stop is handled at the binary boundary on Unix")
}
#[cfg(not(unix))]
{
unreachable!("Command::Stop is handled at the binary boundary on Windows")
}
}
Command::Croncheck => {
let config = load_config(config_path, explicit)?;
let target = croncheck_target(&config);
match probe_greggd(target) {
CroncheckProbe::Running => Ok(()),
CroncheckProbe::Absent => {
build_daemon_command(config_path, explicit)?.spawn()?;
Ok(())
}
CroncheckProbe::Ambiguous => Err(Box::new(std::io::Error::other(
"croncheck could not prove greggd is absent or healthy",
))),
}
}
Command::Configprint => {
let config = load_config(config_path, explicit)?;
println!("{}", display_address(&config));
Ok(())
}
Command::Status => {
let config = load_config(config_path, explicit)?;
let report = crate::status::gather_status(
&config,
config_path,
version_string(),
probe_health,
crate::startup::startup_state(),
);
print!("{}", crate::status::render_status(&report));
if crate::status::status_is_present(&report) {
Ok(())
} else {
Err(Box::new(std::io::Error::other(format!(
"greggd status: configured endpoint is {} (health: {})",
crate::status::status_outcome(&report),
crate::status::health_token(report.health),
))) as Box<dyn std::error::Error>)
}
}
Command::Host { address } => mutate_config(config_path, explicit, |config| {
config.host = *address;
}),
Command::Port { port } => mutate_config(config_path, explicit, |config| {
config.port = *port;
}),
Command::Version => {
println!("{}", version_string());
Ok(())
}
Command::Update => {
let outcome = crate::update::run_update(config_path, explicit)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
match outcome {
crate::update::UpdateOutcome::AlreadyCurrent { .. }
| crate::update::UpdateOutcome::UpdatedBinary { .. }
| crate::update::UpdateOutcome::UpdatedFromCargo { .. } => {
println!("{outcome}");
Ok(())
}
crate::update::UpdateOutcome::UpdatedButRestartFailed { .. } => {
eprintln!("{outcome}");
Err(Box::new(crate::update::UpdateError::RestartFailed(
outcome.to_string(),
)) as Box<dyn std::error::Error>)
}
}
}
Command::Restart => {
#[cfg(target_os = "windows")]
{
unreachable!("Windows service commands are dispatched at the binary boundary")
}
#[cfg(not(target_os = "windows"))]
{
let exe = std::env::current_exe()?;
crate::startup::restart_daemon(&exe, config_path, explicit)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
Ok(())
}
}
Command::Startup { command } => match command {
StartupCommand::Install { method } => {
let exe = std::env::current_exe()?;
crate::startup::install_startup(&exe, config_path, explicit, *method)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
Ok(())
}
StartupCommand::Instructions { method } => {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("greggd"));
let resolved = crate::startup::resolve_startup_method(*method);
let text =
crate::startup::render_instructions(resolved, &exe, config_path, explicit);
println!("{text}");
Ok(())
}
},
#[cfg(target_os = "windows")]
Command::Start => {
unreachable!("Windows service commands are dispatched at the binary boundary")
}
#[cfg(target_os = "windows")]
Command::Service => {
unreachable!("Command::Service is handled in main.rs")
}
}
}
#[cfg(all(test, not(target_os = "windows")))]
mod native_tests {
use super::*;
use clap::Parser;
use std::net::TcpListener;
#[test]
fn parser_accepts_run_stop_croncheck_mutations_and_version_but_not_windows_lifecycle() {
for args in [
"run",
"stop",
"croncheck",
"configprint",
"version",
"restart",
] {
let argv = if args == "host" {
vec!["greggd", "host", "127.0.0.1"]
} else if args == "port" {
vec!["greggd", "port", "11310"]
} else {
vec!["greggd", args]
};
assert!(Cli::try_parse_from(argv).is_ok(), "failed to parse {args}");
}
assert!(Cli::try_parse_from(["greggd", "host", "127.0.0.1"]).is_ok());
assert!(Cli::try_parse_from(["greggd", "port", "11310"]).is_ok());
assert!(Cli::try_parse_from(["greggd", "startup", "install"]).is_ok());
assert!(
Cli::try_parse_from(["greggd", "startup", "install", "--method", "systemd"]).is_ok()
);
assert!(Cli::try_parse_from(["greggd", "startup", "install", "--method", "cron"]).is_ok());
assert!(Cli::try_parse_from(["greggd", "startup", "instructions"]).is_ok());
assert!(
Cli::try_parse_from(["greggd", "startup", "instructions", "--method", "launchd"])
.is_ok()
);
assert!(
Cli::try_parse_from(["greggd", "croncheck", "--target", "192.168.182.143:11310"])
.is_err()
);
{
let command = "start";
assert!(Cli::try_parse_from(["greggd", command]).is_err());
}
}
#[test]
fn wildcard_probe_addresses_use_loopback() {
assert_eq!(
probe_address("0.0.0.0".parse::<IpAddr>().unwrap()),
"127.0.0.1".parse::<IpAddr>().unwrap()
);
assert_eq!(
probe_address("::".parse::<IpAddr>().unwrap()),
"::1".parse::<IpAddr>().unwrap()
);
assert_eq!(
probe_address("192.0.2.1".parse::<IpAddr>().unwrap()),
"192.0.2.1".parse::<IpAddr>().unwrap()
);
}
#[test]
fn config_address_preserves_wildcards_and_formats_ipv6() {
let mut config = Config::default();
assert_eq!(config_address(&config).to_string(), "0.0.0.0:11310");
config.host = "fd00::10".parse().unwrap();
config.port = 11320;
assert_eq!(config_address(&config).to_string(), "[fd00::10]:11320");
}
#[test]
fn display_address_preserves_specific_hosts() {
let config = Config {
host: "192.168.182.143".parse().unwrap(),
port: 11310,
..Config::default()
};
assert_eq!(
display_address_from(&config, || None, || None).to_string(),
"192.168.182.143:11310"
);
let config = Config {
host: "fd00::10".parse().unwrap(),
port: 11320,
..Config::default()
};
assert_eq!(
display_address_from(&config, || None, || None).to_string(),
"[fd00::10]:11320"
);
}
#[test]
fn display_address_resolves_ipv4_wildcard_to_local_ipv4() {
let config = Config {
host: "0.0.0.0".parse().unwrap(),
..Config::default()
};
let resolved = display_address_from(
&config,
|| Some("192.168.182.143".parse().unwrap()),
|| None,
);
assert_eq!(resolved.to_string(), "192.168.182.143:11310");
}
#[test]
fn display_address_resolves_ipv6_wildcard_to_local_ipv6() {
let config = Config {
host: "::".parse().unwrap(),
..Config::default()
};
let resolved = display_address_from(&config, || None, || Some("fd00::10".parse().unwrap()));
assert_eq!(resolved.to_string(), "[fd00::10]:11310");
}
#[test]
fn display_address_falls_back_from_ipv6_to_ipv4_wildcard() {
let config = Config {
host: "::".parse().unwrap(),
..Config::default()
};
let resolved = display_address_from(
&config,
|| Some("192.168.182.143".parse().unwrap()),
|| None,
);
assert_eq!(resolved.to_string(), "192.168.182.143:11310");
}
#[test]
fn display_address_preserves_wildcard_when_no_local_address() {
let config = Config {
host: "0.0.0.0".parse().unwrap(),
..Config::default()
};
let resolved = display_address_from(&config, || None, || None);
assert_eq!(resolved.to_string(), "0.0.0.0:11310");
let config = Config {
host: "::".parse().unwrap(),
..Config::default()
};
let resolved = display_address_from(&config, || None, || None);
assert_eq!(resolved.to_string(), "[::]:11310");
}
#[test]
fn display_address_prefers_configured_loopback_over_resolved_address() {
let config = Config {
host: "127.0.0.1".parse().unwrap(),
..Config::default()
};
let resolved = display_address_from(
&config,
|| Some("192.168.182.143".parse().unwrap()),
|| None,
);
assert_eq!(resolved.to_string(), "127.0.0.1:11310");
}
#[test]
fn configprint_uses_default_for_missing_implicit_config() {
let path =
std::env::temp_dir().join(format!("greggd-configprint-{}.toml", std::process::id()));
let _ = std::fs::remove_file(&path);
dispatch_with_config_intent(&Command::Configprint, &path, false).unwrap();
assert!(!path.exists());
}
#[test]
fn configprint_rejects_missing_explicit_config() {
let path = std::env::temp_dir().join(format!(
"greggd-configprint-missing-{}.toml",
std::process::id(),
));
let _ = std::fs::remove_file(&path);
assert!(dispatch_with_config_intent(&Command::Configprint, &path, true).is_err());
}
#[test]
fn load_config_propagates_non_not_found_metadata_errors() {
let dir = std::env::temp_dir().join(format!("greggd-config-error-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let blocker = dir.join("blocker");
std::fs::write(&blocker, b"not a directory").unwrap();
let path = blocker.join("config.toml");
let result = load_config(&path, false);
assert!(matches!(
result,
Err(ConfigError::Io { source, .. })
if source.kind() == std::io::ErrorKind::NotADirectory
));
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn version_string_uses_package_version() {
assert_eq!(
version_string(),
format!("greggd {}", env!("CARGO_PKG_VERSION"))
);
}
#[test]
fn config_mutation_is_persisted_without_service_dispatch() {
let dir = std::env::temp_dir().join("greggd_native_mutation_test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
mutate_config(&path, false, |config| config.port = 11320).unwrap();
assert_eq!(Config::load(&path).unwrap().port, 11320);
let _ = std::fs::remove_dir_all(dir);
}
fn http_fixture(status: u16, body: &str) -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let target = listener.local_addr().unwrap();
let body = body.to_string();
std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut request = [0_u8; 1024];
let _ = stream.read(&mut request);
let response = format!("HTTP/1.1 {status} Test\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len());
let _ = stream.write_all(response.as_bytes());
}
});
target
}
fn silent_fixture() -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let target = listener.local_addr().unwrap();
std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut request = [0_u8; 1024];
let _ = stream.read(&mut request);
std::thread::sleep(Duration::from_secs(2));
}
});
target
}
fn unbound_loopback() -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap()
}
#[test]
fn croncheck_accepts_a_warming_health_response() {
let target = http_fixture(
503,
r#"{"schema_version":2,"state":"warming","category":"warming","message":"warming"}"#,
);
assert_eq!(probe_greggd(target), CroncheckProbe::Running);
}
#[test]
fn croncheck_refuses_a_closed_port() {
let target = unbound_loopback();
assert_eq!(probe_greggd(target), CroncheckProbe::Absent);
}
#[test]
fn croncheck_accepts_a_failed_health_response() {
let target = http_fixture(
503,
r#"{"schema_version":2,"state":"failed","category":"collector_failure","message":"failed"}"#,
);
assert_eq!(probe_greggd(target), CroncheckProbe::Running);
}
#[test]
fn croncheck_rejects_unrelated_http() {
let target = http_fixture(200, r#"{"ok":true}"#);
assert_eq!(probe_greggd(target), CroncheckProbe::Ambiguous);
}
#[test]
fn croncheck_rejects_malformed_health() {
let target = http_fixture(200, "not-json");
assert_eq!(probe_greggd(target), CroncheckProbe::Ambiguous);
}
#[test]
fn croncheck_rejects_silent_peer_within_bound() {
let target = silent_fixture();
assert_eq!(probe_greggd(target), CroncheckProbe::Ambiguous);
}
#[test]
fn croncheck_dispatch_exits_when_greggd_is_running_without_spawning() {
let target = http_fixture(
503,
r#"{"schema_version":2,"state":"failed","category":"collector_failure","message":"failed"}"#,
);
let dir = std::env::temp_dir().join("greggd_croncheck_listener_up_test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("greggd.toml");
std::fs::write(
&path,
format!(
"name = \"loopback-croncheck-test\"\n\
host = \"127.0.0.1\"\n\
port = {}\n\
sample_interval_ms = 1000\n\
stale_after_ms = 10000\n",
target.port()
),
)
.unwrap();
dispatch_with_config_intent(&Command::Croncheck, &path, true).unwrap();
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn build_daemon_command_includes_run_and_explicit_config() {
let dir = std::env::temp_dir().join("greggd_build_daemon_explicit_test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
let cmd = build_daemon_command(&path, true).unwrap();
assert_eq!(
cmd.get_program(),
std::env::current_exe().unwrap().as_os_str()
);
let args: Vec<std::ffi::OsString> =
cmd.get_args().map(std::ffi::OsStr::to_os_string).collect();
assert_eq!(
args,
vec![
std::ffi::OsString::from("run"),
std::ffi::OsString::from("--config"),
path.as_os_str().to_os_string(),
]
);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn build_daemon_command_omits_config_when_implicit() {
let cmd = build_daemon_command(std::path::Path::new("/nonexistent.toml"), false).unwrap();
assert_eq!(
cmd.get_program(),
std::env::current_exe().unwrap().as_os_str()
);
let args: Vec<std::ffi::OsString> =
cmd.get_args().map(std::ffi::OsStr::to_os_string).collect();
assert_eq!(args, vec![std::ffi::OsString::from("run")]);
}
}