pub(crate) mod change_neighbors;
pub(crate) mod pending_index;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime};
pub(crate) const WATCH_MARKER_STALE_SECS: u64 = 120;
#[cfg(unix)]
pub(crate) fn process_running(pid: u32) -> bool {
if pid == 0 {
return false;
}
Command::new("kill")
.args(["-0", &pid.to_string()])
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
#[cfg(windows)]
pub(crate) fn process_running(pid: u32) -> bool {
Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/NH"])
.output()
.is_ok_and(|output| {
output.status.success()
&& String::from_utf8_lossy(&output.stdout).contains(&pid.to_string())
})
}
pub fn try_claim(path: &Path, stale_after: Duration) -> Result<(), AlreadyHeld> {
use std::fs::OpenOptions;
for _ in 0..2 {
if let Ok(mut file) = OpenOptions::new().write(true).create_new(true).open(path) {
let _ = writeln!(file, "{}", std::process::id());
return Ok(());
}
match read_pid(path) {
Some(pid) if pid == std::process::id() => {
let _ = fs::write(path, format!("{}\n", std::process::id()));
return Ok(());
}
Some(pid) if process_running(pid) => return Err(AlreadyHeld),
Some(_) => {
if fs::remove_file(path).is_err() {
return Err(AlreadyHeld);
}
}
None => {
if marker_age(path).is_some_and(|age| age < stale_after) {
return Err(AlreadyHeld);
}
if fs::remove_file(path).is_err() {
return Err(AlreadyHeld);
}
}
}
}
Err(AlreadyHeld)
}
#[derive(Debug, Clone, Copy)]
pub struct AlreadyHeld;
pub fn is_alive(path: &Path, stale_after: Duration) -> bool {
match read_pid(path) {
Some(pid) => process_running(pid),
None => marker_age(path).is_some_and(|age| age < stale_after),
}
}
pub fn read_pid(path: &Path) -> Option<u32> {
fs::read_to_string(path).ok()?.trim().parse::<u32>().ok()
}
fn marker_age(path: &Path) -> Option<Duration> {
let modified = fs::metadata(path).ok()?.modified().ok()?;
SystemTime::now().duration_since(modified).ok()
}
pub struct PidMarker {
path: PathBuf,
}
impl PidMarker {
pub fn install(path: PathBuf) -> Result<Self, InstallError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|_| InstallError::Setup)?;
}
try_claim(&path, Duration::from_secs(WATCH_MARKER_STALE_SECS))
.map_err(|AlreadyHeld| InstallError::AlreadyHeld)?;
Ok(Self { path })
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn heartbeat(&self) {
let _ = fs::write(&self.path, std::process::id().to_string());
}
}
impl Drop for PidMarker {
fn drop(&mut self) {
if read_pid(&self.path) == Some(std::process::id()) {
let _ = fs::remove_file(&self.path);
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum InstallError {
AlreadyHeld,
Setup,
}
impl std::fmt::Display for InstallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AlreadyHeld => f.write_str("marker already held by a live process"),
Self::Setup => f.write_str("failed to prepare marker directory"),
}
}
}
impl std::error::Error for InstallError {}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[cfg(unix)]
#[test]
fn install_refuses_live_foreign_pid() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let path = dir.path().join("watch.pid");
let mut child = std::process::Command::new("sleep")
.arg("60")
.spawn()
.ok()
.unwrap_or_else(|| unreachable!());
let foreign_pid = child.id();
assert!(std::fs::write(&path, foreign_pid.to_string()).is_ok());
let result = PidMarker::install(path.clone());
let stored_after = std::fs::read_to_string(&path).ok();
let _ = child.kill();
let _ = child.wait();
assert!(
matches!(result, Err(InstallError::AlreadyHeld)),
"expected AlreadyHeld when a live foreign PID owns the marker"
);
assert_eq!(
stored_after.as_deref().map(str::trim),
Some(foreign_pid.to_string().as_str()),
"foreign marker contents must be untouched"
);
}
#[test]
fn install_clears_malformed_marker() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let path = dir.path().join("watch.pid");
assert!(std::fs::write(&path, "not-a-pid").is_ok());
let stale = SystemTime::now() - Duration::from_secs(3 * 60);
let _ = fs::File::open(&path).and_then(|f| f.set_modified(stale));
let marker = PidMarker::install(path.clone());
assert!(marker.is_ok(), "malformed marker must be reclaimable");
}
#[test]
fn install_takes_over_dead_pid() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let path = dir.path().join("watch.pid");
let dead_pid = pick_dead_pid();
assert!(std::fs::write(&path, dead_pid.to_string()).is_ok());
let marker = PidMarker::install(path.clone());
assert!(marker.is_ok(), "must reclaim a stale marker");
let stored = std::fs::read_to_string(&path).ok();
assert_eq!(
stored.as_deref().map(str::trim),
Some(std::process::id().to_string().as_str())
);
}
#[test]
fn install_adopts_handoff_with_own_pid() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let path = dir.path().join("watch.pid");
assert!(std::fs::write(&path, std::process::id().to_string()).is_ok());
let marker = PidMarker::install(path.clone());
assert!(marker.is_ok(), "must adopt parent's hand-off claim");
}
#[test]
fn drop_leaves_foreign_pid_alone() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let path = dir.path().join("watch.pid");
let marker = PidMarker::install(path.clone())
.ok()
.unwrap_or_else(|| unreachable!());
let foreign_pid = if std::process::id() == 1 { 2 } else { 1 };
assert!(std::fs::write(&path, foreign_pid.to_string()).is_ok());
drop(marker);
assert!(
path.exists(),
"drop must not remove a marker reclaimed by another process"
);
}
fn pick_dead_pid() -> u32 {
for candidate in [9_999_999u32, 8_888_888, 7_777_777] {
if !process_running(candidate) {
return candidate;
}
}
9_999_999
}
}