Skip to main content

cargo_reclaim/scheduler/
model.rs

1use std::fmt;
2use std::path::{Path, PathBuf};
3
4use crate::PolicyKind;
5
6pub const DEFAULT_SCHEDULER_INSTANCE_NAME: &str = "cargo-reclaim";
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SchedulerPlatform {
10    SystemdUser,
11    Launchd,
12    TaskScheduler,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SchedulerMode {
17    Observe,
18    Cleanup,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Schedule {
23    pub hour: u8,
24    pub minute: u8,
25}
26
27impl Schedule {
28    pub fn parse(value: &str) -> Result<Self, SchedulerError> {
29        let Some((hour, minute)) = value.split_once(':') else {
30            return Err(SchedulerError::InvalidSchedule(value.to_string()));
31        };
32        if hour.len() != 2 || minute.len() != 2 {
33            return Err(SchedulerError::InvalidSchedule(value.to_string()));
34        }
35        let hour = hour
36            .parse::<u8>()
37            .map_err(|_| SchedulerError::InvalidSchedule(value.to_string()))?;
38        let minute = minute
39            .parse::<u8>()
40            .map_err(|_| SchedulerError::InvalidSchedule(value.to_string()))?;
41        if hour > 23 || minute > 59 {
42            return Err(SchedulerError::InvalidSchedule(value.to_string()));
43        }
44        Ok(Self { hour, minute })
45    }
46
47    pub fn as_hh_mm(self) -> String {
48        format!("{:02}:{:02}", self.hour, self.minute)
49    }
50}
51
52impl Default for Schedule {
53    fn default() -> Self {
54        Self { hour: 3, minute: 0 }
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct SchedulerRequest {
60    pub platform: SchedulerPlatform,
61    pub instance_name: String,
62    pub config_path: PathBuf,
63    pub cargo_reclaim_bin: PathBuf,
64    pub schedule: Schedule,
65    pub mode: SchedulerMode,
66    pub policy: Option<PolicyKind>,
67    pub allow_unattended_cleanup: bool,
68    pub allow_unattended_high_policy: bool,
69    pub state_dir: Option<PathBuf>,
70    pub log_dir: Option<PathBuf>,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum GeneratedArtifactKind {
75    SystemdService,
76    SystemdTimer,
77    LaunchdPlist,
78    TaskSchedulerXml,
79    RunnerScript,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct GeneratedArtifact {
84    pub kind: GeneratedArtifactKind,
85    pub intended_install_path: PathBuf,
86    pub contents: String,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct SchedulerReport {
91    pub command: &'static str,
92    pub dry_run: bool,
93    pub platform: SchedulerPlatform,
94    pub mode: SchedulerMode,
95    pub schedule: Schedule,
96    pub effective_policy: PolicyKind,
97    pub artifacts: Vec<GeneratedArtifact>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum SchedulerOperation {
102    Install,
103    Uninstall,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct SchedulerOperationPlan {
108    pub command: &'static str,
109    pub operation: SchedulerOperation,
110    pub dry_run: bool,
111    pub platform: SchedulerPlatform,
112    pub artifacts: Vec<GeneratedArtifact>,
113    pub steps: Vec<SchedulerPlanStep>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum SchedulerPlanStep {
118    EnsureDir {
119        path: PathBuf,
120    },
121    WriteFile {
122        path: PathBuf,
123        artifact_kind: GeneratedArtifactKind,
124    },
125    SetExecutable {
126        path: PathBuf,
127    },
128    RemoveFile {
129        path: PathBuf,
130    },
131    RunCommand {
132        argv: Vec<String>,
133    },
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct SchedulerExecutionReport {
138    pub command: &'static str,
139    pub operation: SchedulerOperation,
140    pub dry_run: bool,
141    pub platform: SchedulerPlatform,
142    pub artifacts: Vec<GeneratedArtifact>,
143    pub steps: Vec<SchedulerExecutionStep>,
144    pub totals: SchedulerExecutionTotals,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct SchedulerExecutionStep {
149    pub step: SchedulerPlanStep,
150    pub status: SchedulerExecutionStatus,
151    pub message: Option<String>,
152    pub command_output: Option<SchedulerCommandOutput>,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum SchedulerExecutionStatus {
157    Applied,
158    Skipped,
159    Failed,
160    Blocked,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
164pub struct SchedulerExecutionTotals {
165    pub applied: usize,
166    pub skipped: usize,
167    pub failed: usize,
168    pub blocked: usize,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct SchedulerCommandOutput {
173    pub exit_code: Option<i32>,
174    pub stdout: String,
175    pub stderr: String,
176}
177
178impl SchedulerExecutionReport {
179    pub fn succeeded(&self) -> bool {
180        self.totals.failed == 0 && self.totals.blocked == 0
181    }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub enum SchedulerError {
186    InvalidSchedule(String),
187    InvalidInstanceName(String),
188    CleanupNotAllowed,
189    HighPolicyNotAllowed(PolicyKind),
190}
191
192impl fmt::Display for SchedulerError {
193    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
194        match self {
195            Self::InvalidSchedule(value) => write!(
196                formatter,
197                "invalid scheduler time `{value}`; expected HH:MM in 24-hour time"
198            ),
199            Self::InvalidInstanceName(value) => write!(
200                formatter,
201                "invalid scheduler name `{value}`; use only ASCII letters, digits, '-', '_', or '.', and do not use path separators"
202            ),
203            Self::CleanupNotAllowed => formatter.write_str(
204                "scheduler cleanup preview requires --allow-unattended-cleanup or [scheduler].allow_unattended_cleanup = true",
205            ),
206            Self::HighPolicyNotAllowed(policy) => write!(
207                formatter,
208                "scheduler cleanup with {} policy requires an explicit policy and --allow-unattended-high-policy or [scheduler].allow_unattended_high_policy = true",
209                policy_label(*policy)
210            ),
211        }
212    }
213}
214
215impl std::error::Error for SchedulerError {}
216
217pub fn scheduler_instance_name_from_config(
218    explicit_name: Option<&str>,
219    _config_path: &Path,
220) -> Result<String, SchedulerError> {
221    if let Some(name) = explicit_name {
222        return validate_scheduler_instance_name(name).map(ToOwned::to_owned);
223    }
224
225    Ok(DEFAULT_SCHEDULER_INSTANCE_NAME.to_string())
226}
227
228pub(crate) fn validate_scheduler_instance_name(name: &str) -> Result<&str, SchedulerError> {
229    if name.is_empty()
230        || name == "."
231        || name == ".."
232        || name.contains('/')
233        || name.contains('\\')
234        || !name.chars().all(is_safe_instance_character)
235    {
236        return Err(SchedulerError::InvalidInstanceName(name.to_string()));
237    }
238    Ok(name)
239}
240
241fn is_safe_instance_character(character: char) -> bool {
242    character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')
243}
244
245pub fn policy_label(policy: PolicyKind) -> &'static str {
246    match policy {
247        PolicyKind::Observe => "observe",
248        PolicyKind::Conservative => "conservative",
249        PolicyKind::Balanced => "balanced",
250        PolicyKind::Aggressive => "aggressive",
251        PolicyKind::Custom => "custom",
252    }
253}
254
255pub fn platform_label(platform: SchedulerPlatform) -> &'static str {
256    match platform {
257        SchedulerPlatform::SystemdUser => "systemd-user",
258        SchedulerPlatform::Launchd => "launchd",
259        SchedulerPlatform::TaskScheduler => "task-scheduler",
260    }
261}
262
263pub fn mode_label(mode: SchedulerMode) -> &'static str {
264    match mode {
265        SchedulerMode::Observe => "observe",
266        SchedulerMode::Cleanup => "cleanup",
267    }
268}
269
270pub fn operation_label(operation: SchedulerOperation) -> &'static str {
271    match operation {
272        SchedulerOperation::Install => "install",
273        SchedulerOperation::Uninstall => "uninstall",
274    }
275}
276
277pub fn execution_status_label(status: SchedulerExecutionStatus) -> &'static str {
278    match status {
279        SchedulerExecutionStatus::Applied => "applied",
280        SchedulerExecutionStatus::Skipped => "skipped",
281        SchedulerExecutionStatus::Failed => "failed",
282        SchedulerExecutionStatus::Blocked => "blocked",
283    }
284}
285
286pub fn artifact_kind_label(kind: GeneratedArtifactKind) -> &'static str {
287    match kind {
288        GeneratedArtifactKind::SystemdService => "systemd-service",
289        GeneratedArtifactKind::SystemdTimer => "systemd-timer",
290        GeneratedArtifactKind::LaunchdPlist => "launchd-plist",
291        GeneratedArtifactKind::TaskSchedulerXml => "task-scheduler-xml",
292        GeneratedArtifactKind::RunnerScript => "runner-script",
293    }
294}