use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[cfg(unix)]
pub(crate) const RESTART_PID_FD_ENV: &str = "_ARF_INTERNAL_RESTART_PID_FD";
#[cfg(unix)]
static INHERITED_PID_FD: std::sync::OnceLock<Option<std::os::unix::io::RawFd>> =
std::sync::OnceLock::new();
#[cfg(unix)]
static INHERITED_PID_FD_VALID: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
#[cfg(unix)]
static OWNED_PID_FD: std::sync::OnceLock<std::sync::Mutex<Option<std::fs::File>>> =
std::sync::OnceLock::new();
static INITIAL_PID_FILE_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
pub(crate) fn capture_restart_context() {
#[cfg(unix)]
{
let fd = unsafe {
let value = std::env::var_os(RESTART_PID_FD_ENV);
std::env::remove_var(RESTART_PID_FD_ENV);
value
.and_then(|value| value.to_str()?.parse::<std::os::unix::io::RawFd>().ok())
.filter(|fd| *fd >= 3)
};
let _ = INHERITED_PID_FD.set(fd);
}
}
pub(crate) fn set_initial_pid_file_path(path: &Path) {
let resolved = absolute_path(path);
let _ = INITIAL_PID_FILE_PATH.set(resolved);
}
#[cfg(not(unix))]
pub(crate) fn initial_pid_file_path() -> Option<PathBuf> {
INITIAL_PID_FILE_PATH.get().cloned()
}
#[cfg(unix)]
pub(crate) fn authorize_inherited_pid_fd(path: &Path) {
let valid = INHERITED_PID_FD.get().copied().flatten().is_some_and(|fd| {
validate_inherited_pid_fd(fd, path, std::process::id().to_string().as_bytes())
});
let _ = INHERITED_PID_FD_VALID.set(valid);
}
#[cfg(unix)]
fn owned_pid_fd() -> &'static std::sync::Mutex<Option<std::fs::File>> {
OWNED_PID_FD.get_or_init(|| std::sync::Mutex::new(None))
}
#[cfg(unix)]
pub(crate) fn restart_fd_carrier() -> Option<std::ffi::OsString> {
use std::os::unix::io::AsRawFd;
if INHERITED_PID_FD_VALID.get().copied() == Some(true) {
let fd = INHERITED_PID_FD.get().copied().flatten()?;
return Some(fd.to_string().into());
}
let guard = owned_pid_fd().lock().ok()?;
guard
.as_ref()
.map(|file| file.as_raw_fd().to_string().into())
}
#[cfg(unix)]
pub(crate) fn finish_loader_reexec() {
if INHERITED_PID_FD_VALID.get().copied() == Some(true)
&& let Some(fd) = INHERITED_PID_FD.get().copied().flatten()
{
let _ = set_fd_cloexec(fd, true);
}
}
pub(crate) fn write_pid_file(path: &std::path::Path) -> Result<()> {
let pid = std::process::id().to_string();
#[cfg(unix)]
if let Some(fd) = INHERITED_PID_FD.get().copied().flatten() {
use std::os::unix::io::FromRawFd;
if validate_inherited_pid_fd(fd, path, pid.as_bytes()) {
let file = unsafe { std::fs::File::from_raw_fd(fd) };
*owned_pid_fd().lock().unwrap() = Some(file);
log::info!("Adopted PID file for restart: {}", path.display());
return Ok(());
}
}
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
.with_context(|| format!("Failed to create PID file: {}", path.display()))?;
file.write_all(pid.as_bytes())
.with_context(|| format!("Failed to write PID file: {}", path.display()))?;
*owned_pid_fd().lock().unwrap() = Some(move_file_to_safe_fd(file)?);
}
#[cfg(not(unix))]
{
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.and_then(|mut f| {
use std::io::Write;
f.write_all(pid.as_bytes())
})
.with_context(|| format!("Failed to create PID file: {}", path.display()))?;
}
log::info!("PID file written: {}", path.display());
Ok(())
}
#[cfg(unix)]
#[cfg(test)]
fn pid_file_contains_current_pid(path: &Path, expected: &[u8]) -> bool {
use std::io::Read;
use std::os::unix::fs::OpenOptionsExt;
let Ok(file) = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
else {
return false;
};
let Ok(metadata) = file.metadata() else {
return false;
};
if !metadata.file_type().is_file() {
return false;
}
let mut contents = Vec::with_capacity(expected.len() + 1);
if file
.take(expected.len() as u64 + 1)
.read_to_end(&mut contents)
.is_err()
{
return false;
}
contents == expected
}
#[cfg(unix)]
fn validate_inherited_pid_fd(fd: std::os::unix::io::RawFd, path: &Path, expected: &[u8]) -> bool {
use std::io::{Read, Seek, SeekFrom};
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::os::unix::io::FromRawFd;
let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
if duplicate < 0 {
return false;
}
let file = unsafe { std::fs::File::from_raw_fd(duplicate) };
let fd_meta = match file.metadata() {
Ok(meta) if meta.file_type().is_file() => meta,
_ => return false,
};
let path_file = match std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
{
Ok(file) => file,
Err(_) => return false,
};
let path_meta = match path_file.metadata() {
Ok(meta) if meta.file_type().is_file() => meta,
_ => return false,
};
if fd_meta.dev() != path_meta.dev() || fd_meta.ino() != path_meta.ino() {
return false;
}
let mut contents = Vec::with_capacity(expected.len() + 1);
let mut reader = file;
if reader.seek(SeekFrom::Start(0)).is_err()
|| reader
.take(expected.len() as u64 + 1)
.read_to_end(&mut contents)
.is_err()
{
return false;
}
contents == expected
}
#[cfg(unix)]
pub(crate) struct PidFdExecGuard {
fd: std::os::unix::io::RawFd,
}
#[cfg(unix)]
impl Drop for PidFdExecGuard {
fn drop(&mut self) {
let _ = set_fd_cloexec(self.fd, true);
}
}
#[cfg(unix)]
pub(crate) fn prepare_pid_fd_for_exec() -> Option<anyhow::Result<PidFdExecGuard>> {
use std::os::unix::io::AsRawFd;
let guard = owned_pid_fd().lock().ok()?;
let fd = guard.as_ref()?.as_raw_fd();
Some(set_fd_cloexec(fd, false).map(|()| PidFdExecGuard { fd }))
}
#[cfg(unix)]
fn set_fd_cloexec(fd: std::os::unix::io::RawFd, enabled: bool) -> anyhow::Result<()> {
let current = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if current < 0 {
return Err(anyhow::anyhow!(std::io::Error::last_os_error()));
}
let flags = if enabled {
current | libc::FD_CLOEXEC
} else {
current & !libc::FD_CLOEXEC
};
if unsafe { libc::fcntl(fd, libc::F_SETFD, flags) } < 0 {
return Err(anyhow::anyhow!(std::io::Error::last_os_error()));
}
Ok(())
}
#[cfg(unix)]
fn move_file_to_safe_fd(file: std::fs::File) -> anyhow::Result<std::fs::File> {
use std::os::unix::io::{FromRawFd, IntoRawFd};
let raw = file.into_raw_fd();
let safe = unsafe { libc::fcntl(raw, libc::F_DUPFD_CLOEXEC, 3) };
if safe < 0 {
unsafe { libc::close(raw) };
return Err(anyhow::anyhow!(std::io::Error::last_os_error()));
}
unsafe { libc::close(raw) };
Ok(unsafe { std::fs::File::from_raw_fd(safe) })
}
pub(crate) fn absolute_pid_file_path(path: &std::path::Path) -> std::path::PathBuf {
INITIAL_PID_FILE_PATH
.get()
.cloned()
.unwrap_or_else(|| absolute_path(path))
}
fn absolute_path(path: &Path) -> PathBuf {
std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf())
}
static IPC_PID_FILE_PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
static IPC_PID_FILE_CLEANUP_DONE: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub(crate) fn register_ipc_pid_file_atexit(path: &std::path::Path) {
let _ = IPC_PID_FILE_PATH.set(absolute_pid_file_path(path));
let ret = unsafe { libc::atexit(remove_ipc_pid_file_at_exit) };
if ret != 0 {
log::warn!("Failed to register IPC PID file cleanup with atexit");
}
}
extern "C" fn remove_ipc_pid_file_at_exit() {
if IPC_PID_FILE_CLEANUP_DONE.load(std::sync::atomic::Ordering::Acquire) {
return;
}
if let Some(path) = IPC_PID_FILE_PATH.get() {
let _ = std::fs::remove_file(path);
}
}
pub(crate) fn cleanup_ipc_pid_file(path: &std::path::Path) {
let cleanup_path = IPC_PID_FILE_PATH
.get()
.cloned()
.unwrap_or_else(|| absolute_pid_file_path(path));
if let Err(e) = std::fs::remove_file(&cleanup_path) {
log::debug!(
"Could not remove PID file {}: {}",
cleanup_path.display(),
e
);
}
IPC_PID_FILE_CLEANUP_DONE.store(true, std::sync::atomic::Ordering::Release);
}
#[cfg(not(unix))]
pub(crate) fn relinquish_pid_file_for_restart(path: &Path) -> Result<()> {
let cleanup_path = IPC_PID_FILE_PATH
.get()
.cloned()
.unwrap_or_else(|| absolute_pid_file_path(path));
std::fs::remove_file(&cleanup_path).with_context(|| {
format!(
"Failed to relinquish PID file before restart: {}",
cleanup_path.display()
)
})?;
IPC_PID_FILE_CLEANUP_DONE.store(true, std::sync::atomic::Ordering::Release);
Ok(())
}
#[cfg(all(test, unix))]
mod tests {
use super::{pid_file_contains_current_pid, validate_inherited_pid_fd};
use std::os::unix::io::AsRawFd;
#[test]
fn adoption_check_requires_exact_regular_file_contents() {
let directory = tempfile::tempdir().expect("create temp directory");
let path = directory.path().join("arf.pid");
std::fs::write(&path, b"1234").expect("write PID file");
assert!(pid_file_contains_current_pid(&path, b"1234"));
assert!(!pid_file_contains_current_pid(&path, b"123"));
std::fs::write(&path, b"1234\n").expect("write non-exact PID file");
assert!(!pid_file_contains_current_pid(&path, b"1234"));
}
#[test]
fn adoption_check_rejects_symlink() {
let directory = tempfile::tempdir().expect("create temp directory");
let target = directory.path().join("target");
let link = directory.path().join("arf.pid");
std::fs::write(&target, b"1234").expect("write target file");
std::os::unix::fs::symlink(&target, &link).expect("create symlink");
assert!(!pid_file_contains_current_pid(&link, b"1234"));
}
#[test]
fn inherited_fd_validation_requires_matching_inode_and_content() {
let directory = tempfile::tempdir().expect("create temp directory");
let path = directory.path().join("arf.pid");
let other = directory.path().join("other.pid");
std::fs::write(&path, b"1234").expect("write PID file");
std::fs::write(&other, b"1234").expect("write other file");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.expect("open PID file");
let fd = file.as_raw_fd();
assert!(validate_inherited_pid_fd(fd, &path, b"1234"));
assert!(!validate_inherited_pid_fd(fd, &other, b"1234"));
assert!(!validate_inherited_pid_fd(fd, &path, b"5678"));
}
#[test]
fn inherited_fd_validation_rejects_symlink_and_invalid_fd_without_closing() {
let directory = tempfile::tempdir().expect("create temp directory");
let target = directory.path().join("target");
let link = directory.path().join("arf.pid");
std::fs::write(&target, b"1234").expect("write target");
std::os::unix::fs::symlink(&target, &link).expect("create symlink");
let file = std::fs::File::open(&target).expect("open target");
let fd = file.as_raw_fd();
assert!(!validate_inherited_pid_fd(fd, &link, b"1234"));
assert!(!validate_inherited_pid_fd(999_999, &target, b"1234"));
assert_eq!(file.metadata().expect("descriptor remains open").len(), 4);
}
}