use std::collections::HashSet;
use std::time::Duration;
use car_scheduler::os_schedule::LABEL_PREFIX;
use car_scheduler::{parse_interval, run_command, TaskStore, TaskTrigger};
const POLL_SECS: u64 = 30;
const MAX_EXECUTIONS: usize = 50;
pub fn spawn_command_scheduler() {
tokio::spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(POLL_SECS));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
ticker.tick().await;
if let Err(e) = poll_once().await {
tracing::warn!(target: "car::scheduler", "[command-scheduler] {e}");
}
}
});
}
async fn poll_once() -> Result<(), String> {
let store = TaskStore::new(&TaskStore::default_path());
let tasks = store
.try_list()
.map_err(|e| format!("could not read task store: {e}"))?;
let os_installed: HashSet<String> = car_scheduler::list_installed()
.map_err(|e| format!("could not read OS schedules (skipping tick): {e}"))?
.into_iter()
.collect();
let now = chrono::Utc::now();
for mut task in tasks {
if !task.enabled || !task.is_command() || task.trigger != TaskTrigger::Interval {
continue;
}
let label = format!("{LABEL_PREFIX}{}", task.id);
if os_installed.contains(&label) {
continue;
}
let interval_secs = parse_interval(&task.schedule);
if interval_secs <= 0.0 {
continue;
}
let due = match task.last_run_at {
None => true, Some(last) => (now - last).num_seconds() as f64 >= interval_secs,
};
if !due {
continue;
}
let Some(cmd) = task.command.clone() else {
continue;
};
let exec = run_command(&cmd).await;
tracing::info!(
target: "car::scheduler",
task = %task.id, status = ?exec.status,
"[command-scheduler] fired command task"
);
task.last_run_at = Some(now);
task.run_count = task.run_count.saturating_add(1);
task.status = exec.status;
task.executions.push(exec);
if task.executions.len() > MAX_EXECUTIONS {
let drop = task.executions.len() - MAX_EXECUTIONS;
task.executions.drain(0..drop);
}
if let Err(e) = store.save(&task) {
tracing::warn!(target: "car::scheduler", "[command-scheduler] save {}: {e}", task.id);
}
}
Ok(())
}