use crate::routines::{load_agent_command, shell_quote, Routine, RoutineStore};
use crate::sync::{read_crontab, replace_block_with, to_os_schedule, write_crontab, SyncError};
use crate::utils::lock::LockRecover;
const BLOCK_BEGIN: &str = "# BEGIN MOADIM-ROUTINES";
const BLOCK_END: &str = "# END MOADIM-ROUTINES";
const BLOCK_HEADER: &str = "# Managed by moadim — routines (agent tmux sessions)";
pub(crate) fn format_routine_line(routine: &Routine) -> String {
let exe = std::env::current_exe().expect("daemon executable path is resolvable");
let schedule = to_os_schedule(&routine.schedule);
format!(
"{} {} schedule trigger {} # moadim-routine:{}",
schedule,
shell_quote(&exe.to_string_lossy()),
shell_quote(&routine.id),
routine.id
)
}
fn build_block(store: &RoutineStore) -> String {
if crate::global_lock::is_globally_locked() {
log::info!("routine sync: global lock active — clearing all routine crontab lines");
return format!("{BLOCK_BEGIN}\n{BLOCK_HEADER}\n{BLOCK_END}");
}
let me = crate::machine::current_machine();
let mut routines: Vec<Routine> = {
let lock = store.lock_recover();
lock.values()
.filter(|routine| routine.source == "managed" && routine.enabled)
.cloned()
.collect()
};
warn_dormant_routines(&routines);
routines.retain(|routine| crate::machine::targets(&routine.machines, &me));
routines.sort_by_key(|routine| routine.created_at);
let lines: Vec<String> = routines
.iter()
.filter_map(|routine| match load_agent_command(&routine.agent) {
Ok(_) => Some(format_routine_line(routine)),
Err(err) => {
log::warn!(
"routine sync: cannot load agent {:?} ({}) for routine {:?}; skipping",
routine.agent,
err,
routine.id
);
None
}
})
.collect();
if lines.is_empty() {
format!("{BLOCK_BEGIN}\n{BLOCK_HEADER}\n{BLOCK_END}")
} else {
format!(
"{BLOCK_BEGIN}\n{BLOCK_HEADER}\n{}\n{BLOCK_END}",
lines.join("\n")
)
}
}
fn warn_dormant_routines(routines: &[Routine]) {
let dormant: Vec<&str> = routines
.iter()
.filter(|routine| routine.machines.is_empty())
.map(|routine| routine.title.as_str())
.collect();
if !dormant.is_empty() {
log::warn!(
"{} enabled routine(s) have no machine assignment and will not be scheduled on any \
machine: {}; assign with `moadim routines update <id> --machines '[\"<name>\"]'`",
dormant.len(),
dormant.join(", ")
);
}
}
const ROUTINE_LINE_MARKER: &str = "# moadim-routine:";
pub fn sync_routines_to_crontab(store: &RoutineStore) -> Result<(), SyncError> {
let current = read_crontab()?;
if store.lock_recover().is_empty() && current.contains(ROUTINE_LINE_MARKER) {
log::warn!(
"routine sync: store is empty but the crontab still has routine lines; refusing to \
wipe the routines block (suspected load failure or a concurrent daemon)"
);
return Ok(());
}
let block = build_block(store);
let new_crontab = replace_block_with(¤t, &block, BLOCK_BEGIN, BLOCK_END);
if new_crontab == current {
return Ok(());
}
write_crontab(&new_crontab)
}
#[cfg(test)]
#[path = "routines_sync_tests.rs"]
mod routines_sync_tests;