use std::io;
use std::os::unix::fs::PermissionsExt;
use crate::paths::routine_script_path;
use crate::routines::{
build_routine_command, load_agent_command, shell_quote, AgentCommand, Routine, RoutineStore,
};
use crate::sync::{read_crontab, replace_block_with, to_os_schedule, write_crontab, SyncError};
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)";
fn write_routine_script(routine: &Routine, agent: &AgentCommand) -> io::Result<std::path::PathBuf> {
let path = routine_script_path(&routine.id);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let command = build_routine_command(routine, agent);
std::fs::write(&path, format!("#!/bin/sh\n{command}\n"))?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))?;
Ok(path)
}
pub(crate) fn format_routine_line(routine: &Routine, agent: &AgentCommand) -> Option<String> {
let script = match write_routine_script(routine, agent) {
Ok(p) => p,
Err(e) => {
log::warn!(
"routine sync: failed to write run.sh for routine {:?}: {e}; skipping",
routine.id
);
return None;
}
};
let schedule = to_os_schedule(&routine.schedule);
Some(format!(
"{} /bin/sh {} # moadim-routine:{}",
schedule,
shell_quote(&script.to_string_lossy()),
routine.id
))
}
fn build_block(store: &RoutineStore) -> String {
let mut routines: Vec<Routine> = {
let lock = store.lock().unwrap();
lock.values()
.filter(|r| r.source == "managed" && r.enabled)
.cloned()
.collect()
};
routines.sort_by_key(|r| r.created_at);
let lines: Vec<String> = routines
.iter()
.filter_map(|r| match load_agent_command(&r.agent) {
Some(agent) => format_routine_line(r, &agent),
None => {
log::warn!(
"routine sync: agent config not found for routine {:?} (agent {:?}); skipping",
r.id,
r.agent
);
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")
)
}
}
pub fn sync_routines_to_crontab(store: &RoutineStore) -> Result<(), SyncError> {
let current = read_crontab()?;
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;