#![allow(dead_code, clippy::missing_const_for_fn)]
use std::path::{Path, PathBuf};
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(windows)]
{
windows_impl::process_image_stem(pid)
}
#[cfg(not(any(target_os = "linux", windows)))]
{
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(windows)]
{
windows_impl::process_creation_time(pid)
}
#[cfg(not(any(target_os = "linux", windows)))]
{
let _ = pid;
None
}
}
#[must_use]
pub fn process_start_epoch_secs(pid: u32) -> Option<u64> {
#[cfg(windows)]
{
windows_impl::process_creation_time(pid)
}
#[cfg(not(windows))]
{
let _ = pid;
None
}
}
#[must_use]
pub fn process_is_out_of_reach(pid: u32) -> bool {
#[cfg(windows)]
{
is_process_alive(pid) && process_start_time(pid).is_none()
}
#[cfg(not(windows))]
{
let _ = pid;
false
}
}
#[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)?;
}
if let Some(existing) = read_pidfile(path)
&& is_record_alive(&existing)
{
return Err(AcquireError::AlreadyRunning(existing.pid));
}
let start = record_start_time(pid)?;
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
}
const START_TIME_ATTEMPTS: u32 = 5;
const START_TIME_RETRY: Duration = Duration::from_millis(50);
fn record_start_time(pid: u32) -> Result<u64, AcquireError> {
for attempt in 0..START_TIME_ATTEMPTS {
if let Some(start) = process_start_time(pid) {
return Ok(start);
}
if !platform_reports_start_times() {
return Ok(0);
}
if attempt + 1 < START_TIME_ATTEMPTS {
std::thread::sleep(START_TIME_RETRY);
}
}
Err(AcquireError::Io(std::io::Error::other(format!(
"could not read the start time of process {pid}, so its lockfile would \
be indistinguishable from a stale one"
))))
}
const fn platform_reports_start_times() -> bool {
cfg!(any(target_os = "linux", windows))
}
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(windows)]
#[must_use]
pub fn is_process_alive(pid: u32) -> bool {
windows_impl::is_process_alive(pid)
}
#[cfg(not(any(unix, windows)))]
#[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(windows)]
pub fn force_kill(pid: u32) {
windows_impl::terminate(pid);
}
#[cfg(windows)]
pub fn force_kill_group(pgid: u32) {
for pid in descendants_of(pgid, &windows_impl::process_snapshot()) {
windows_impl::terminate(pid);
}
windows_impl::terminate(pgid);
}
#[cfg(not(any(unix, windows)))]
pub fn force_kill(_pid: u32) {}
#[cfg(not(any(unix, windows)))]
pub fn force_kill_group(_pgid: u32) {}
#[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
}
#[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
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProcRow {
pub pid: u32,
pub parent: u32,
pub created: u64,
}
#[must_use]
pub fn descendants_of(root: u32, rows: &[ProcRow]) -> Vec<u32> {
let Some(root_created) = rows.iter().find(|r| r.pid == root).map(|r| r.created) else {
return Vec::new();
};
let mut seen = std::collections::BTreeSet::from([root]);
let mut frontier = vec![(root, root_created)];
let mut found = Vec::new();
while let Some((parent, parent_created)) = frontier.pop() {
for row in rows {
if row.parent != parent || row.pid == parent || row.pid == root {
continue;
}
if parent_created != 0 && row.created != 0 && row.created < parent_created {
continue;
}
if !seen.insert(row.pid) {
continue;
}
found.push(row.pid);
frontier.push((row.pid, row.created));
}
}
found.reverse();
found
}
#[derive(Debug, Clone, Copy)]
pub enum StopRequest<'a> {
Signal,
File(&'a Path),
}
pub fn deliver_stop_request(record: &PidRecord, request: &StopRequest<'_>) -> std::io::Result<()> {
match *request {
StopRequest::Signal => {
#[cfg(unix)]
{
signal_terminate(record.pid)
}
#[cfg(not(unix))]
{
let _ = record;
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"this platform has no POSIX signals; use StopRequest::File",
))
}
}
StopRequest::File(path) => create_stop_request(path),
}
}
pub fn create_stop_request(path: &Path) -> std::io::Result<()> {
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent)?;
}
std::fs::File::create(path).map(|_| ())
}
pub fn clear_stop_request(path: &Path) {
let _ = std::fs::remove_file(path);
}
const KILL_SETTLE: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StopOutcome {
Drained,
Escalated,
Unreachable(String),
Failed,
}
impl StopOutcome {
#[must_use]
pub fn stopped(&self) -> bool {
!matches!(self, Self::Failed)
}
}
#[must_use]
pub fn stop_record(
record: &PidRecord,
timeout: Duration,
request: &StopRequest<'_>,
) -> StopOutcome {
let delivery = deliver_stop_request(record, request);
let asked = delivery.is_ok();
if asked && wait_for_record_exit(record, timeout) {
return StopOutcome::Drained;
}
if !is_record_alive(record) {
return StopOutcome::Drained;
}
force_kill_group(record.pid);
force_kill(record.pid);
if !wait_for_record_exit(record, KILL_SETTLE) {
return StopOutcome::Failed;
}
match delivery {
Ok(()) => StopOutcome::Escalated,
Err(e) => StopOutcome::Unreachable(e.to_string()),
}
}
#[must_use]
pub fn pgsignal_pipe_name(pid: u32) -> String {
format!(r"\\.\pipe\pgsignal_{pid}")
}
const PG_SIGINT: u8 = 2;
#[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_group(pid);
force_kill(pid);
}
}
#[cfg(windows)]
pub fn stop_postmaster(pid: u32, timeout: Duration) {
windows_impl::send_pg_signal(pid, PG_SIGINT);
if !wait_for_pid_exit(pid, timeout) {
force_kill_group(pid);
force_kill(pid);
}
}
#[cfg(not(any(unix, windows)))]
pub fn stop_postmaster(_pid: u32, _timeout: Duration) {
let _ = PG_SIGINT;
}
#[cfg(windows)]
mod windows_impl {
use super::ProcRow;
use std::io::Write as _;
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
const PG_SIGNAL_ATTEMPTS: u32 = 10;
const PG_SIGNAL_RETRY: std::time::Duration = std::time::Duration::from_millis(100);
const SECURITY_SQOS_PRESENT: u32 = 0x0010_0000;
const SECURITY_IDENTIFICATION: u32 = 0x0001_0000;
fn snapshot(pid: Option<u32>) -> System {
let mut system = System::new();
let refresh = ProcessRefreshKind::nothing();
match pid {
Some(pid) => {
let only = [Pid::from_u32(pid)];
system.refresh_processes_specifics(
ProcessesToUpdate::Some(&only),
true,
refresh.with_exe(sysinfo::UpdateKind::Always),
);
}
None => {
system.refresh_processes_specifics(ProcessesToUpdate::All, true, refresh);
}
}
system
}
pub(super) fn is_process_alive(pid: u32) -> bool {
if pid == 0 {
return false;
}
if snapshot(Some(pid)).process(Pid::from_u32(pid)).is_some() {
return true;
}
let all = snapshot(None);
all.processes().is_empty() || all.process(Pid::from_u32(pid)).is_some()
}
pub(super) fn process_creation_time(pid: u32) -> Option<u64> {
if pid == 0 {
return None;
}
let system = snapshot(Some(pid));
let start = system.process(Pid::from_u32(pid))?.start_time();
(start != 0).then_some(start)
}
pub(super) fn process_image_stem(pid: u32) -> Option<String> {
if pid == 0 {
return None;
}
let system = snapshot(Some(pid));
let process = system.process(Pid::from_u32(pid))?;
let name = process
.exe()
.and_then(std::path::Path::file_stem)
.map_or_else(
|| {
std::path::Path::new(process.name())
.file_stem()
.map(std::ffi::OsStr::to_os_string)
},
|stem| Some(stem.to_os_string()),
)?;
Some(name.to_string_lossy().to_lowercase())
}
pub(super) fn terminate(pid: u32) {
if pid == 0 {
return;
}
let taskkill = std::path::PathBuf::from(
std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_owned()),
)
.join("System32")
.join("taskkill.exe");
let _ = std::process::Command::new(taskkill)
.args(["/PID", &pid.to_string(), "/F"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
pub(super) fn process_snapshot() -> Vec<ProcRow> {
snapshot(None)
.processes()
.values()
.map(|process| ProcRow {
pid: process.pid().as_u32(),
parent: process.parent().map_or(0, sysinfo::Pid::as_u32),
created: process.start_time(),
})
.collect()
}
pub(super) fn send_pg_signal(pid: u32, signal: u8) -> bool {
use std::os::windows::fs::OpenOptionsExt as _;
let path = super::pgsignal_pipe_name(pid);
for attempt in 0..PG_SIGNAL_ATTEMPTS {
let delivered = std::fs::OpenOptions::new()
.read(true)
.write(true)
.security_qos_flags(SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION)
.open(&path)
.and_then(|mut pipe| pipe.write_all(&[signal]).and_then(|()| pipe.flush()))
.is_ok();
if delivered {
return true;
}
if attempt + 1 < PG_SIGNAL_ATTEMPTS {
std::thread::sleep(PG_SIGNAL_RETRY);
}
}
false
}
}
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();
}
#[test]
fn a_pidfile_records_a_real_start_time_where_the_platform_has_one() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("serve.pid");
acquire_pidfile(&path, std::process::id()).expect("acquire");
let rec = read_pidfile(&path).expect("record");
assert_eq!(
rec.start_time.is_some(),
platform_reports_start_times(),
"a platform that reports start times must not record `unknown`"
);
}
#[test]
fn a_start_time_for_a_process_that_does_not_exist_fails_the_acquire() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("serve.pid");
let outcome = acquire_pidfile(&path, 2_147_483_640);
if platform_reports_start_times() {
assert!(
matches!(outcome, Err(AcquireError::Io(_))),
"expected an Io error, got {outcome:?}"
);
assert!(!path.exists(), "no lockfile may be left behind");
} else {
assert!(outcome.is_ok());
}
}
#[test]
fn stop_request_file_is_created_on_delivery() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("nested").join("serve.stop");
let rec = PidRecord {
pid: std::process::id(),
start_time: None,
};
deliver_stop_request(&rec, &StopRequest::File(&path)).expect("deliver");
assert!(path.is_file(), "the drain request must be a regular file");
}
#[test]
fn stop_request_file_delivery_is_idempotent() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("serve.stop");
let rec = PidRecord {
pid: std::process::id(),
start_time: None,
};
deliver_stop_request(&rec, &StopRequest::File(&path)).expect("first");
deliver_stop_request(&rec, &StopRequest::File(&path)).expect("second");
assert!(path.is_file());
}
fn row(pid: u32, parent: u32, created: u64) -> ProcRow {
ProcRow {
pid,
parent,
created,
}
}
#[test]
fn descendants_are_collected_deepest_first() {
let rows = [
row(100, 1, 10),
row(200, 100, 20),
row(300, 200, 30),
row(400, 1, 40),
];
assert_eq!(descendants_of(100, &rows), vec![300, 200]);
}
#[test]
fn a_child_older_than_its_parent_is_a_reused_pid_not_a_descendant() {
let rows = [row(100, 1, 50), row(200, 100, 10)];
assert!(descendants_of(100, &rows).is_empty());
}
#[test]
fn the_root_is_never_reported_as_its_own_descendant() {
let rows = [row(100, 100, 10)];
assert!(descendants_of(100, &rows).is_empty());
}
#[test]
fn a_parent_cycle_terminates() {
let rows = [
row(100, 1, 10),
row(200, 100, 20),
row(300, 200, 30),
row(200, 300, 20),
];
let found = descendants_of(100, &rows);
assert!(found.contains(&200) && found.contains(&300), "{found:?}");
}
fn long_running_child() -> (u32, std::thread::JoinHandle<()>) {
#[cfg(unix)]
let mut cmd = {
let mut c = std::process::Command::new("sleep");
c.arg("60");
c
};
#[cfg(windows)]
let mut cmd = {
let mut c = std::process::Command::new("cmd");
c.args(["/C", "ping -n 61 127.0.0.1"]);
c
};
let mut child = cmd
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn a long-running child");
let pid = child.id();
let reaper = std::thread::spawn(move || {
let _ = child.wait();
});
(pid, reaper)
}
#[test]
fn a_child_that_ignores_the_request_is_escalated_after_the_budget() {
let dir = tempfile::tempdir().expect("tempdir");
let stop_file = dir.path().join("serve.stop");
let (pid, reaper) = long_running_child();
let rec = PidRecord {
pid,
start_time: process_start_time(pid),
};
let outcome = stop_record(
&rec,
Duration::from_millis(200),
&StopRequest::File(&stop_file),
);
assert_eq!(outcome, StopOutcome::Escalated);
assert!(outcome.stopped());
assert!(
stop_file.is_file(),
"the drain was requested before the kill"
);
reaper.join().expect("reaper");
}
#[test]
fn an_undeliverable_request_is_reported_rather_than_passed_off_as_a_drain() {
let dir = tempfile::tempdir().expect("tempdir");
let blocked = dir.path().join("serve.stop");
std::fs::create_dir(&blocked).expect("mkdir");
let (pid, reaper) = long_running_child();
let rec = PidRecord {
pid,
start_time: process_start_time(pid),
};
let began = std::time::Instant::now();
let outcome = stop_record(&rec, Duration::from_secs(30), &StopRequest::File(&blocked));
assert!(
matches!(outcome, StopOutcome::Unreachable(_)),
"expected Unreachable, got {outcome:?}"
);
assert!(outcome.stopped());
assert!(
began.elapsed() < Duration::from_secs(20),
"an unasked daemon must not be waited out"
);
reaper.join().expect("reaper");
}
#[test]
fn a_dead_daemon_reports_drained_without_killing_anything() {
let dir = tempfile::tempdir().expect("tempdir");
let rec = PidRecord {
pid: 2_147_483_640,
start_time: None,
};
assert_eq!(
stop_record(
&rec,
Duration::from_millis(50),
&StopRequest::File(&dir.path().join("serve.stop")),
),
StopOutcome::Drained
);
}
#[test]
fn pgsignal_pipe_name_matches_postgres_pgkill() {
assert_eq!(pgsignal_pipe_name(4242), r"\\.\pipe\pgsignal_4242");
}
#[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");
}
}
}
}
}