use crate::cli::daemon_probe::{self, AddrSource, HTTP_PORT_ENV};
use crate::cli::output::OutputConfig;
use crate::cli::palace;
use crate::cli::stop;
use anyhow::Result;
#[derive(Debug, Clone)]
enum CheckResult {
Ok(String),
Warn(String, String),
Error(String, String),
}
impl CheckResult {
fn print(&self) {
match self {
CheckResult::Ok(label) => println!(" ✓ {label}"),
CheckResult::Warn(label, msg) => println!(" ⚠ {label}: {msg}"),
CheckResult::Error(label, msg) => println!(" ✗ {label}: {msg}"),
}
}
fn is_error(&self) -> bool {
matches!(self, CheckResult::Error(..))
}
fn is_warn(&self) -> bool {
matches!(self, CheckResult::Warn(..))
}
}
pub async fn handle(_out: &OutputConfig) -> Result<()> {
println!("\ntrusty-memory doctor\n");
println!("Checking configuration...\n");
let mut checks: Vec<CheckResult> = Vec::new();
checks.push(check_data_dir());
checks.push(check_palace_registry().await);
checks.push(check_daemon_running());
checks.push(check_discovery_files());
for c in &checks {
c.print();
}
let errors = checks.iter().filter(|c| c.is_error()).count();
let warnings = checks.iter().filter(|c| c.is_warn()).count();
println!();
if errors == 0 && warnings == 0 {
println!("Everything looks good!");
} else {
println!(
"Issues found: {warnings} warning{}, {errors} error{}",
if warnings == 1 { "" } else { "s" },
if errors == 1 { "" } else { "s" },
);
}
if errors > 0 {
std::process::exit(1);
}
Ok(())
}
fn check_data_dir() -> CheckResult {
let label = "data dir".to_string();
let root = match palace::data_root() {
Ok(r) => r,
Err(e) => return CheckResult::Error(label, format!("could not resolve: {e:#}")),
};
match std::fs::metadata(&root) {
Ok(m) if m.is_dir() => CheckResult::Ok(format!("data dir ({})", root.display())),
Ok(_) => CheckResult::Error(label, format!("{} is not a directory", root.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => CheckResult::Warn(
label,
format!(
"{} does not exist yet (run `trusty-memory palace new <name>` to create)",
root.display()
),
),
Err(e) => CheckResult::Error(label, format!("{} unreadable: {e}", root.display())),
}
}
async fn check_palace_registry() -> CheckResult {
let label = "palaces".to_string();
let root = match palace::data_root() {
Ok(r) => r,
Err(e) => return CheckResult::Error(label, format!("could not resolve data root: {e:#}")),
};
let palaces = tokio::task::spawn_blocking(move || {
trusty_memory_core::PalaceRegistry::list_palaces(&root)
})
.await;
match palaces {
Ok(Ok(list)) if list.is_empty() => CheckResult::Warn(
label,
"no palaces yet — create one with `trusty-memory palace new <name>`".to_string(),
),
Ok(Ok(list)) => CheckResult::Ok(format!("palaces ({} registered)", list.len())),
Ok(Err(e)) => CheckResult::Error(label, format!("registry read failed: {e:#}")),
Err(e) => CheckResult::Error(label, format!("join error: {e}")),
}
}
fn check_daemon_running() -> CheckResult {
let label = "daemon".to_string();
match daemon_probe::probe_daemon() {
Some(found) => {
let suffix = match found.source {
AddrSource::EnvVar => format!(" [via ${HTTP_PORT_ENV}]"),
AddrSource::DiscoveryFile => String::new(),
AddrSource::CandidatePort => {
" [discovery file missing/stale — found via port scan]".to_string()
}
};
CheckResult::Ok(format!(
"daemon (running at http://{}){suffix}",
found.addr
))
}
None => CheckResult::Warn(
label,
format!(
"not running on any known address (checked ${HTTP_PORT_ENV}, discovery file, and ports 3031..=3050) — start it with `trusty-memory start`"
),
),
}
}
fn check_discovery_files() -> CheckResult {
let label = "discovery files".to_string();
let pid = stop::read_pid_file();
let addr = trusty_common::read_daemon_addr("trusty-memory")
.ok()
.flatten()
.filter(|s| !s.is_empty());
let probe = daemon_probe::probe_daemon();
match (pid, addr, probe) {
(None, None, None) => {
CheckResult::Ok("discovery files (none; daemon not running)".to_string())
}
(Some(_), Some(_), _) => {
CheckResult::Ok("discovery files (PID + addr present)".to_string())
}
(Some(pid), None, _) => CheckResult::Warn(
label,
format!("stale PID file (pid {pid}) without addr file — daemon likely crashed"),
),
(None, Some(addr), _) => CheckResult::Warn(
label,
format!("addr file ({addr}) without PID file — `stop` will not find the daemon"),
),
(None, None, Some(found)) => CheckResult::Warn(
label,
format!(
"daemon answering at {} but no PID/addr file — likely launched outside this CLI; `stop` will not find it",
found.addr
),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_result_flags_are_mutually_exclusive() {
let ok = CheckResult::Ok("x".to_string());
let warn = CheckResult::Warn("x".to_string(), "y".to_string());
let err = CheckResult::Error("x".to_string(), "y".to_string());
assert!(!ok.is_error() && !ok.is_warn());
assert!(warn.is_warn() && !warn.is_error());
assert!(err.is_error() && !err.is_warn());
}
}