use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use tracing::warn;
pub(crate) const SWEEPS_DIR: &str = "sweeps";
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SweepMarker {
pid: u32,
started_unix_ms: u64,
#[serde(default)]
finished_unix_ms: Option<u64>,
#[serde(default)]
last_pass_ms: Option<u64>,
}
pub(crate) struct SweepPass {
pub running: bool,
pub last_pass: Option<Duration>,
}
pub(crate) fn marker_path(framework_root: &Path, sweep: &str) -> PathBuf {
framework_root
.join(SWEEPS_DIR)
.join(format!("{sweep}.json"))
}
pub(crate) fn record_start(path: &Path) {
write_marker(
path,
&SweepMarker {
pid: std::process::id(),
started_unix_ms: now_unix_ms(),
finished_unix_ms: None,
last_pass_ms: None,
},
);
}
pub(crate) fn record_finish(path: &Path, elapsed: Duration) {
let elapsed_ms = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX - 1);
let now = now_unix_ms();
let started = read_marker(path)
.map(|m| m.started_unix_ms)
.unwrap_or_else(|| now.saturating_sub(elapsed_ms));
write_marker(
path,
&SweepMarker {
pid: std::process::id(),
started_unix_ms: started,
finished_unix_ms: Some(now),
last_pass_ms: Some(elapsed_ms),
},
);
}
pub(crate) fn read_pass(framework_root: &Path, sweep: &str) -> Option<SweepPass> {
let marker = read_marker(&marker_path(framework_root, sweep))?;
let running =
marker.finished_unix_ms.is_none() && crate::core::process::is_process_alive(marker.pid);
Some(SweepPass {
running,
last_pass: marker.last_pass_ms.map(Duration::from_millis),
})
}
fn read_marker(path: &Path) -> Option<SweepMarker> {
let bytes = std::fs::read(path).ok()?;
serde_json::from_slice(&bytes).ok()
}
fn write_marker(path: &Path, marker: &SweepMarker) {
let Some(dir) = path.parent() else {
return;
};
if let Err(e) = std::fs::create_dir_all(dir) {
warn!(path = %path.display(), "sweep marker: cannot create {SWEEPS_DIR}: {e}");
return;
}
let bytes = match serde_json::to_vec(marker) {
Ok(b) => b,
Err(e) => {
warn!(path = %path.display(), "sweep marker: cannot encode: {e}");
return;
}
};
let tmp = path.with_extension(format!("json.{}.tmp", std::process::id()));
if let Err(e) = std::fs::write(&tmp, &bytes) {
warn!(path = %tmp.display(), "sweep marker: cannot write: {e}");
return;
}
if let Err(e) = std::fs::rename(&tmp, path) {
warn!(path = %path.display(), "sweep marker: cannot publish: {e}");
let _ = std::fs::remove_file(&tmp);
}
}
fn now_unix_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
.unwrap_or(0)
}
#[cfg(test)]
#[path = "sweep_state_file_tests.rs"]
mod sweep_state_file_tests;