use std::path::{Path, PathBuf};
use crate::pidfile::{pid_alive, pidfile_path, read_pidfile_raw};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionEntry {
pub label: String,
pub state_dir: PathBuf,
pub pid: u32,
pub version: String,
pub endpoint: String,
pub alive: bool,
}
#[must_use]
pub fn enumerate(base_dir: &Path) -> Vec<SessionEntry> {
enumerate_with(base_dir, &crate::replace::pid_belongs_to_daemon)
}
#[must_use]
pub fn enumerate_with(base_dir: &Path, identity: &dyn Fn(u32, &Path) -> bool) -> Vec<SessionEntry> {
let mut out = Vec::new();
if let Some(e) = entry_for(base_dir, "default", identity) {
out.push(e);
}
if let Ok(rd) = std::fs::read_dir(base_dir) {
for ent in rd.flatten() {
let p = ent.path();
if p.is_dir() && pidfile_path(&p).exists() {
let label = ent.file_name().to_string_lossy().into_owned();
if let Some(e) = entry_for(&p, &label, identity) {
out.push(e);
}
}
}
}
out
}
fn entry_for(
state_dir: &Path,
label: &str,
identity: &dyn Fn(u32, &Path) -> bool,
) -> Option<SessionEntry> {
let rec = read_pidfile_raw(state_dir)?;
let alive = pid_alive(rec.pid) && identity(rec.pid, state_dir);
Some(SessionEntry {
label: label.to_owned(),
state_dir: state_dir.to_path_buf(),
pid: rec.pid,
version: rec.version,
endpoint: rec.endpoint,
alive,
})
}
#[must_use]
pub fn cleanup_stale(state_dir: &Path, classified_pid: u32) -> bool {
match read_pidfile_raw(state_dir) {
Some(rec) if rec.pid == classified_pid => {
crate::pidfile::remove_pidfile(state_dir);
true
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pidfile::{RunningDaemon, write_pidfile};
fn tmp() -> PathBuf {
std::env::temp_dir().join(format!(
"tc-sessions-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
#[test]
fn enumerate_finds_default_and_seeded_with_stale_classification() {
let base = tmp();
write_pidfile(
&base,
&RunningDaemon {
pid: 999_999_999,
version: "0.1.0".into(),
endpoint: "base.sock".into(),
},
)
.unwrap();
let seeded = base.join("agent-1");
write_pidfile(
&seeded,
&RunningDaemon {
pid: std::process::id(),
version: "0.1.1".into(),
endpoint: "agent.sock".into(),
},
)
.unwrap();
let me = std::process::id();
let identity = move |pid: u32, _dir: &Path| pid == me;
let mut got = enumerate_with(&base, &identity);
got.sort_by(|a, b| a.label.cmp(&b.label));
assert_eq!(got.len(), 2);
let agent = got.iter().find(|e| e.label == "agent-1").unwrap();
assert!(agent.alive, "seeded session with this pid must be alive");
assert_eq!(agent.version, "0.1.1");
let def = got.iter().find(|e| e.label == "default").unwrap();
assert!(!def.alive, "default session with dead pid must be stale");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn recycled_pid_is_reported_not_alive_and_reaped() {
let base = tmp();
let recycled = std::process::id(); write_pidfile(
&base,
&RunningDaemon {
pid: recycled,
version: "0".into(),
endpoint: "x".into(),
},
)
.unwrap();
let identity = |_pid: u32, _dir: &Path| false;
let entries = enumerate_with(&base, &identity);
assert_eq!(entries.len(), 1);
let e = &entries[0];
assert_eq!(e.pid, recycled, "pid is the recycled (live) pid");
assert!(
!e.alive,
"a live-but-non-daemon recycled pid must be reported NOT alive so it is reapable"
);
assert!(
cleanup_stale(&e.state_dir, e.pid),
"stale pidfile for a recycled pid must be cleaned"
);
assert!(
read_pidfile_raw(&base).is_none(),
"stale pidfile must be gone after reap"
);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn cleanup_stale_removes_only_matching_dead_pid() {
let base = tmp();
write_pidfile(
&base,
&RunningDaemon {
pid: 999_999_999,
version: "0".into(),
endpoint: "x".into(),
},
)
.unwrap();
assert!(
cleanup_stale(&base, 999_999_999),
"matching dead pid must be cleaned"
);
assert!(read_pidfile_raw(&base).is_none(), "pidfile must be gone");
write_pidfile(
&base,
&RunningDaemon {
pid: std::process::id(),
version: "0".into(),
endpoint: "x".into(),
},
)
.unwrap();
assert!(
!cleanup_stale(&base, 999_999_999),
"must NOT delete when current pid differs from classified"
);
assert!(
read_pidfile_raw(&base).is_some(),
"live pidfile must survive"
);
let _ = std::fs::remove_dir_all(&base);
}
}