use std::path::Path;
use std::time::Duration;
use crate::paths::workbenches_dir;
use crate::utils::time::now_secs;
use super::model::RoutineStore;
mod snapshot;
mod ttl;
pub const CLEANUP_INTERVAL: Duration = Duration::from_secs(60 * 60);
fn parse_workbench_name(name: &str) -> Option<(&str, u64)> {
let (slug, ts) = name.rsplit_once('-')?;
if slug.is_empty() || ts.is_empty() || !ts.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
Some((slug, ts.parse().ok()?))
}
fn is_expired(now: u64, ts: u64, ttl: u64) -> bool {
now.saturating_sub(ts) > ttl
}
fn tmux_session_alive(session: &str) -> bool {
std::process::Command::new("tmux")
.arg("has-session")
.arg("-t")
.arg(format!("={session}"))
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn reap_dir(
dir: &Path,
now: u64,
ttl_for: &dyn Fn(&str) -> u64,
is_alive: &dyn Fn(&str) -> bool,
) -> usize {
let Ok(entries) = std::fs::read_dir(dir) else {
return 0;
};
let mut removed = 0;
for entry in entries.flatten() {
if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
continue;
}
let name = entry.file_name().to_string_lossy().into_owned();
let Some((slug, ts)) = parse_workbench_name(&name) else {
continue;
};
if !is_expired(now, ts, ttl_for(slug)) {
continue;
}
if is_alive(&format!("moadim-{name}")) {
continue;
}
match std::fs::remove_dir_all(entry.path()) {
Ok(()) => {
removed += 1;
log::info!("cleanup: removed expired workbench {name:?}");
}
Err(e) => log::warn!("cleanup: failed to remove workbench {name:?}: {e}"),
}
}
removed
}
pub fn cleanup_expired_workbenches(store: &RoutineStore) -> usize {
let ttls = snapshot::snapshot_ttls(store);
let ttl_for = |slug: &str| snapshot::ttl_for(&ttls, slug);
reap_dir(
&workbenches_dir(),
now_secs(),
&ttl_for,
&tmux_session_alive,
)
}
#[cfg(test)]
#[path = "cleanup_tests.rs"]
mod cleanup_tests;