use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FreezeMarker {
pub pid: u32,
pub reason: String,
pub incident: bool,
pub at: String,
}
impl FreezeMarker {
pub(crate) fn remedy(&self) -> &'static str {
if self.incident {
":incident END"
} else {
":thaw-deploys"
}
}
}
fn marker_path() -> PathBuf {
crate::util::cache_dir().join("freeze.json")
}
pub(crate) fn write_marker(reason: &str, incident: bool) -> std::io::Result<()> {
write_marker_at(&marker_path(), std::process::id(), reason, incident)
}
fn write_marker_at(path: &Path, pid: u32, reason: &str, incident: bool) -> std::io::Result<()> {
let body = format!(
"{{\"pid\":{pid},\"reason\":{},\"incident\":{incident},\"at\":{}}}\n",
crate::util::json_string(reason),
crate::util::json_string(&chrono::Utc::now().to_rfc3339()),
);
static WRITE_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let tmp = path.with_extension(format!(
"tmp.{}.{}",
std::process::id(),
WRITE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
crate::util::write_secure(&tmp, body.as_bytes())?;
if let Err(e) = std::fs::rename(&tmp, path) {
let _ = std::fs::remove_file(&tmp);
tracing::warn!(error = %e, "could not persist freeze marker — cross-process enforcement inactive");
return Err(e);
}
Ok(())
}
pub fn clear_marker_if_own() {
clear_if_pid_at(&marker_path(), std::process::id());
}
fn clear_if_pid_at(path: &Path, own_pid: u32) {
if let Some(m) = parse_file(path) {
if m.pid == own_pid {
let _ = std::fs::remove_file(path);
}
}
}
pub(crate) fn refusal_message(marker: &FreezeMarker) -> String {
let reason = if marker.reason.is_empty() {
"no reason given"
} else {
marker.reason.as_str()
};
format!(
"fleet freeze active ({reason}) — lift with `{}` in the owning TUI (pid {})",
marker.remedy(),
marker.pid
)
}
pub(crate) fn read_active() -> Option<FreezeMarker> {
read_active_with(&marker_path(), pid_alive, process_start_epoch)
}
const START_SLACK_SECS: i64 = 300;
fn marker_owner_is_live(
alive: bool,
start_epoch: Option<i64>,
marker_at_epoch: Option<i64>,
) -> bool {
if !alive {
return false;
}
let Some(marker_at) = marker_at_epoch else {
return true;
};
!matches!(start_epoch, Some(start) if start > marker_at.saturating_add(START_SLACK_SECS))
}
fn read_active_with(
path: &Path,
alive: impl Fn(u32) -> bool,
start_epoch: impl Fn(u32) -> Option<i64>,
) -> Option<FreezeMarker> {
let m = parse_file(path)?;
let at_epoch = chrono::DateTime::parse_from_rfc3339(&m.at)
.map(|t| t.timestamp())
.ok();
if marker_owner_is_live(alive(m.pid), start_epoch(m.pid), at_epoch) {
return Some(m);
}
if parse_file(path).map(|m2| m2.pid) == Some(m.pid) {
let _ = std::fs::remove_file(path);
}
None
}
fn parse_file(path: &Path) -> Option<FreezeMarker> {
let text = std::fs::read_to_string(path).ok()?;
parse_marker(&text)
}
pub(crate) fn parse_marker(text: &str) -> Option<FreezeMarker> {
let v: serde_json::Value = serde_json::from_str(text).ok()?;
Some(FreezeMarker {
pid: v.get("pid")?.as_u64()? as u32,
reason: v.get("reason")?.as_str()?.to_string(),
incident: v.get("incident").and_then(|b| b.as_bool()).unwrap_or(false),
at: v
.get("at")
.and_then(|s| s.as_str())
.unwrap_or("")
.to_string(),
})
}
#[cfg(test)]
pub(crate) static MARKER_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(unix)]
fn pid_alive(pid: u32) -> bool {
let r = unsafe { libc::kill(pid as libc::pid_t, 0) };
r == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(not(unix))]
fn pid_alive(_pid: u32) -> bool {
true
}
#[cfg(target_os = "macos")]
fn process_start_epoch(pid: u32) -> Option<i64> {
unsafe {
let mut info: libc::proc_bsdinfo = std::mem::zeroed();
let want = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
let got = libc::proc_pidinfo(
pid as libc::c_int,
libc::PROC_PIDTBSDINFO,
0,
&mut info as *mut _ as *mut libc::c_void,
want,
);
(got == want).then_some(info.pbi_start_tvsec as i64)
}
}
#[cfg(any(target_os = "linux", test))]
pub(crate) fn start_ticks_from_stat(stat: &str) -> Option<u64> {
let after = &stat[stat.rfind(')')? + 1..];
after.split_whitespace().nth(19)?.parse().ok()
}
#[cfg(any(target_os = "linux", test))]
pub(crate) fn btime_from_proc_stat(body: &str) -> Option<i64> {
body.lines()
.find_map(|l| l.strip_prefix("btime "))?
.trim()
.parse()
.ok()
}
#[cfg(any(target_os = "linux", test))]
pub(crate) fn start_epoch_from(btime: i64, ticks: u64, hz: i64) -> Option<i64> {
if hz <= 0 {
return None;
}
Some(btime + (ticks / hz as u64) as i64)
}
#[cfg(target_os = "linux")]
fn process_start_epoch(pid: u32) -> Option<i64> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let ticks = start_ticks_from_stat(&stat)?;
let hz = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
let btime = btime_from_proc_stat(&std::fs::read_to_string("/proc/stat").ok()?)?;
start_epoch_from(btime, ticks, hz)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn process_start_epoch(_pid: u32) -> Option<i64> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn start_ticks_reads_field_22_after_the_last_paren() {
let f: Vec<String> = (1..=30).map(|i| i.to_string()).collect();
let stat = format!("42 (bash) S {}", f.join(" "));
assert_eq!(start_ticks_from_stat(&stat), Some(19));
}
#[test]
fn a_comm_containing_spaces_and_parens_does_not_shift_the_fields() {
let f: Vec<String> = (1..=30).map(|i| i.to_string()).collect();
let plain = format!("42 (bash) S {}", f.join(" "));
for comm in ["((sd-pam))", "(Web Content)", "(a b) c)", "(x (y) z)"] {
let odd = format!("42 {comm} S {}", f.join(" "));
assert_eq!(
start_ticks_from_stat(&odd),
start_ticks_from_stat(&plain),
"comm {comm:?} shifted the field count"
);
}
}
#[test]
fn malformed_stat_lines_yield_no_start_time() {
for bad in [
"",
"42 bash S 1 2 3",
"42 (bash) S 1 2 3",
"42 (bash) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 x",
] {
assert_eq!(start_ticks_from_stat(bad), None, "accepted {bad:?}");
}
}
#[test]
fn btime_is_read_from_its_own_line() {
let body = "cpu 1 2 3\nintr 99\nbtime 1756000000\nprocesses 5\n";
assert_eq!(btime_from_proc_stat(body), Some(1_756_000_000));
assert_eq!(btime_from_proc_stat("cpu 1\nnot_btime 5\n"), None);
assert_eq!(btime_from_proc_stat(""), None);
assert_eq!(btime_from_proc_stat("btime notanumber\n"), None);
}
#[test]
fn start_epoch_converts_ticks_to_seconds_and_adds_boot_time() {
assert_eq!(start_epoch_from(1_000_000, 0, 100), Some(1_000_000));
assert_eq!(start_epoch_from(1_000_000, 100, 100), Some(1_000_001));
assert_eq!(
start_epoch_from(1_000_000, 250, 100),
Some(1_000_002),
"truncates"
);
}
#[test]
fn a_bad_clock_tick_yields_no_start_time_rather_than_dividing_by_it() {
assert_eq!(start_epoch_from(1_000_000, 100, 0), None);
assert_eq!(start_epoch_from(1_000_000, 100, -1), None);
}
fn tmp(name: &str) -> PathBuf {
std::env::temp_dir().join(format!("ebman-freeze-{}-{name}.json", std::process::id()))
}
#[test]
fn marker_round_trips() {
let p = tmp("rt");
write_marker_at(&p, 4242, "checkout 5xx", true).unwrap();
let m = parse_file(&p).expect("parses");
assert_eq!(m.pid, 4242);
assert_eq!(m.reason, "checkout 5xx");
assert!(m.incident);
assert_eq!(m.remedy(), ":incident END");
let _ = std::fs::remove_file(&p);
}
#[test]
fn dead_pid_marker_is_ignored_and_cleaned() {
let p = tmp("dead");
write_marker_at(&p, 4242, "stale", false).unwrap();
assert!(read_active_with(&p, |_| false, |_| None).is_none());
assert!(!p.exists(), "stale marker must be removed by the reader");
}
#[test]
fn live_pid_marker_is_active() {
let p = tmp("live");
write_marker_at(&p, 4242, "deploy freeze", false).unwrap();
let m = read_active_with(&p, |_| true, |_| None).expect("active");
assert_eq!(m.remedy(), ":thaw-deploys");
assert!(p.exists());
let _ = std::fs::remove_file(&p);
}
#[test]
fn clear_only_removes_own_marker() {
let p = tmp("own");
write_marker_at(&p, 111, "someone else's", false).unwrap();
clear_if_pid_at(&p, 222);
assert!(p.exists(), "another session's marker survives");
clear_if_pid_at(&p, 111);
assert!(!p.exists());
}
#[test]
fn reader_cleanup_does_not_delete_a_freshly_written_live_marker() {
let p = tmp("toctou");
write_marker_at(&p, 4242, "dead session", false).unwrap();
let overwritten = std::cell::Cell::new(false);
let result = read_active_with(
&p,
|_pid| {
if !overwritten.get() {
write_marker_at(&p, 111, "new live freeze", true).unwrap();
overwritten.set(true);
}
false },
|_| None,
);
assert!(
result.is_none(),
"original dead marker not returned as active"
);
assert!(p.exists(), "the freshly-written live marker must survive");
let m = parse_file(&p).unwrap();
assert_eq!(m.pid, 111, "live marker intact");
let _ = std::fs::remove_file(&p);
}
#[test]
fn a_reused_pid_does_not_hold_the_freeze() {
let at = 1_700_000_000;
assert!(
!marker_owner_is_live(true, Some(at + 86_400), Some(at)),
"a process that started a day after the marker cannot have \
written it"
);
assert!(marker_owner_is_live(true, Some(at - 60), Some(at)));
assert!(
marker_owner_is_live(true, Some(at), Some(at)),
"same second"
);
assert!(!marker_owner_is_live(false, Some(at - 60), Some(at)));
assert!(!marker_owner_is_live(false, None, Some(at)));
}
#[test]
fn an_unreadable_start_time_fails_closed() {
let at = 1_700_000_000;
assert!(
marker_owner_is_live(true, None, Some(at)),
"unknown start time must not lift the freeze"
);
assert!(
marker_owner_is_live(true, Some(at + START_SLACK_SECS - 1), Some(at)),
"a start time inside the slack window is still the owner"
);
assert!(
!marker_owner_is_live(true, Some(at + START_SLACK_SECS + 1), Some(at)),
"past the slack it is reuse"
);
}
#[test]
fn an_unreadable_marker_timestamp_keeps_the_freeze_rather_than_lifting_it() {
let live_process_start = 1_756_000_000_i64;
assert!(
marker_owner_is_live(true, Some(live_process_start), None),
"an unjudgeable marker must keep the freeze, not lift it"
);
assert!(!marker_owner_is_live(false, Some(live_process_start), None));
}
#[test]
fn the_start_time_probe_works_on_this_process() {
let start = super::process_start_epoch(std::process::id());
let start = start.expect(
"this platform must report a process start time, or the \
reuse check silently degrades to the old behaviour",
);
let now = chrono::Utc::now().timestamp();
assert!(
start <= now && start > now - 86_400 * 365,
"start {start} is not a plausible epoch second near {now}"
);
}
#[test]
fn corrupt_marker_never_blocks() {
let p = tmp("corrupt");
let _ = crate::util::write_secure(&p, b"not json at all");
assert!(read_active_with(&p, |_| true, |_| None).is_none());
let _ = std::fs::remove_file(&p);
}
#[test]
fn reason_with_quotes_survives() {
let p = tmp("quotes");
write_marker_at(&p, 1, "the \"big\" one\nline2", false).unwrap();
let m = parse_file(&p).expect("parses");
assert_eq!(m.reason, "the \"big\" one\nline2");
let _ = std::fs::remove_file(&p);
}
#[test]
fn one_freeze_refusal_sentence_for_every_surface() {
let m = FreezeMarker {
pid: 4242,
reason: "prod incident".into(),
incident: false,
at: "2026-08-22T10:00:00Z".into(),
};
let msg = refusal_message(&m);
assert!(msg.contains("prod incident"), "{msg}");
assert!(msg.contains("4242"), "names the owning pid: {msg}");
assert!(msg.contains(m.remedy()), "names the remedy: {msg}");
let m = FreezeMarker {
reason: String::new(),
..m
};
let msg = refusal_message(&m);
assert!(msg.contains("no reason given"), "{msg}");
assert!(!msg.contains("()"), "{msg}");
}
#[cfg(unix)]
#[test]
fn pid_alive_says_yes_to_this_process_and_no_to_a_reaped_one() {
assert!(
super::pid_alive(std::process::id()),
"this very process is alive; a probe that says otherwise would \
lift a live session's freeze"
);
let child = std::process::Command::new("true")
.spawn()
.expect("spawn a trivial child");
let pid = child.id();
let mut child = child;
let _ = child.wait().expect("reap the child");
assert!(
!super::pid_alive(pid),
"pid {pid} exited and was reaped; a probe that still says alive \
would leave a crashed session's freeze in place forever"
);
}
#[test]
fn a_written_marker_is_readable_through_the_real_path() {
let _guard = super::MARKER_LOCK.lock().unwrap_or_else(|e| e.into_inner());
super::clear_marker_if_own();
super::write_marker("incident #4321", true).expect("marker must be written");
let path = super::marker_path();
assert!(
path.ends_with("freeze.json"),
"the marker must land at the real path, not a default: {}",
path.display()
);
assert!(path.exists(), "nothing was written to {}", path.display());
let found = super::read_active().expect(
"a marker written by THIS live process must read back as active — \
otherwise a freeze silently fails open while the operator \
believes the fleet is frozen",
);
assert_eq!(found.reason, "incident #4321");
assert!(
found.incident,
"the incident flag must survive the round trip"
);
assert_eq!(found.pid, std::process::id());
super::clear_marker_if_own();
assert!(
super::read_active().is_none(),
"clearing our own marker must lift the freeze"
);
}
}