cargo-reclaim 0.2.1

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::error::Error;
use std::fmt;
use std::path::PathBuf;
use std::time::Duration;

use serde::{Deserialize, Serialize};

use crate::error::ReclaimError;
use crate::persistence::{PersistedTimestamp, PlanPersistenceError};
use crate::scheduler::{SchedulerError, SchedulerMode};

use super::super::BackgroundRunnerError;

pub const BACKGROUND_SERVICE_STATE_SCHEMA_VERSION: u16 = 1;
pub const DEFAULT_BACKGROUND_CHECK_EVERY: Duration = Duration::from_secs(60 * 60);

pub type BackgroundServiceResult<T> = Result<T, BackgroundServiceError>;

#[derive(Debug)]
pub enum BackgroundServiceError {
    Io {
        path: PathBuf,
        source: std::io::Error,
    },
    Serialize {
        path: PathBuf,
        source: serde_json::Error,
    },
    Json {
        path: PathBuf,
        source: serde_json::Error,
    },
    Timestamp(PlanPersistenceError),
    Scheduler(SchedulerError),
    Runner(BackgroundRunnerError),
    Config(String),
    AlreadyRunning {
        lock_path: PathBuf,
    },
    StaleLock {
        lock_path: PathBuf,
    },
}

impl fmt::Display for BackgroundServiceError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io { path, source } => write!(formatter, "{}: {source}", path.display()),
            Self::Serialize { path, source } => {
                write!(
                    formatter,
                    "failed to serialize {}: {source}",
                    path.display()
                )
            }
            Self::Json { path, source } => {
                write!(formatter, "failed to parse {}: {source}", path.display())
            }
            Self::Timestamp(source) => write!(formatter, "invalid service timestamp: {source}"),
            Self::Scheduler(source) => source.fmt(formatter),
            Self::Runner(source) => source.fmt(formatter),
            Self::Config(message) => formatter.write_str(message),
            Self::AlreadyRunning { lock_path } => write!(
                formatter,
                "cargo-reclaim scheduler service is already running; lock exists at {}",
                lock_path.display()
            ),
            Self::StaleLock { lock_path } => write!(
                formatter,
                "cargo-reclaim scheduler service lock exists at {}; stale-lock recovery is not automatic yet",
                lock_path.display()
            ),
        }
    }
}

impl Error for BackgroundServiceError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Io { source, .. } => Some(source),
            Self::Serialize { source, .. } | Self::Json { source, .. } => Some(source),
            Self::Timestamp(source) => Some(source),
            Self::Scheduler(source) => Some(source),
            Self::Runner(source) => Some(source),
            Self::Config(_) | Self::AlreadyRunning { .. } | Self::StaleLock { .. } => None,
        }
    }
}

impl From<SchedulerError> for BackgroundServiceError {
    fn from(error: SchedulerError) -> Self {
        Self::Scheduler(error)
    }
}

impl From<BackgroundRunnerError> for BackgroundServiceError {
    fn from(error: BackgroundRunnerError) -> Self {
        Self::Runner(error)
    }
}

impl From<ReclaimError> for BackgroundServiceError {
    fn from(error: ReclaimError) -> Self {
        Self::Config(error.to_string())
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackgroundServiceOptions {
    pub config_path: PathBuf,
    pub state_dir: PathBuf,
    pub log_dir: PathBuf,
    pub mode: Option<SchedulerMode>,
    pub max_cycles: Option<usize>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackgroundServicePaths {
    pub state_dir: PathBuf,
    pub log_dir: PathBuf,
    pub plans_dir: PathBuf,
    pub lock_path: PathBuf,
    pub state_path: PathBuf,
    pub runs_log_path: PathBuf,
}

impl BackgroundServicePaths {
    pub fn new(state_dir: impl Into<PathBuf>, log_dir: impl Into<PathBuf>) -> Self {
        let state_dir = state_dir.into();
        let log_dir = log_dir.into();
        Self {
            plans_dir: state_dir.join("plans"),
            lock_path: state_dir.join("service.lock"),
            state_path: state_dir.join("service-state.json"),
            runs_log_path: log_dir.join("runs.jsonl"),
            state_dir,
            log_dir,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackgroundServiceState {
    pub schema_version: u16,
    pub status: BackgroundServiceStatus,
    pub pid: Option<u32>,
    pub started_at: Option<PersistedTimestamp>,
    pub last_run_id: Option<String>,
    pub last_run_at: Option<PersistedTimestamp>,
    pub next_run_at: Option<PersistedTimestamp>,
    pub consecutive_failures: u32,
    pub last_problem: Option<String>,
}

impl BackgroundServiceState {
    pub fn missing() -> Self {
        Self {
            schema_version: BACKGROUND_SERVICE_STATE_SCHEMA_VERSION,
            status: BackgroundServiceStatus::Unknown,
            pid: None,
            started_at: None,
            last_run_id: None,
            last_run_at: None,
            next_run_at: None,
            consecutive_failures: 0,
            last_problem: Some("service state file does not exist".to_owned()),
        }
    }

    pub(crate) fn running(started_at: PersistedTimestamp) -> Self {
        Self {
            schema_version: BACKGROUND_SERVICE_STATE_SCHEMA_VERSION,
            status: BackgroundServiceStatus::Running,
            pid: Some(std::process::id()),
            started_at: Some(started_at),
            last_run_id: None,
            last_run_at: None,
            next_run_at: None,
            consecutive_failures: 0,
            last_problem: None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackgroundServiceStatus {
    Running,
    Stopped,
    Unknown,
    Stale,
    Error,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackgroundServiceRunSummary {
    pub state: BackgroundServiceState,
    pub cycles_completed: usize,
}