use super::daemon_utils::daemon_base_url;
use super::doctor_checks::{
check_daemon_running, check_data_dir, check_lock_file, check_log_rotation, check_model_cache,
check_port_reachable, doctor_data_dir, fetch_index_names, fetch_index_statuses,
print_index_breakdown, probe_daemon_health, read_daemon_port, summarize_indexes, CheckResult,
EmptyIndex,
};
use async_trait::async_trait;
use std::sync::Mutex;
#[async_trait]
pub(crate) trait DoctorCheck: Send + Sync {
#[allow(dead_code)]
fn name(&self) -> &str;
async fn run(&self, state: &DoctorState) -> Vec<CheckResult>;
}
pub(crate) struct DoctorState {
pub client: reqwest::Client,
pub base: String,
pub port: u16,
pub data_dir: std::path::PathBuf,
daemon_running: Mutex<bool>,
daemon_version: Mutex<String>,
empty_indexes: Mutex<Vec<EmptyIndex>>,
}
impl DoctorState {
fn new(client: reqwest::Client) -> Self {
Self {
client,
base: daemon_base_url(),
port: read_daemon_port(),
data_dir: doctor_data_dir(),
daemon_running: Mutex::new(false),
daemon_version: Mutex::new(String::new()),
empty_indexes: Mutex::new(Vec::new()),
}
}
fn set_daemon_health(&self, running: bool, version: String) {
*self.daemon_running.lock().expect("doctor state poisoned") = running;
*self.daemon_version.lock().expect("doctor state poisoned") = version;
}
fn daemon_running(&self) -> bool {
*self.daemon_running.lock().expect("doctor state poisoned")
}
#[allow(dead_code)]
fn daemon_version(&self) -> String {
self.daemon_version
.lock()
.expect("doctor state poisoned")
.clone()
}
fn push_empty_indexes(&self, mut items: Vec<EmptyIndex>) {
self.empty_indexes
.lock()
.expect("doctor state poisoned")
.append(&mut items);
}
fn take_empty_indexes(&self) -> Vec<EmptyIndex> {
std::mem::take(&mut *self.empty_indexes.lock().expect("doctor state poisoned"))
}
}
pub(crate) struct DaemonHealthCheck;
#[async_trait]
impl DoctorCheck for DaemonHealthCheck {
fn name(&self) -> &str {
"daemon_health"
}
async fn run(&self, state: &DoctorState) -> Vec<CheckResult> {
let (running, version) = probe_daemon_health(&state.client, &state.base).await;
state.set_daemon_health(running, version.clone());
vec![check_daemon_running(running, &state.base, &version)]
}
}
pub(crate) struct ModelCacheCheck;
#[async_trait]
impl DoctorCheck for ModelCacheCheck {
fn name(&self) -> &str {
"model_cache"
}
async fn run(&self, _state: &DoctorState) -> Vec<CheckResult> {
vec![check_model_cache()]
}
}
pub(crate) struct DataDirCheck;
#[async_trait]
impl DoctorCheck for DataDirCheck {
fn name(&self) -> &str {
"data_dir"
}
async fn run(&self, state: &DoctorState) -> Vec<CheckResult> {
vec![check_data_dir(&state.data_dir)]
}
}
pub(crate) struct LockFileCheck;
#[async_trait]
impl DoctorCheck for LockFileCheck {
fn name(&self) -> &str {
"lock_file"
}
async fn run(&self, state: &DoctorState) -> Vec<CheckResult> {
vec![check_lock_file(&state.data_dir, state.daemon_running())]
}
}
pub(crate) struct IndexesCheck;
#[async_trait]
impl DoctorCheck for IndexesCheck {
fn name(&self) -> &str {
"indexes"
}
async fn run(&self, state: &DoctorState) -> Vec<CheckResult> {
if !state.daemon_running() {
return vec![CheckResult::Warn(
"Indexes: skipped (daemon not running)".into(),
)];
}
let names = fetch_index_names(&state.client, &state.base).await;
if names.is_empty() {
return vec![CheckResult::Warn(
"No indexes registered — run `trusty-search index` to add a project".into(),
)];
}
let per_index = fetch_index_statuses(&state.client, &state.base, &names).await;
let zero_count = per_index
.iter()
.filter(|(_, b)| b.get("chunk_count").and_then(|v| v.as_u64()).unwrap_or(0) == 0)
.count();
let summary = summarize_indexes(per_index.len(), zero_count);
let mut empty_buf: Vec<EmptyIndex> = Vec::new();
print_index_breakdown(&per_index, &mut empty_buf);
state.push_empty_indexes(empty_buf);
vec![summary]
}
}
pub(crate) struct PortReachableCheck;
#[async_trait]
impl DoctorCheck for PortReachableCheck {
fn name(&self) -> &str {
"port_reachable"
}
async fn run(&self, state: &DoctorState) -> Vec<CheckResult> {
vec![check_port_reachable(state.port).await]
}
}
pub(crate) struct LogRotationCheck;
#[async_trait]
impl DoctorCheck for LogRotationCheck {
fn name(&self) -> &str {
"log_rotation"
}
async fn run(&self, _state: &DoctorState) -> Vec<CheckResult> {
vec![check_log_rotation()]
}
}
fn default_checks() -> Vec<Box<dyn DoctorCheck>> {
vec![
Box::new(DaemonHealthCheck),
Box::new(ModelCacheCheck),
Box::new(DataDirCheck),
Box::new(LockFileCheck),
Box::new(IndexesCheck),
Box::new(PortReachableCheck),
Box::new(LogRotationCheck),
]
}
pub(crate) async fn run_doctor_checks() -> (Vec<CheckResult>, Vec<EmptyIndex>) {
let client = match trusty_common::server::daemon_http_client() {
Ok(c) => c,
Err(e) => {
return (
vec![CheckResult::Error(format!(
"failed to build HTTP client: {e}"
))],
Vec::new(),
);
}
};
let state = DoctorState::new(client);
let mut checks: Vec<CheckResult> = Vec::new();
for check in default_checks() {
checks.extend(check.run(&state).await);
}
(checks, state.take_empty_indexes())
}