#[cfg(unix)]
use std::time::{Duration, Instant};
use camino::Utf8Path;
use tracing::{info, warn};
use crate::observability::LOG_TARGET;
#[cfg(unix)]
const STOP_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct ProcessId(u32);
impl ProcessId {
pub(super) const fn new(raw: u32) -> Self { Self(raw) }
pub(super) const fn get(self) -> u32 { self.0 }
}
impl std::fmt::Display for ProcessId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.get().fmt(formatter)
}
}
pub(super) trait OrphanStop {
fn stop(&self, pid: ProcessId) -> bool;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ServerState {
Absent,
Running(ProcessId),
Unconfirmed(ProcessId),
}
pub(super) fn stop_orphaned_server(data_dir: &Utf8Path, stop: &dyn OrphanStop) -> bool {
match server_state(data_dir) {
ServerState::Absent => true,
ServerState::Unconfirmed(pid) => {
warn_unconfirmed(pid, data_dir);
false
}
ServerState::Running(pid) => stop_running(pid, data_dir, stop),
}
}
fn warn_unconfirmed(pid: ProcessId, data_dir: &Utf8Path) {
warn!(target: LOG_TARGET, pid = %pid, data_dir = %data_dir,
"a live process holds a dead slot's postmaster.pid and is not confirmed as postgres");
}
fn stop_running(pid: ProcessId, data_dir: &Utf8Path, stop: &dyn OrphanStop) -> bool {
let stopped = stop.stop(pid);
if stopped {
info!(target: LOG_TARGET, pid = %pid, data_dir = %data_dir, "stopped a server orphaned by a dead owner");
}
stopped
}
pub(super) fn server_state(data_dir: &Utf8Path) -> ServerState {
let Some(pid) = postmaster_pid(data_dir) else {
return ServerState::Absent;
};
if !process_alive(pid) {
return ServerState::Absent;
}
match is_server_for(pid, data_dir) {
Some(true) => ServerState::Running(pid),
Some(false) => ServerState::Absent,
None => ServerState::Unconfirmed(pid),
}
}
fn postmaster_pid(data_dir: &Utf8Path) -> Option<ProcessId> {
let text = std::fs::read_to_string(data_dir.join("postmaster.pid")).ok()?;
let raw: u32 = text.lines().next()?.trim().parse().ok()?;
(raw > 0).then_some(ProcessId::new(raw))
}
#[cfg(unix)]
fn process_alive(pid: ProcessId) -> bool {
let Ok(raw) = i32::try_from(pid.get()) else {
return false;
};
match nix::sys::signal::kill(nix::unistd::Pid::from_raw(raw), None) {
Ok(()) | Err(nix::errno::Errno::EPERM) => true,
Err(_) => false,
}
}
#[cfg(not(unix))]
const fn process_alive(_pid: ProcessId) -> bool { true }
#[cfg(target_os = "linux")]
fn is_server_for(pid: ProcessId, data_dir: &Utf8Path) -> Option<bool> {
let comm = std::fs::read_to_string(format!("/proc/{pid}/comm")).ok()?;
if !names_postgres(&comm) {
return Some(false);
}
let cwd = std::fs::read_link(format!("/proc/{pid}/cwd")).ok()?;
Some(cwd == std::fs::canonicalize(data_dir).ok()?)
}
#[cfg(all(unix, not(target_os = "linux")))]
fn is_server_for(pid: ProcessId, data_dir: &Utf8Path) -> Option<bool> {
if !names_postgres(&ps_comm(pid)?) {
return Some(false);
}
Some(lsof_cwd(pid)? == std::fs::canonicalize(data_dir).ok()?)
}
#[cfg(not(unix))]
const fn is_server_for(_pid: ProcessId, _data_dir: &Utf8Path) -> Option<bool> { None }
#[cfg(all(unix, not(target_os = "linux")))]
fn ps_comm(pid: ProcessId) -> Option<String> {
let output = std::process::Command::new("ps")
.args(["-o", "comm=", "-p", &pid.to_string()])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout).into_owned();
(output.status.success() && !text.trim().is_empty()).then_some(text)
}
#[cfg(all(unix, not(target_os = "linux")))]
fn lsof_cwd(pid: ProcessId) -> Option<std::path::PathBuf> {
let output = std::process::Command::new("lsof")
.args(["-a", "-d", "cwd", "-Fn", "-p", &pid.to_string()])
.output()
.ok()?;
if !output.status.success() {
return None;
}
parse_lsof_cwd(&String::from_utf8_lossy(&output.stdout))
}
#[cfg(all(unix, not(target_os = "linux")))]
fn parse_lsof_cwd(output: &str) -> Option<std::path::PathBuf> {
output
.lines()
.find_map(|line| line.strip_prefix('n'))
.filter(|path| !path.is_empty())
.map(std::path::PathBuf::from)
}
#[cfg(unix)]
fn names_postgres(comm: &str) -> bool {
let name = comm.trim();
name.rsplit('/').next() == Some("postgres")
}
pub(super) struct SignalStop;
impl OrphanStop for SignalStop {
#[cfg(unix)]
fn stop(&self, pid: ProcessId) -> bool {
use nix::sys::signal::{Signal, kill};
let Ok(raw) = i32::try_from(pid.get()) else {
return false;
};
if kill(nix::unistd::Pid::from_raw(raw), Signal::SIGQUIT).is_err() {
return !process_alive(pid);
}
wait_for_exit(pid)
}
#[cfg(not(unix))]
fn stop(&self, _pid: ProcessId) -> bool { false }
}
#[cfg(unix)]
fn wait_for_exit(pid: ProcessId) -> bool {
let deadline = Instant::now() + STOP_TIMEOUT;
while Instant::now() < deadline {
if !process_alive(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(50));
}
!process_alive(pid)
}
#[cfg(test)]
#[path = "orphan_tests.rs"]
pub(super) mod tests;