use crate::constants::paths::STOP_SIGNAL_FILE;
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
fn signal_content() -> String {
format!(
"Stop requested at {}",
crate::timeutil::now_utc_rfc3339_or_fallback()
)
}
pub fn stop_signal_path(cache_dir: &Path) -> PathBuf {
cache_dir.join(STOP_SIGNAL_FILE)
}
pub fn create_stop_signal(cache_dir: &Path) -> Result<()> {
let path = stop_signal_path(cache_dir);
fs::create_dir_all(cache_dir)
.with_context(|| format!("create cache directory {}", cache_dir.display()))?;
crate::fsutil::write_atomic(&path, signal_content().as_bytes())
.with_context(|| format!("write stop signal file {}", path.display()))?;
log::info!("Stop signal created at {}", path.display());
Ok(())
}
pub fn stop_signal_exists(cache_dir: &Path) -> bool {
stop_signal_path(cache_dir).exists()
}
pub fn clear_stop_signal(cache_dir: &Path) -> Result<bool> {
let path = stop_signal_path(cache_dir);
if !path.exists() {
return Ok(false);
}
fs::remove_file(&path)
.with_context(|| format!("remove stop signal file {}", path.display()))?;
log::debug!("Stop signal cleared at {}", path.display());
Ok(true)
}
pub fn clear_stop_signal_at_loop_start(cache_dir: &Path) {
match clear_stop_signal(cache_dir) {
Ok(true) => {
log::debug!("Cleared stale stop signal from previous run");
}
Ok(false) => {
}
Err(e) => {
log::warn!("Failed to clear stop signal: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn stop_signal_path_construction() {
let repo_root = crate::testsupport::path::portable_abs_path("signal-path-construction");
let cache_dir = repo_root.join(".ralph/cache");
let path = stop_signal_path(&cache_dir);
assert_eq!(path, cache_dir.join("stop_requested"));
}
#[test]
fn create_stop_signal_creates_file() -> anyhow::Result<()> {
let temp = TempDir::new()?;
let cache_dir = temp.path().join("cache");
assert!(!stop_signal_exists(&cache_dir));
create_stop_signal(&cache_dir)?;
assert!(stop_signal_exists(&cache_dir));
let path = stop_signal_path(&cache_dir);
let content = fs::read_to_string(&path)?;
assert!(content.contains("Stop requested at"));
Ok(())
}
#[test]
fn create_stop_signal_is_idempotent() -> anyhow::Result<()> {
let temp = TempDir::new()?;
let cache_dir = temp.path().join("cache");
let path = stop_signal_path(&cache_dir);
fs::create_dir_all(&cache_dir)?;
fs::write(&path, "Stop requested at stale-timestamp")?;
create_stop_signal(&cache_dir)?;
let refreshed_content = fs::read_to_string(&path)?;
assert!(stop_signal_exists(&cache_dir));
assert_ne!(refreshed_content, "Stop requested at stale-timestamp");
assert!(refreshed_content.contains("Stop requested at"));
Ok(())
}
#[test]
fn clear_stop_signal_removes_file() -> anyhow::Result<()> {
let temp = TempDir::new()?;
let cache_dir = temp.path().join("cache");
create_stop_signal(&cache_dir)?;
assert!(stop_signal_exists(&cache_dir));
let cleared = clear_stop_signal(&cache_dir)?;
assert!(cleared);
assert!(!stop_signal_exists(&cache_dir));
Ok(())
}
#[test]
fn clear_stop_signal_is_idempotent() -> anyhow::Result<()> {
let temp = TempDir::new()?;
let cache_dir = temp.path().join("cache");
let cleared = clear_stop_signal(&cache_dir)?;
assert!(!cleared);
Ok(())
}
#[test]
fn clear_stop_signal_at_loop_start_handles_missing() {
let temp = TempDir::new().unwrap();
let cache_dir = temp.path().join("cache");
clear_stop_signal_at_loop_start(&cache_dir);
assert!(!stop_signal_exists(&cache_dir));
}
#[test]
fn clear_stop_signal_at_loop_start_clears_existing() {
let temp = TempDir::new().unwrap();
let cache_dir = temp.path().join("cache");
create_stop_signal(&cache_dir).unwrap();
assert!(stop_signal_exists(&cache_dir));
clear_stop_signal_at_loop_start(&cache_dir);
assert!(!stop_signal_exists(&cache_dir));
}
}