use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
pub fn default_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".anvil-runner")
.join("runner.pid")
}
pub fn instance_path(instance: &str) -> PathBuf {
let dir = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".anvil-runner");
if instance == "default" {
dir.join("runner.pid")
} else {
dir.join(format!("runner-{instance}.pid"))
}
}
#[derive(Debug)]
pub enum Existing {
None,
Live(i32),
Stale(i32),
}
pub fn inspect(path: &Path) -> Existing {
let Ok(contents) = fs::read_to_string(path) else {
return Existing::None;
};
let Ok(pid) = contents.trim().parse::<i32>() else {
return Existing::Stale(0);
};
if pid_is_alive(pid) {
Existing::Live(pid)
} else {
Existing::Stale(pid)
}
}
#[cfg(unix)]
fn pid_is_alive(pid: i32) -> bool {
if pid <= 0 {
return false;
}
let rc = unsafe { libc::kill(pid, 0) };
if rc == 0 {
return true;
}
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
errno == libc::EPERM
}
#[cfg(not(unix))]
fn pid_is_alive(_pid: i32) -> bool {
true
}
pub struct Guard {
path: PathBuf,
}
impl Guard {
pub fn acquire(path: PathBuf, pid: i32) -> std::io::Result<Self> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let tmp = path.with_extension(format!("tmp.{pid}"));
{
let mut file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp)?;
writeln!(file, "{pid}")?;
file.sync_all()?;
}
fs::rename(&tmp, &path)?;
Ok(Guard { path })
}
#[allow(dead_code)]
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for Guard {
fn drop(&mut self) {
let my_pid = std::process::id().to_string();
if let Ok(contents) = fs::read_to_string(&self.path) {
if contents.trim() == my_pid {
let _ = fs::remove_file(&self.path);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_pid_path(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"anvil-runner-pid-test-{}-{tag}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir.join("runner.pid")
}
#[test]
fn inspect_absent_returns_none() {
let path = temp_pid_path("absent");
let _ = std::fs::remove_file(&path);
assert!(matches!(inspect(&path), Existing::None));
}
#[test]
fn inspect_garbage_returns_stale() {
let path = temp_pid_path("garbage");
std::fs::write(&path, "not-a-pid\n").unwrap();
assert!(matches!(inspect(&path), Existing::Stale(_)));
let _ = std::fs::remove_file(&path);
}
#[test]
fn inspect_dead_pid_returns_stale() {
let path = temp_pid_path("dead");
std::fs::write(&path, "2147483647\n").unwrap();
assert!(matches!(inspect(&path), Existing::Stale(_)));
let _ = std::fs::remove_file(&path);
}
#[cfg(unix)]
#[test]
fn inspect_live_pid_returns_live() {
let path = temp_pid_path("live");
let my_pid = std::process::id() as i32;
std::fs::write(&path, format!("{my_pid}\n")).unwrap();
assert!(matches!(inspect(&path), Existing::Live(p) if p == my_pid));
let _ = std::fs::remove_file(&path);
}
#[test]
fn guard_writes_pid_and_removes_on_drop_when_pid_matches() {
let path = temp_pid_path("guard");
let _ = std::fs::remove_file(&path);
let my_pid = std::process::id() as i32;
{
let guard = Guard::acquire(path.clone(), my_pid).unwrap();
assert!(path.exists());
let contents = std::fs::read_to_string(guard.path()).unwrap();
assert_eq!(contents.trim(), my_pid.to_string());
}
assert!(
!path.exists(),
"guard should remove file on drop when it still holds our PID"
);
}
#[test]
fn guard_leaves_file_alone_on_drop_when_pid_was_clobbered() {
let path = temp_pid_path("clobber");
let _ = std::fs::remove_file(&path);
let my_pid = std::process::id() as i32;
{
let _guard = Guard::acquire(path.clone(), my_pid).unwrap();
std::fs::write(&path, "99999\n").unwrap();
}
assert!(
path.exists(),
"guard should NOT remove a file containing someone else's PID"
);
assert_eq!(std::fs::read_to_string(&path).unwrap().trim(), "99999");
let _ = std::fs::remove_file(&path);
}
}