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: i32 = match content.trim().parse() {
32        Ok(p) => p,
33        Err(_) => return false,
34    };
35
36    // Check if process is alive (signal 0 = existence check)
37    #[cfg(unix)]
38    {
39        unsafe { libc::kill(pid, 0) == 0 }
40    }
41    #[cfg(not(unix))]
42    {
43        // On non-Unix, assume Commander is not running (best effort)
44        false
45    }
46}
47
48/// Default schedules.yaml path.
49pub fn schedules_path() -> PathBuf {
50    dirs::home_dir()
51        .unwrap_or_default()
52        .join(".mur")
53        .join("schedules.yaml")
54}
55
56/// Load schedules from the default path.
57pub fn load_schedules() -> Result<Vec<Schedule>, Box<dyn std::error::Error>> {
58    let path = schedules_path();
59    if !path.exists() {
60        return Ok(Vec::new());
61    }
62    let content = std::fs::read_to_string(&path)?;
63    let file: SchedulesFile = serde_yaml::from_str(&content)?;
64    Ok(file.schedules)
65}
66
67/// Save schedules to the default path.
68pub fn save_schedules(schedules: &[Schedule]) -> Result<(), Box<dyn std::error::Error>> {
69    let path = schedules_path();
70    if let Some(parent) = path.parent() {
71        std::fs::create_dir_all(parent)?;
72    }
73    let file = SchedulesFile {
74        schedules: schedules.to_vec(),
75    };
76    let yaml = serde_yaml::to_string(&file)?;
77    std::fs::write(&path, yaml)?;
78    Ok(())
79}
80
81/// Claim all schedules for Commander — sets executor to Commander.
82/// Returns the list of schedules that were claimed (had executor != Commander).
83pub fn claim_all_for_commander() -> Result<Vec<String>, Box<dyn std::error::Error>> {
84    let mut schedules = load_schedules()?;
85    let mut claimed = Vec::new();
86
87    for schedule in &mut schedules {
88        if schedule.executor != ScheduleExecutor::Commander {
89            claimed.push(schedule.workflow.clone());
90            schedule.executor = ScheduleExecutor::Commander;
91        }
92    }
93
94    if !claimed.is_empty() {
95        save_schedules(&schedules)?;
96    }
97
98    Ok(claimed)
99}
100
101/// Release all schedules from Commander — sets executor back to SystemCron.
102/// Returns the list of schedules that were released.
103pub fn release_all_from_commander() -> Result<Vec<String>, Box<dyn std::error::Error>> {
104    let mut schedules = load_schedules()?;
105    let mut released = Vec::new();
106
107    for schedule in &mut schedules {
108        if schedule.executor == ScheduleExecutor::Commander {
109            released.push(schedule.workflow.clone());
110            schedule.executor = ScheduleExecutor::SystemCron;
111        }
112    }
113
114    if !released.is_empty() {
115        save_schedules(&schedules)?;
116    }
117
118    Ok(released)
119}
120
121/// Determine the appropriate executor for a new schedule.
122/// If Commander is running, use Commander. Otherwise, use SystemCron.
123pub fn auto_detect_executor() -> ScheduleExecutor {
124    if is_commander_running() {
125        ScheduleExecutor::Commander
126    } else {
127        ScheduleExecutor::SystemCron
128    }
129}