use std::path::Path;
use std::time::Duration;
use crate::doctor::check::{CheckResult, FixAction};
use crate::supervise::pid_file;
use super::Ctx;
const STALE_AGE: Duration = Duration::from_secs(24 * 60 * 60);
pub fn check(ctx: &Ctx) -> Vec<CheckResult> {
let Some(root) = ctx.root.as_deref() else {
return Vec::new();
};
let runs_dir = root.join("runs");
let entries = match std::fs::read_dir(&runs_dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Vec::new(),
Err(e) => {
return vec![CheckResult::fail(
"data.orphan-supervisor",
format!("cannot read {}: {e}", runs_dir.display()),
format!("check permissions on {}", runs_dir.display()),
)];
}
};
let mut run_ids: Vec<String> = entries
.flatten()
.filter(|e| e.file_type().is_ok_and(|t| t.is_dir()))
.filter_map(|e| e.file_name().to_str().map(str::to_string))
.collect();
run_ids.sort();
let mut out = Vec::new();
for run_id in &run_ids {
let pid_path = runs_dir.join(run_id).join("supervisor.pid");
let id = format!("data.orphan-supervisor.{run_id}");
if !pid_path.exists() {
continue;
}
let Some(pid) = pid_file::read_pid(&pid_path) else {
out.push(CheckResult::warn(
id,
format!(
"supervisor pid file {} is unreadable/unparseable",
pid_path.display()
),
format!("inspect or rm {}", pid_path.display()),
));
continue;
};
if pid_file::pid_alive(pid) {
out.push(CheckResult::ok(id, format!("supervisor pid {pid} alive")));
continue;
}
let suggestion = format!(
"orchestratectl run reattach {run_id} (or rm {})",
pid_path.display()
);
let mut result = CheckResult::warn(
id,
format!(
"supervisor pid {pid} is dead (stale {})",
pid_path.display()
),
suggestion,
);
if pid_file_is_stale(&pid_path) {
result = result.with_safe_fix(FixAction::RemoveStaleSupervisorPid {
path: pid_path,
observed_pid: pid,
});
}
out.push(result);
}
out
}
fn pid_file_is_stale(path: &Path) -> bool {
std::fs::metadata(path)
.and_then(|m| m.modified())
.ok()
.and_then(|mtime| mtime.elapsed().ok())
.is_some_and(|age| age > STALE_AGE)
}