use std::process::Command;
use super::ops::{postgres_container_name, rgs_container_name};
use super::InfraContext;
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub enum InfraHealthStatus {
Healthy,
Running,
Degraded,
Down,
External,
}
#[derive(Debug, Clone)]
pub struct ServiceHealth {
pub name: String,
pub status: InfraHealthStatus,
pub detail: String,
}
pub fn check_all(ctx: &InfraContext) -> Vec<ServiceHealth> {
vec![
check_container(ctx, "postgres", &postgres_container_name(ctx)),
check_rgs_container(ctx),
]
}
fn is_running(container: &str) -> bool {
Command::new("docker")
.args(["inspect", "--format", "{{.State.Running}}", container])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim() == "true")
.unwrap_or(false)
}
fn container_health(container: &str) -> Option<String> {
let out = Command::new("docker")
.args(["inspect", "--format", "{{.State.Health.Status}}", container])
.output()
.ok()?;
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() || s == "<no value>" { None } else { Some(s) }
}
fn check_container(ctx: &InfraContext, name: &str, container: &str) -> ServiceHealth {
if !is_running(container) {
return ServiceHealth {
name: name.to_string(),
status: InfraHealthStatus::Down,
detail: format!("container {container} not running"),
};
}
match container_health(container).as_deref() {
Some("healthy") => {
let detail = if name == "postgres" {
db_summary_detail(ctx).unwrap_or_else(|| "healthy".to_string())
} else {
"healthy".to_string()
};
ServiceHealth { name: name.to_string(), status: InfraHealthStatus::Healthy, detail }
}
Some(h) => ServiceHealth {
name: name.to_string(),
status: InfraHealthStatus::Running,
detail: format!("running ({h})"),
},
None => ServiceHealth {
name: name.to_string(),
status: InfraHealthStatus::Running,
detail: "running".to_string(),
},
}
}
fn check_rgs_container(ctx: &InfraContext) -> ServiceHealth {
let container = rgs_container_name(ctx);
if !is_running(&container) {
return ServiceHealth {
name: "rgs".to_string(),
status: InfraHealthStatus::Down,
detail: format!("container {container} not running"),
};
}
let has_snapshot = Command::new("docker")
.args(["exec", &container, "test", "-s", "/res/symlinks/0.bin"])
.status()
.map(|s| s.success())
.unwrap_or(false);
if has_snapshot {
let ts = super::snapshot::fetch_snapshot_summary(ctx, None)
.ok()
.map(|s| format!("snapshot: {}", s.timestamp_str))
.unwrap_or_else(|| "snapshot ready".to_string());
ServiceHealth { name: "rgs".to_string(), status: InfraHealthStatus::Healthy, detail: format!(":8011 · {ts}") }
} else {
ServiceHealth { name: "rgs".to_string(), status: InfraHealthStatus::Running, detail: ":8011 · syncing gossip…".to_string() }
}
}
fn db_summary_detail(ctx: &InfraContext) -> Option<String> {
let s = super::db::query_summary(ctx).ok()?;
Some(format!(":5432 · {} nodes · {} channels · {} updates",
s.node_announcements, s.channel_announcements, s.channel_updates))
}
pub fn print_status(ctx: &InfraContext) {
let services = check_all(ctx);
println!("{:<12} {:<14} Details", "Service", "Status");
println!("{}", "─".repeat(60));
for svc in &services {
let label = match svc.status {
InfraHealthStatus::Healthy => "✓ healthy",
InfraHealthStatus::Running => "◐ running",
InfraHealthStatus::Degraded => "⚠ degraded",
InfraHealthStatus::Down => "✗ down",
InfraHealthStatus::External => "⚡ external",
};
println!("{:<12} {:<14} {}", svc.name, label, svc.detail);
}
}