Skip to main content

mur_common/
schedule_claim.rs

1//! Schedule claim/release — prevents double execution between CLI cron and Commander.
2//!
3//! The `executor` field in Schedule tracks who is responsible for ticking.
4//! PID file at `~/.mur/commander/commander.pid` indicates Commander is alive.
5
6use std::path::{Path, PathBuf};
7
8use crate::schedule::{Schedule, ScheduleExecutor, SchedulesFile};
9
10/// Default Commander PID file path.
11pub fn commander_pid_path() -> PathBuf {
12    dirs::home_dir()
13        .unwrap_or_default()
14        .join(".mur")
15        .join("commander")
16        .join("commander.pid")
17}
18
19/// Check if Commander daemon is currently running.
20pub fn is_commander_running() -> bool {
21    is_commander_running_at(&commander_pid_path())
22}
23
24/// Check if Commander is running given a specific PID file path.
25pub fn is_commander_running_at(pid_path: &Path) -> bool {
26    let content = match std::fs::read_to_string(pid_path) {
27        Ok(c) => c,
28        Err(_) => return false,
29    };
30
31    let pid: u32 = match content.trim().parse() {
32        Ok(p) => p,
33        Err(_) => return false,
34    };
35
36    // Same liveness question as a runtime lock file — use the same answer
37    // instead of a second hand-rolled probe. The old `#[cfg(not(unix))]` arm
38    // returned false unconditionally, so on Windows `auto_detect_executor`
39    // never chose Commander and `mur workflow` reclaimed schedules from a
40    // Commander that was very much alive.
41    crate::lock_file::pid_alive(pid)
42}
43
44/// Default schedules.yaml path.
45pub fn schedules_path() -> PathBuf {
46    dirs::home_dir()
47        .unwrap_or_default()
48        .join(".mur")
49        .join("schedules.yaml")
50}
51
52/// Load schedules from the default path.
53pub fn load_schedules() -> Result<Vec<Schedule>, Box<dyn std::error::Error>> {
54    let path = schedules_path();
55    if !path.exists() {
56        return Ok(Vec::new());
57    }
58    let content = std::fs::read_to_string(&path)?;
59    let file: SchedulesFile = serde_yaml::from_str(&content)?;
60    Ok(file.schedules)
61}
62
63/// Save schedules to the default path.
64pub fn save_schedules(schedules: &[Schedule]) -> Result<(), Box<dyn std::error::Error>> {
65    let path = schedules_path();
66    if let Some(parent) = path.parent() {
67        std::fs::create_dir_all(parent)?;
68    }
69    let file = SchedulesFile {
70        schedules: schedules.to_vec(),
71    };
72    let yaml = serde_yaml::to_string(&file)?;
73    std::fs::write(&path, yaml)?;
74    Ok(())
75}
76
77/// Claim all schedules for Commander — sets executor to Commander.
78/// Returns the list of schedules that were claimed (had executor != Commander).
79pub fn claim_all_for_commander() -> Result<Vec<String>, Box<dyn std::error::Error>> {
80    let mut schedules = load_schedules()?;
81    let mut claimed = Vec::new();
82
83    for schedule in &mut schedules {
84        if schedule.executor != ScheduleExecutor::Commander {
85            claimed.push(schedule.workflow.clone());
86            schedule.executor = ScheduleExecutor::Commander;
87        }
88    }
89
90    if !claimed.is_empty() {
91        save_schedules(&schedules)?;
92    }
93
94    Ok(claimed)
95}
96
97/// Release all schedules from Commander — sets executor back to SystemCron.
98/// Returns the list of schedules that were released.
99pub fn release_all_from_commander() -> Result<Vec<String>, Box<dyn std::error::Error>> {
100    let mut schedules = load_schedules()?;
101    let mut released = Vec::new();
102
103    for schedule in &mut schedules {
104        if schedule.executor == ScheduleExecutor::Commander {
105            released.push(schedule.workflow.clone());
106            schedule.executor = ScheduleExecutor::SystemCron;
107        }
108    }
109
110    if !released.is_empty() {
111        save_schedules(&schedules)?;
112    }
113
114    Ok(released)
115}
116
117/// Determine the appropriate executor for a new schedule.
118/// If Commander is running, use Commander. Otherwise, use SystemCron.
119pub fn auto_detect_executor() -> ScheduleExecutor {
120    if is_commander_running() {
121        ScheduleExecutor::Commander
122    } else {
123        ScheduleExecutor::SystemCron
124    }
125}