use std::path::{Path, PathBuf};
use std::time::Duration;
use super::CheckResult;
pub fn check_fastembed_cache() -> CheckResult {
let cache = trusty_common::embedder::resolve_fastembed_cache_dir();
let label = "fastembed cache".to_string();
if !cache.exists() {
return CheckResult::fail(
label,
format!(
"missing: {} — run `trusty-memory setup` to pre-warm",
cache.display()
),
);
}
if !cache.is_dir() {
return CheckResult::fail(label, format!("not a directory: {}", cache.display()));
}
match fastembed_cache_has_models(&cache) {
Ok(true) => CheckResult::pass(label, format!("ready at {}", cache.display())),
Ok(false) => CheckResult::warn(
label,
format!(
"{} exists but is empty — daemon will download on first request",
cache.display()
),
),
Err(e) => CheckResult::fail(label, format!("cannot read {}: {e}", cache.display())),
}
}
pub fn fastembed_cache_has_models(path: &Path) -> std::io::Result<bool> {
let mut iter = std::fs::read_dir(path)?;
Ok(iter.next().is_some())
}
#[cfg(target_os = "macos")]
pub fn check_launchd_plist() -> CheckResult {
let label = "launchd plist".to_string();
let Some(home) = dirs::home_dir() else {
return CheckResult::fail(label, "could not resolve $HOME".to_string());
};
let plist = home
.join("Library")
.join("LaunchAgents")
.join(format!("{}.plist", crate::commands::service::LAUNCHD_LABEL));
if !plist.exists() {
return CheckResult::fail(
label,
format!(
"missing: {} — run `trusty-memory service install`",
plist.display()
),
);
}
match plist_contains_fastembed_cache_path(&plist) {
Ok(true) => CheckResult::pass(label, format!("{} ok", plist.display())),
Ok(false) => CheckResult::fail(
label,
format!(
"{} is missing FASTEMBED_CACHE_PATH — reinstall via `trusty-memory service install`",
plist.display()
),
),
Err(e) => CheckResult::fail(label, format!("cannot read {}: {e}", plist.display())),
}
}
#[cfg(target_os = "macos")]
pub fn plist_contains_fastembed_cache_path(path: &Path) -> std::io::Result<bool> {
let contents = std::fs::read_to_string(path)?;
Ok(contents.contains("FASTEMBED_CACHE_PATH"))
}
pub async fn check_daemon_health() -> CheckResult {
let label = "HTTP daemon".to_string();
let client = match reqwest::Client::builder()
.timeout(Duration::from_secs(2))
.build()
{
Ok(c) => c,
Err(e) => return CheckResult::fail(label, format!("could not build HTTP client: {e}")),
};
let recorded_url = match trusty_common::read_daemon_addr("trusty-memory") {
Ok(Some(addr)) => {
let base = if addr.starts_with("http://") || addr.starts_with("https://") {
addr.clone()
} else {
format!("http://{addr}")
};
Some(base)
}
Ok(None) => None,
Err(e) => {
tracing::debug!("doctor: could not read daemon addr file: {e:#}");
None
}
};
if let Some(ref base) = recorded_url {
let url = format!("{base}/health");
match client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => {
return CheckResult::pass(label, format!("{} → {}", url, resp.status()));
}
Ok(resp) => {
tracing::debug!(
"doctor: recorded addr {url} returned {}; trying fallback ports",
resp.status()
);
}
Err(_) => {
tracing::debug!(
"doctor: recorded addr {url} unreachable (stale?); trying fallback ports"
);
}
}
}
for port in crate::DEFAULT_HTTP_PORT..=crate::DEFAULT_HTTP_PORT.saturating_add(9) {
let url = format!("http://127.0.0.1:{port}/health");
match client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => {
let note = if recorded_url.is_some() {
format!(
"{url} → {} (addr file was stale — daemon is live on fallback port {port})",
resp.status()
)
} else {
format!(
"{url} → {} (no addr file; found daemon on default port {port})",
resp.status()
)
};
return CheckResult::pass(label, note);
}
_ => continue,
}
}
if recorded_url.is_some() {
CheckResult::fail(
label,
"recorded address unreachable and no daemon found on default ports 7070-7079 \
— start with `trusty-memory service start`"
.to_string(),
)
} else {
CheckResult::fail(
label,
"no daemon address recorded and no daemon found on default ports 7070-7079 \
— start with `trusty-memory service start`"
.to_string(),
)
}
}
pub fn check_stale_palace_locks() -> CheckResult {
let label = "palace locks".to_string();
let data_dir = match trusty_common::resolve_data_dir("trusty-memory") {
Ok(d) => d,
Err(e) => return CheckResult::fail(label, format!("could not resolve data dir: {e}")),
};
let root = crate::resolve_palace_registry_dir(data_dir);
let locks = find_lock_files(&root);
if locks.is_empty() {
CheckResult::pass(label, format!("{} clean", root.display()))
} else {
let preview = locks
.iter()
.take(3)
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ");
let suffix = if locks.len() > 3 {
format!(" (+{} more)", locks.len() - 3)
} else {
String::new()
};
CheckResult::warn(
label,
format!(
"{} lock file(s) found: {preview}{suffix} — if the daemon is stopped, these can be removed",
locks.len()
),
)
}
}
pub fn find_lock_files(root: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let Ok(entries) = std::fs::read_dir(root) else {
return out;
};
for entry in entries.flatten() {
let path = entry.path();
if is_lock_file(&path) {
out.push(path.clone());
}
if path.is_dir() {
if let Ok(sub) = std::fs::read_dir(&path) {
for child in sub.flatten() {
let cpath = child.path();
if is_lock_file(&cpath) {
out.push(cpath);
}
}
}
}
}
out
}
pub(super) fn is_lock_file(path: &Path) -> bool {
path.extension().and_then(|s| s.to_str()) == Some("lock")
}