use anyhow::Context;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[derive(Debug, PartialEq)]
pub(crate) struct FixReport {
pub action: &'static str,
pub detail: String,
}
pub async fn clear_stuck_cron_runs(
pool: &crate::db::Pool,
max_age_secs: i64,
) -> anyhow::Result<usize> {
let cutoff = (chrono::Utc::now() - chrono::Duration::seconds(max_age_secs)).to_rfc3339();
let n = pool
.get()
.await
.context("Failed to get connection")?
.interact(move |conn| {
conn.execute(
"UPDATE cron_job_runs SET status='error', \
error='stuck: cleared by doctor --fix (no completion within max age)', \
completed_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') \
WHERE status='running' AND started_at < ?1",
[cutoff],
)
})
.await
.map_err(|_| anyhow::anyhow!("cron interact failed"))??;
Ok(n)
}
pub fn clear_stale_preinit_markers(roots: &[PathBuf], max_age: Duration) -> Vec<FixReport> {
let mut removed = Vec::new();
let cutoff = std::time::SystemTime::now()
.checked_sub(max_age)
.unwrap_or(std::time::UNIX_EPOCH);
for root in roots {
let entries = match std::fs::read_dir(root) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
if !is_preinit_marker(&path) {
continue;
}
let stale = entry
.metadata()
.ok()
.and_then(|m| m.modified().ok())
.map(|t| t < cutoff)
.unwrap_or(false);
if stale && std::fs::remove_file(&path).is_ok() {
removed.push(FixReport {
action: "stale-preinit-marker",
detail: path.display().to_string(),
});
}
}
}
removed
}
fn is_preinit_marker(path: &Path) -> bool {
path.is_file()
&& path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with(".opencrabs_plan_") && n.ends_with(".preinit"))
.unwrap_or(false)
}
#[cfg(unix)]
pub fn fix_brain_log_permissions(home: &Path) -> Vec<FixReport> {
use std::os::unix::fs::PermissionsExt;
let mut fixed = Vec::new();
for rel in ["brain", "logs"] {
let entries = match std::fs::read_dir(home.join(rel)) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let Ok(meta) = entry.metadata() else {
continue;
};
let mode = meta.permissions().mode();
if mode & 0o077 != 0
&& std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).is_ok()
{
fixed.push(FixReport {
action: "permissions-tightened",
detail: path.display().to_string(),
});
}
}
}
fixed
}
pub const STUCK_CRON_MAX_AGE_SECS: i64 = 3600;
pub const PREINIT_MAX_AGE: Duration = Duration::from_secs(7 * 24 * 3600);
#[cfg_attr(not(unix), allow(unused_variables))]
pub async fn run_all(
pool: &crate::db::Pool,
marker_roots: &[PathBuf],
home: &Path,
) -> anyhow::Result<Vec<FixReport>> {
let mut reports = Vec::new();
let stuck = clear_stuck_cron_runs(pool, STUCK_CRON_MAX_AGE_SECS).await?;
if stuck > 0 {
reports.push(FixReport {
action: "stuck-cron-rows-cleared",
detail: format!("{stuck} row(s) marked error"),
});
}
reports.extend(clear_stale_preinit_markers(marker_roots, PREINIT_MAX_AGE));
#[cfg(unix)]
reports.extend(fix_brain_log_permissions(home));
Ok(reports)
}