#![allow(dead_code, clippy::missing_const_for_fn)]
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::process::Child;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PidState {
Free,
Stale,
Alive(u32),
}
#[must_use]
pub const fn classify(recorded_pid: Option<u32>, alive: bool) -> PidState {
match recorded_pid {
Some(pid) if alive => PidState::Alive(pid),
Some(_) => PidState::Stale,
None => PidState::Free,
}
}
#[derive(Debug)]
pub enum AcquireError {
AlreadyRunning(u32),
Io(std::io::Error),
}
impl std::fmt::Display for AcquireError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AlreadyRunning(pid) => {
write!(f, "a daemon is already running (pid {pid})")
}
Self::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for AcquireError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PidRecord {
pub pid: u32,
pub start_time: Option<u64>,
}
#[must_use]
pub fn read_pidfile(path: &Path) -> Option<PidRecord> {
let contents = std::fs::read_to_string(path).ok()?;
let mut parts = contents.split_whitespace();
let pid = parts.next()?.parse::<u32>().ok()?;
let start_time = parts
.next()
.and_then(|s| s.parse::<u64>().ok())
.filter(|&s| s != 0);
Some(PidRecord { pid, start_time })
}
#[must_use]
pub fn process_command_name(pid: u32) -> Option<String> {
#[cfg(target_os = "linux")]
{
std::fs::read_to_string(format!("/proc/{pid}/comm"))
.ok()
.map(|s| s.trim().to_owned())
}
#[cfg(not(target_os = "linux"))]
{
let _ = pid;
None
}
}
#[must_use]
pub fn process_cwd(pid: u32) -> Option<PathBuf> {
#[cfg(target_os = "linux")]
{
std::fs::read_link(format!("/proc/{pid}/cwd")).ok()
}
#[cfg(not(target_os = "linux"))]
{
let _ = pid;
None
}
}
#[must_use]
pub fn process_start_time(pid: u32) -> Option<u64> {
#[cfg(target_os = "linux")]
{
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let rest = stat.rsplit_once(')')?.1;
rest.split_whitespace().nth(19)?.parse::<u64>().ok()
}
#[cfg(not(target_os = "linux"))]
{
let _ = pid;
None
}
}
#[must_use]
pub fn is_record_alive(record: &PidRecord) -> bool {
if !is_process_alive(record.pid) {
return false;
}
match (record.start_time, process_start_time(record.pid)) {
(Some(recorded), Some(current)) => recorded == current,
_ => true,
}
}
pub fn acquire_pidfile(path: &Path, pid: u32) -> Result<(), AcquireError> {
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent).map_err(AcquireError::Io)?;
}
let start = process_start_time(pid).unwrap_or(0);
let parent = path.parent().filter(|p| !p.as_os_str().is_empty());
let file_name = path
.file_name()
.ok_or_else(|| AcquireError::Io(std::io::Error::other("pidfile path has no file name")))?
.to_string_lossy()
.into_owned();
let temp_name = format!(".{file_name}.tmp.{}", std::process::id());
let temp = parent.map_or_else(|| PathBuf::from(&temp_name), |p| p.join(&temp_name));
let _ = std::fs::remove_file(&temp);
if let Err(e) = std::fs::write(&temp, format!("{pid} {start}\n")) {
return Err(AcquireError::Io(e));
}
let result = acquire_via_link(&temp, path);
let _ = std::fs::remove_file(&temp);
result
}
fn acquire_via_link(temp: &Path, path: &Path) -> Result<(), AcquireError> {
for attempt in 0..2 {
match std::fs::hard_link(temp, path) {
Ok(()) => return Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
let recorded = read_pidfile(path);
let alive = recorded.as_ref().is_some_and(is_record_alive);
match classify(recorded.map(|r| r.pid), alive) {
PidState::Alive(existing) => {
return Err(AcquireError::AlreadyRunning(existing));
}
PidState::Stale | PidState::Free if attempt == 0 => {
if let Err(err) = std::fs::remove_file(path)
&& err.kind() != std::io::ErrorKind::NotFound
{
return Err(AcquireError::Io(err));
}
}
_ => return Err(AcquireError::Io(e)),
}
}
Err(e) => return Err(AcquireError::Io(e)),
}
}
Err(AcquireError::Io(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"could not acquire pid lockfile after reclaiming stale lock",
)))
}
#[cfg(unix)]
#[must_use]
pub fn validate_pid_for_kill(pid: u32) -> Option<libc::pid_t> {
let cast_pid = pid.try_into().ok()?;
if cast_pid > 0 { Some(cast_pid) } else { None }
}
#[cfg(unix)]
#[must_use]
pub fn is_process_alive(pid: u32) -> bool {
let Some(p) = validate_pid_for_kill(pid) else {
return false;
};
match nix::sys::signal::kill(nix::unistd::Pid::from_raw(p), None) {
Ok(()) | Err(nix::errno::Errno::EPERM) => true,
Err(_) => false,
}
}
#[cfg(not(unix))]
#[must_use]
pub fn is_process_alive(_pid: u32) -> bool {
true
}
#[cfg(unix)]
pub fn signal_terminate(pid: u32) -> std::io::Result<()> {
let p = validate_pid_for_kill(pid)
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid pid"))?;
nix::sys::signal::kill(
nix::unistd::Pid::from_raw(p),
nix::sys::signal::Signal::SIGTERM,
)
.map_err(|e| std::io::Error::other(e.to_string()))
}
#[cfg(unix)]
pub fn force_kill(pid: u32) {
if let Some(p) = validate_pid_for_kill(pid) {
let _ = nix::sys::signal::kill(
nix::unistd::Pid::from_raw(p),
nix::sys::signal::Signal::SIGKILL,
);
}
}
#[cfg(unix)]
pub fn force_kill_group(pgid: u32) {
if let Some(p) = validate_pid_for_kill(pgid) {
let _ = nix::sys::signal::killpg(
nix::unistd::Pid::from_raw(p),
nix::sys::signal::Signal::SIGKILL,
);
}
}
#[cfg(not(unix))]
pub fn force_kill_group(_pgid: u32) {}
#[cfg(unix)]
#[must_use]
pub fn wait_for_pid_exit(pid: u32, timeout: Duration) -> bool {
let start = std::time::Instant::now();
while is_process_alive(pid) {
if start.elapsed() >= timeout {
return false;
}
std::thread::sleep(Duration::from_millis(50));
}
true
}
#[cfg(unix)]
#[must_use]
pub fn wait_for_record_exit(record: &PidRecord, timeout: Duration) -> bool {
let start = std::time::Instant::now();
while is_record_alive(record) {
if start.elapsed() >= timeout {
return false;
}
std::thread::sleep(Duration::from_millis(50));
}
true
}
#[cfg(unix)]
#[must_use]
pub fn stop_record(record: &PidRecord, timeout: Duration, socket: &Path) -> bool {
let _ = signal_terminate(record.pid);
if wait_for_record_exit(record, timeout) {
return true;
}
if !is_record_alive(record) {
return true;
}
let _ = socket;
force_kill(record.pid);
force_kill_group(record.pid);
wait_for_record_exit(record, Duration::from_secs(5))
}
#[cfg(unix)]
pub fn stop_postmaster(pid: u32, timeout: Duration) {
if let Some(p) = validate_pid_for_kill(pid) {
let _ = nix::sys::signal::kill(
nix::unistd::Pid::from_raw(p),
nix::sys::signal::Signal::SIGINT,
);
}
if !wait_for_pid_exit(pid, timeout) {
force_kill(pid);
force_kill_group(pid);
}
}
#[cfg(not(unix))]
pub fn stop_postmaster(_pid: u32, _timeout: Duration) {}
#[cfg(not(unix))]
#[must_use]
pub fn stop_record(_record: &PidRecord, _timeout: Duration, _socket: &Path) -> bool {
false
}
#[cfg(unix)]
pub fn wait_with_timeout(child: &mut Child, timeout: Duration) -> Result<(), ()> {
let start = std::time::Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => return Ok(()),
Ok(None) => {
if start.elapsed() >= timeout {
return Err(());
}
std::thread::sleep(Duration::from_millis(50));
}
Err(_) => return Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pid_state_free_creates() {
assert_eq!(classify(None, false), PidState::Free);
assert_eq!(classify(None, true), PidState::Free);
}
#[test]
fn pid_state_alive_rejected() {
assert_eq!(classify(Some(123), true), PidState::Alive(123));
}
#[test]
fn pid_state_stale_reclaimed() {
assert_eq!(classify(Some(123), false), PidState::Stale);
}
#[test]
fn write_pidfile_roundtrips() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("serve.pid");
let pid = std::process::id();
acquire_pidfile(&path, pid).expect("acquire");
assert_eq!(read_pidfile(&path).map(|r| r.pid), Some(pid));
}
#[test]
fn read_pidfile_parses_pid_and_start_time() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("serve.pid");
std::fs::write(&path, "4242 99887766\n").expect("write");
let rec = read_pidfile(&path).expect("record");
assert_eq!(rec.pid, 4242);
assert_eq!(rec.start_time, Some(99_887_766));
}
#[test]
fn read_pidfile_back_compat_pid_only() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("serve.pid");
std::fs::write(&path, "4242\n").expect("write");
let rec = read_pidfile(&path).expect("record");
assert_eq!(rec.pid, 4242);
assert_eq!(rec.start_time, None);
}
#[cfg(target_os = "linux")]
#[test]
fn record_with_mismatched_start_time_is_not_alive() {
let rec = PidRecord {
pid: std::process::id(),
start_time: Some(1),
};
assert!(!is_record_alive(&rec));
}
#[cfg(target_os = "linux")]
#[test]
fn record_with_matching_start_time_is_alive() {
let pid = std::process::id();
let rec = PidRecord {
pid,
start_time: process_start_time(pid),
};
assert!(is_record_alive(&rec));
}
#[test]
fn create_new_rejects_live_pid() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("serve.pid");
acquire_pidfile(&path, std::process::id()).expect("first acquire");
match acquire_pidfile(&path, std::process::id() + 1) {
Err(AcquireError::AlreadyRunning(pid)) => assert_eq!(pid, std::process::id()),
other => panic!("expected AlreadyRunning, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn stale_pidfile_reclaimed() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("serve.pid");
std::fs::write(&path, "2147483640\n").expect("seed stale pidfile");
acquire_pidfile(&path, std::process::id()).expect("stale lock should be reclaimed");
assert_eq!(read_pidfile(&path).map(|r| r.pid), Some(std::process::id()));
}
#[cfg(unix)]
#[test]
fn current_process_is_alive() {
assert!(is_process_alive(std::process::id()));
}
#[cfg(unix)]
#[test]
fn high_unused_pid_is_not_alive() {
assert!(!is_process_alive(2_147_483_640));
}
#[cfg(unix)]
#[test]
fn wait_with_timeout_succeeds_for_fast_process() {
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn true");
std::thread::sleep(Duration::from_millis(50));
assert!(wait_with_timeout(&mut child, Duration::from_secs(2)).is_ok());
}
#[cfg(unix)]
#[test]
fn wait_with_timeout_times_out_for_long_process() {
let mut child = std::process::Command::new("sleep")
.arg("60")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn sleep");
assert!(wait_with_timeout(&mut child, Duration::from_millis(100)).is_err());
let _ = child.kill();
let _ = child.wait();
}
#[cfg(unix)]
mod havoc_proptest {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn safe_pid_cast(pid in proptest::num::u32::ANY) {
if let Some(safe_pid) = validate_pid_for_kill(pid) {
assert!(safe_pid > 0, "Safe PID must be strictly positive");
}
}
}
}
}