use crate::error::AppError;
use crate::paths::workbenches_dir;
use crate::routine_storage::{
append_manual_trigger_log, append_scheduled_trigger_log, append_skip_log, write_routine,
};
use crate::utils::lock::LockRecover;
use crate::utils::time::now_secs;
use crate::routines::agents::load_agent_command;
use crate::routines::cleanup::{
cleanup_expired_workbenches, parse_workbench_name, tmux_session_count,
tmux_session_prefix_alive,
};
use crate::routines::command::{
build_routine_command, inline_prompt_overflow, routine_env_carrier_vars, tmux_session_prefix,
TriggerSource, TMUX_SESSION_PREFIX,
};
use crate::routines::model::{CleanupResponse, Routine, RoutineStore};
use crate::routines::{max_concurrent_runs, MAX_CONCURRENT_RUNS_ENV};
use super::scheduled_trigger_claim::claim_current_minute;
use super::service_log_tail::{read_log_tail_with_meta, LogWithMeta};
#[allow(
clippy::missing_docs_in_private_items,
reason = "split-out module keeps the file under the linecheck limit"
)]
#[path = "spawn_routine_command.rs"]
mod spawn_routine_command;
pub(crate) use spawn_routine_command::*;
#[path = "sh_bin.rs"]
mod sh_bin;
pub(crate) use sh_bin::sh_bin;
pub fn svc_trigger(store: &RoutineStore, id: &str) -> Result<Routine, AppError> {
svc_trigger_with_system_power_saving_override(store, id, false)
}
pub fn svc_trigger_with_system_power_saving_override(
store: &RoutineStore,
id: &str,
override_system_power_saving: bool,
) -> Result<Routine, AppError> {
if crate::global_lock::is_globally_locked() {
return Err(AppError::Locked("routines are globally locked".into()));
}
let mut lock = store.lock_recover();
let routine = lock.get_mut(id).ok_or(AppError::NotFound)?;
if !routine.enabled {
return Err(AppError::Locked("routine is disabled".into()));
}
if let Some(reason) = power_saving_block_reason(
routine,
crate::system_power::is_system_power_saving_active(),
override_system_power_saving,
) {
return Err(AppError::Locked(reason.into()));
}
let ts = now_secs();
routine.last_manual_trigger_at = Some(ts);
let routine = routine.clone();
drop(lock);
write_routine(&routine).map_err(|_| AppError::Internal)?;
append_manual_trigger_log(&crate::routine_storage::routine_rel_dir(&routine), ts);
spawn_routine_command(&routine, TriggerSource::Manual);
Ok(routine)
}
pub fn svc_trigger_scheduled(store: &RoutineStore, id: &str) -> Result<Routine, AppError> {
if crate::global_lock::is_globally_locked() {
return Err(AppError::Locked("routines are globally locked".into()));
}
let mut lock = store.lock_recover();
let routine = lock.get_mut(id).ok_or(AppError::NotFound)?;
if !routine.enabled {
return Err(AppError::Locked("routine is disabled".into()));
}
if let Some(reason) = power_saving_block_reason(
routine,
crate::system_power::is_system_power_saving_active(),
false,
) {
let ts = now_secs();
let rel_dir = crate::routine_storage::routine_rel_dir(routine);
drop(lock);
append_skip_log(&rel_dir, ts, reason);
return Err(AppError::Locked(reason.into()));
}
if let Some(until) = routine.snoozed_until {
if now_secs() < until {
return Err(AppError::Locked(format!("routine snoozed until {until}")));
}
routine.snoozed_until = None;
let routine = routine.clone();
drop(lock);
write_routine(&routine).map_err(|_| AppError::Internal)?;
append_scheduled_trigger_log(
&crate::routine_storage::routine_rel_dir(&routine),
now_secs(),
);
spawn_routine_command(&routine, TriggerSource::Scheduled);
return Ok(routine);
}
if let Some(runs) = routine.skip_runs {
if runs > 0 {
routine.skip_runs = (runs > 1).then_some(runs - 1);
let routine = routine.clone();
drop(lock);
write_routine(&routine).map_err(|_| AppError::Internal)?;
return Err(AppError::Locked(format!(
"routine snoozed, skipping this scheduled run ({} more to skip)",
routine.skip_runs.unwrap_or(0)
)));
}
}
let ts = now_secs();
if !claim_current_minute(id, ts) {
let reason = "routine already fired this minute; skipping duplicate scheduled trigger";
let rel_dir = crate::routine_storage::routine_rel_dir(routine);
drop(lock);
append_skip_log(&rel_dir, ts, reason);
return Err(AppError::Locked(reason.into()));
}
routine.last_scheduled_trigger_at = Some(ts);
let routine = routine.clone();
drop(lock);
let rel_dir = crate::routine_storage::routine_rel_dir(&routine);
append_scheduled_trigger_log(&rel_dir, ts);
log::info!(
"routine scheduled fire accepted: id={:?}, timestamp={ts}, evidence={}",
routine.id,
crate::paths::routine_scheduled_log_path(&rel_dir).display()
);
spawn_routine_command(&routine, TriggerSource::Scheduled);
Ok(routine)
}
pub(super) const fn power_saving_block_reason(
routine: &Routine,
system_active: bool,
override_system_power_saving: bool,
) -> Option<&'static str> {
if routine.power_saving {
return Some("routine is in power-saving mode");
}
if system_active && !routine.power_saving_exempt && !override_system_power_saving {
return Some("system power saving is active");
}
None
}
include!("svc_snooze.rs");