use std::sync::{Mutex, OnceLock};
use crate::routine_storage::read_routine_crons;
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::cron::{normalize_schedule, validate_cron};
use crate::utils::lock::LockRecover;
#[allow(
clippy::missing_docs_in_private_items,
reason = "split-out module keeps the file under the linecheck limit"
)]
#[path = "sync_routines_to_crontab.rs"]
mod sync_routines_to_crontab;
pub(crate) use sync_routines_to_crontab::*;
fn crontab_sync_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
pub(crate) const BLOCK_BEGIN: &str = "# BEGIN MOADIM-ROUTINES";
pub(crate) const BLOCK_END: &str = "# END MOADIM-ROUTINES";
const BLOCK_HEADER: &str = "# Managed by moadim — routines (agent tmux sessions)";
pub(crate) fn format_routine_line_for_schedule(routine: &Routine, schedule: &str) -> String {
#[allow(
clippy::expect_used,
reason = "the daemon is already running from this binary, so resolving its own path \
cannot realistically fail; a failure here means the process has no executable \
path at all, which is unrecoverable, and this fn has no `Result` to propagate \
through short of reshaping every crontab-formatting caller"
)]
let exe = std::env::current_exe().expect("daemon executable path is resolvable");
let schedule = to_os_schedule(schedule);
format!(
"{} {} schedule trigger {} # moadim-routine:{}",
schedule,
shell_quote(&exe.to_string_lossy()),
shell_quote(&routine.id),
routine.id
)
}
#[cfg(test)]
pub(crate) fn format_routine_line(routine: &Routine) -> String {
format_routine_line_for_schedule(routine, &routine.schedule)
}
fn pure_schedules_for_crontab(routine: &Routine) -> Vec<String> {
let rel_dir = crate::routine_storage::routine_rel_dir(routine);
let entries = read_routine_crons(&crate::paths::routine_dir(&rel_dir).join("schedule.cron"));
if entries.is_empty() {
vec![routine.schedule.clone()]
} else {
entries
}
}
fn compailed_schedules_for_crontab(routine: &Routine, pure_schedules: &[String]) -> Vec<String> {
let schedules: Vec<String> = pure_schedules
.iter()
.map(|entry| normalize_schedule(entry))
.filter(|schedule| match validate_cron(schedule) {
Ok(()) => true,
Err(err) => {
log::warn!(
"routine sync: invalid schedule.cron entry {:?} for routine {:?}: {}; skipping",
schedule,
routine.id,
err
);
false
}
})
.collect();
if schedules.is_empty() {
return vec![routine.schedule.clone()];
}
let refs: Vec<&str> = schedules.iter().map(String::as_str).collect();
match cron_union::union(refs) {
Ok(union) => union.iter().map(ToString::to_string).collect(),
Err(err) => {
log::warn!(
"routine sync: cron-union could not compile schedules for routine {:?}: {}; \
using validated schedules without dedupe",
routine.id,
err
);
schedules
}
}
}
fn write_compailed_cron_sidecar(routine: &Routine, schedules: &[String]) {
let rel_dir = crate::routine_storage::routine_rel_dir(routine);
let dir = crate::paths::routine_dir(&rel_dir);
let path = crate::paths::routine_compailed_cron_path(&rel_dir);
let legacy_path = dir.join(".compailed.cron");
let mut text = schedules.join("\n");
text.push('\n');
let _ = crate::utils::fs_perms::create_private_dir_all(&dir);
let _ = std::fs::write(&path, text);
if legacy_path != path && legacy_path.exists() {
let _ = std::fs::remove_file(legacy_path);
}
}
include!("build_block.rs");