use anyhow::{Context, Result};
use std::io::{ErrorKind, Write};
use std::path::PathBuf;
pub struct Pidfile {
path: PathBuf,
}
impl Pidfile {
pub fn new(path: PathBuf) -> Self {
Self { path }
}
pub fn acquire(&self) -> Result<()> {
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(&self.path)
.with_context(|| format!("failed to create pidfile at {}", self.path.display()))?;
let pid = std::process::id();
file.write_all(format!("{pid}\n").as_bytes())
.with_context(|| format!("failed to write pid to {}", self.path.display()))?;
file.sync_all()
.with_context(|| format!("failed to fsync pidfile {}", self.path.display()))?;
Ok(())
}
pub fn release(&self) -> Result<()> {
match std::fs::remove_file(&self.path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
Err(err) => Err(err)
.with_context(|| format!("failed to remove pidfile {}", self.path.display())),
}
}
pub fn read(&self) -> Result<Option<u32>> {
let raw = match std::fs::read_to_string(&self.path) {
Ok(text) => text,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(err)
.with_context(|| format!("failed to read pidfile {}", self.path.display()));
}
};
let trimmed = raw.trim();
trimmed.parse::<u32>().map(Some).map_err(|err| {
anyhow::anyhow!(
"pidfile {} contains unparseable content {:?}: {err}",
self.path.display(),
trimmed
)
})
}
}
#[cfg(unix)]
pub fn process_alive(pid: u32) -> Result<bool> {
let ret = unsafe { libc::kill(pid as libc::pid_t, 0) };
if ret == 0 {
return Ok(true);
}
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(libc::ESRCH) => Ok(false),
Some(libc::EPERM) => Ok(true),
_ => Err(err).with_context(|| format!("kill({pid}, 0) failed")),
}
}
#[cfg(not(unix))]
pub fn process_alive(_pid: u32) -> Result<bool> {
Ok(false)
}
#[cfg(target_os = "linux")]
pub fn process_name(pid: u32) -> Option<String> {
let raw = std::fs::read_to_string(format!("/proc/{pid}/comm")).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
#[cfg(all(unix, not(target_os = "linux")))]
pub fn process_name(pid: u32) -> Option<String> {
use std::process::Command;
let output = Command::new("ps")
.args(["-p", &pid.to_string(), "-o", "comm="])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8(output.stdout).ok()?;
let trimmed = stdout.trim();
if trimmed.is_empty() {
return None;
}
let basename = std::path::Path::new(trimmed)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(trimmed)
.to_string();
Some(basename)
}
#[cfg(not(unix))]
pub fn process_name(_pid: u32) -> Option<String> {
None
}
#[cfg(test)]
mod tests {
use super::{Pidfile, process_alive};
use tempfile::TempDir;
fn make_path() -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("ccs-proxy.pid");
(dir, path)
}
#[test]
fn acquire_writes_pid_to_file() {
let (_dir, path) = make_path();
let pidfile = Pidfile::new(path.clone());
pidfile.acquire().expect("acquire");
let raw = std::fs::read_to_string(&path).expect("read pidfile");
let parsed: u32 = raw.trim().parse().expect("parse pid");
assert_eq!(parsed, std::process::id());
}
#[test]
fn acquire_errors_when_file_already_exists() {
let (_dir, path) = make_path();
let pidfile = Pidfile::new(path);
pidfile.acquire().expect("first acquire");
let second = pidfile.acquire();
assert!(second.is_err(), "second acquire must fail");
}
#[test]
fn release_removes_file() {
let (_dir, path) = make_path();
let pidfile = Pidfile::new(path.clone());
pidfile.acquire().expect("acquire");
pidfile.release().expect("release");
assert!(!path.exists(), "pidfile should be gone after release");
pidfile.release().expect("release is idempotent");
}
#[test]
fn read_missing_file_returns_none() {
let (_dir, path) = make_path();
let pidfile = Pidfile::new(path);
assert!(pidfile.read().expect("read").is_none());
}
#[test]
fn read_unparseable_returns_err() {
let (_dir, path) = make_path();
std::fs::write(&path, "hello\n").expect("write garbage");
let pidfile = Pidfile::new(path);
assert!(pidfile.read().is_err());
}
#[test]
fn read_returns_pid_for_valid_file() {
let (_dir, path) = make_path();
let pidfile = Pidfile::new(path);
pidfile.acquire().expect("acquire");
let pid = pidfile.read().expect("read");
assert_eq!(pid, Some(std::process::id()));
}
#[test]
fn process_alive_for_self() {
let alive = process_alive(std::process::id()).expect("query self");
assert!(alive);
}
#[cfg(unix)]
#[test]
fn process_alive_for_pid_1() {
let alive = process_alive(1).expect("query pid 1");
assert!(alive, "PID 1 must be reported alive on Unix");
}
#[test]
fn process_alive_for_high_unused_pid() {
let alive = process_alive(0xFFFF_FFFE).expect("query unused pid");
assert!(!alive);
}
#[cfg(unix)]
#[test]
fn acquire_sets_unix_0600_perms() {
use std::os::unix::fs::PermissionsExt;
let (_dir, path) = make_path();
let pidfile = Pidfile::new(path.clone());
pidfile.acquire().expect("acquire");
let meta = std::fs::metadata(&path).expect("stat");
let mode = meta.permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "pidfile must be 0600, got {mode:o}");
}
}