mirror of
https://github.com/telemt/telemt.git
synced 2026-10-10 11:25:57 +03:00
499 lines
16 KiB
Rust
499 lines
16 KiB
Rust
use std::ffi::OsStr;
|
|
use std::fs::{self, File};
|
|
use std::io::{self, ErrorKind, Read, Write};
|
|
use std::os::unix::fs::MetadataExt;
|
|
#[cfg(target_os = "linux")]
|
|
use std::os::fd::{FromRawFd, OwnedFd};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use nix::fcntl::{Flock, FlockArg, OFlag, openat};
|
|
use nix::sys::stat::Mode;
|
|
use nix::unistd::{Pid, UnlinkatFlags, getpid, unlinkat};
|
|
use tracing::{debug, info, warn};
|
|
|
|
use super::DaemonError;
|
|
use crate::util::secure_fs::AnchoredPath;
|
|
|
|
/// PID file manager backed by a persistent sibling lock file.
|
|
pub struct PidFile {
|
|
path: PathBuf,
|
|
lock_path: PathBuf,
|
|
pid_file: Option<File>,
|
|
pid_identity: Option<FileIdentity>,
|
|
lock_file: Option<Flock<File>>,
|
|
anchor: Option<AnchoredPath>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
struct FileIdentity {
|
|
device: u64,
|
|
inode: u64,
|
|
}
|
|
|
|
impl FileIdentity {
|
|
fn from_metadata(metadata: &fs::Metadata) -> Self {
|
|
Self {
|
|
device: metadata.dev(),
|
|
inode: metadata.ino(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PidFile {
|
|
/// Creates a new PID file manager for the given path.
|
|
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
|
let path = path.as_ref().to_path_buf();
|
|
let lock_path = sibling_lock_path(&path);
|
|
Self {
|
|
path,
|
|
lock_path,
|
|
pid_file: None,
|
|
pid_identity: None,
|
|
lock_file: None,
|
|
anchor: None,
|
|
}
|
|
}
|
|
|
|
/// Checks whether the PID file names a running process without modifying either file.
|
|
pub fn check_running(&self) -> Result<Option<i32>, DaemonError> {
|
|
let Some(pid) = read_pid_file_if_exists(&self.path)? else {
|
|
return Ok(None);
|
|
};
|
|
Ok(is_process_running(pid).then_some(pid))
|
|
}
|
|
|
|
/// Acquires the persistent sibling lock and writes the current PID.
|
|
///
|
|
/// Fails if another owner holds the lock or the existing PID names a running process.
|
|
pub fn acquire(&mut self) -> Result<(), DaemonError> {
|
|
let anchor = AnchoredPath::open_trusted_parent_or_create(&self.path, 0o755).map_err(
|
|
|error| {
|
|
DaemonError::PidFile(format!(
|
|
"cannot open trusted parent for {}: {}",
|
|
self.path.display(),
|
|
error
|
|
))
|
|
},
|
|
)?;
|
|
let lock_name = self.lock_path.file_name().ok_or_else(|| {
|
|
DaemonError::PidFile(format!(
|
|
"lock path {} has no file name",
|
|
self.lock_path.display()
|
|
))
|
|
})?;
|
|
let lock_file = open_file_at(
|
|
&anchor,
|
|
lock_name,
|
|
OFlag::O_RDWR | OFlag::O_CREAT | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW,
|
|
0o644,
|
|
)
|
|
.map_err(|error| {
|
|
DaemonError::PidFile(format!(
|
|
"cannot open lock file {}: {}",
|
|
self.lock_path.display(),
|
|
error
|
|
))
|
|
})?;
|
|
validate_regular_single_link(&lock_file, &self.lock_path)?;
|
|
let lock_file =
|
|
Flock::lock(lock_file, FlockArg::LockExclusiveNonblock).map_err(|(_, errno)| {
|
|
if let Some(pid) = read_pid_file_at(&anchor, &self.path)
|
|
.ok()
|
|
.flatten()
|
|
.filter(|pid| is_process_running(*pid))
|
|
{
|
|
DaemonError::AlreadyRunning(pid)
|
|
} else {
|
|
DaemonError::PidFile(format!(
|
|
"cannot lock {}: {}",
|
|
self.lock_path.display(),
|
|
errno
|
|
))
|
|
}
|
|
})?;
|
|
|
|
if let Some(pid) = read_pid_file_at(&anchor, &self.path)?
|
|
&& is_process_running(pid)
|
|
{
|
|
return Err(DaemonError::AlreadyRunning(pid));
|
|
}
|
|
|
|
let mut pid_file = open_file_at(
|
|
&anchor,
|
|
anchor.name(),
|
|
OFlag::O_RDWR | OFlag::O_CREAT | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW,
|
|
0o644,
|
|
)
|
|
.map_err(|error| {
|
|
DaemonError::PidFile(format!("cannot open {}: {}", self.path.display(), error))
|
|
})?;
|
|
let pid_metadata = validate_regular_single_link(&pid_file, &self.path)?;
|
|
let pid_identity = FileIdentity::from_metadata(&pid_metadata);
|
|
// Validate the opened inode before modifying it so a hard-link substitution
|
|
// cannot turn PID publication into truncation of an unrelated file.
|
|
pid_file.set_len(0).map_err(|error| {
|
|
DaemonError::PidFile(format!("cannot truncate {}: {}", self.path.display(), error))
|
|
})?;
|
|
let pid = getpid();
|
|
writeln!(pid_file, "{}", pid).map_err(|error| {
|
|
DaemonError::PidFile(format!(
|
|
"cannot write PID to {}: {}",
|
|
self.path.display(),
|
|
error
|
|
))
|
|
})?;
|
|
pid_file.sync_data().map_err(|error| {
|
|
DaemonError::PidFile(format!(
|
|
"cannot sync PID file {}: {}",
|
|
self.path.display(),
|
|
error
|
|
))
|
|
})?;
|
|
|
|
self.pid_file = Some(pid_file);
|
|
self.pid_identity = Some(pid_identity);
|
|
self.lock_file = Some(lock_file);
|
|
self.anchor = Some(anchor);
|
|
info!(pid = pid.as_raw(), path = %self.path.display(), "PID file created");
|
|
Ok(())
|
|
}
|
|
|
|
/// Removes the PID file while retaining exclusive lock ownership until cleanup completes.
|
|
pub fn release(&mut self) -> Result<(), DaemonError> {
|
|
if self.lock_file.is_none() {
|
|
self.pid_file = None;
|
|
self.pid_identity = None;
|
|
self.anchor = None;
|
|
return Ok(());
|
|
}
|
|
|
|
let removal = match self.anchor.as_ref() {
|
|
Some(anchor) => remove_owned_pid_file(anchor, &self.path, self.pid_identity),
|
|
None => Err(DaemonError::PidFile(
|
|
"PID file lock is held without a directory anchor".to_string(),
|
|
)),
|
|
};
|
|
self.pid_file = None;
|
|
self.pid_identity = None;
|
|
self.lock_file = None;
|
|
self.anchor = None;
|
|
removal?;
|
|
debug!(path = %self.path.display(), "PID file removed");
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the path to this PID file.
|
|
pub fn path(&self) -> &Path {
|
|
&self.path
|
|
}
|
|
|
|
/// Returns open files whose ownership must follow the target runtime identity.
|
|
pub(super) fn ownership_file_handles(&self) -> [Option<&File>; 2] {
|
|
[self.pid_file.as_ref(), self.lock_file.as_deref()]
|
|
}
|
|
}
|
|
|
|
impl Drop for PidFile {
|
|
fn drop(&mut self) {
|
|
if self.lock_file.is_some()
|
|
&& let Err(error) = self.release()
|
|
{
|
|
warn!(error = %error, "Failed to clean up PID file on drop");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn sibling_lock_path(path: &Path) -> PathBuf {
|
|
let mut lock_path = path.as_os_str().to_os_string();
|
|
lock_path.push(".lock");
|
|
lock_path.into()
|
|
}
|
|
|
|
fn open_file_at(
|
|
anchor: &AnchoredPath,
|
|
name: &OsStr,
|
|
flags: OFlag,
|
|
mode: u32,
|
|
) -> io::Result<File> {
|
|
let descriptor = openat(
|
|
anchor.parent(),
|
|
name,
|
|
flags,
|
|
Mode::from_bits_truncate(mode),
|
|
)
|
|
.map_err(|error| io::Error::from_raw_os_error(error as i32))?;
|
|
Ok(File::from(descriptor))
|
|
}
|
|
|
|
fn read_pid_file_if_exists(path: &Path) -> Result<Option<i32>, DaemonError> {
|
|
let anchor = match AnchoredPath::open_trusted_parent(path) {
|
|
Ok(anchor) => anchor,
|
|
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
|
|
Err(error) => {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"cannot open trusted parent for {}: {}",
|
|
path.display(),
|
|
error
|
|
)));
|
|
}
|
|
};
|
|
read_pid_file_at(&anchor, path)
|
|
}
|
|
|
|
fn read_pid_file_at(anchor: &AnchoredPath, path: &Path) -> Result<Option<i32>, DaemonError> {
|
|
let mut file = match open_file_at(
|
|
anchor,
|
|
anchor.name(),
|
|
OFlag::O_RDONLY | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW,
|
|
0,
|
|
) {
|
|
Ok(file) => file,
|
|
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
|
|
Err(error) => {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"cannot read {}: {}",
|
|
path.display(),
|
|
error
|
|
)));
|
|
}
|
|
};
|
|
let metadata = validate_regular_single_link(&file, path)?;
|
|
if metadata.len() > 64 {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"invalid PID in {}",
|
|
path.display()
|
|
)));
|
|
}
|
|
let mut contents = String::new();
|
|
file.read_to_string(&mut contents).map_err(|error| {
|
|
DaemonError::PidFile(format!("cannot read {}: {}", path.display(), error))
|
|
})?;
|
|
let pid: i32 = contents
|
|
.trim()
|
|
.parse()
|
|
.map_err(|_| DaemonError::PidFile(format!("invalid PID in {}", path.display())))?;
|
|
if pid <= 1 {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"invalid PID in {}",
|
|
path.display()
|
|
)));
|
|
}
|
|
Ok(Some(pid))
|
|
}
|
|
|
|
fn remove_owned_pid_file(
|
|
anchor: &AnchoredPath,
|
|
path: &Path,
|
|
expected: Option<FileIdentity>,
|
|
) -> Result<(), DaemonError> {
|
|
let file = match open_file_at(
|
|
anchor,
|
|
anchor.name(),
|
|
OFlag::O_RDONLY | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW,
|
|
0,
|
|
) {
|
|
Ok(file) => file,
|
|
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()),
|
|
Err(error) => {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"cannot inspect {} before removal: {}",
|
|
path.display(),
|
|
error
|
|
)));
|
|
}
|
|
};
|
|
let metadata = validate_regular_single_link(&file, path)?;
|
|
if expected != Some(FileIdentity::from_metadata(&metadata)) {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"refusing to remove replaced PID file {}",
|
|
path.display()
|
|
)));
|
|
}
|
|
drop(file);
|
|
unlinkat(
|
|
anchor.parent(),
|
|
anchor.name(),
|
|
UnlinkatFlags::NoRemoveDir,
|
|
)
|
|
.map_err(|error| {
|
|
DaemonError::PidFile(format!(
|
|
"cannot remove {}: {}",
|
|
path.display(),
|
|
io::Error::from_raw_os_error(error as i32)
|
|
))
|
|
})
|
|
}
|
|
|
|
fn validate_regular_single_link(
|
|
file: &File,
|
|
path: &Path,
|
|
) -> Result<fs::Metadata, DaemonError> {
|
|
let metadata = file.metadata().map_err(|error| {
|
|
DaemonError::PidFile(format!("cannot inspect {}: {}", path.display(), error))
|
|
})?;
|
|
if !metadata.is_file() || metadata.nlink() != 1 {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"{} must be a regular file with one directory entry",
|
|
path.display()
|
|
)));
|
|
}
|
|
Ok(metadata)
|
|
}
|
|
|
|
/// Reads a PID from a PID file.
|
|
#[allow(dead_code)]
|
|
pub fn read_pid_file<P: AsRef<Path>>(path: P) -> Result<i32, DaemonError> {
|
|
let path = path.as_ref();
|
|
read_pid_file_if_exists(path)?.ok_or_else(|| {
|
|
DaemonError::PidFile(format!(
|
|
"cannot read {}: file does not exist",
|
|
path.display()
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Sends a signal to the process specified in a PID file.
|
|
#[allow(dead_code)]
|
|
pub fn signal_pid_file<P: AsRef<Path>>(
|
|
path: P,
|
|
signal: nix::sys::signal::Signal,
|
|
) -> Result<(), DaemonError> {
|
|
let path = path.as_ref();
|
|
let pid = read_pid_file(path)?;
|
|
#[cfg(target_os = "linux")]
|
|
let pidfd = open_pidfd(pid)?;
|
|
if !daemon_lock_is_held(path)? {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"refusing to signal unlocked or stale PID file {}",
|
|
path.display()
|
|
)));
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
return signal_pidfd(&pidfd, pid, signal);
|
|
#[cfg(not(target_os = "linux"))]
|
|
nix::sys::signal::kill(Pid::from_raw(pid), signal)
|
|
.map_err(|error| DaemonError::PidFile(format!("cannot signal process {}: {}", pid, error)))
|
|
}
|
|
|
|
/// Daemon state derived from the PID file.
|
|
#[allow(dead_code)]
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum DaemonStatus {
|
|
/// Daemon is running with the given PID.
|
|
Running(i32),
|
|
/// PID file exists but the named process is not running.
|
|
Stale(i32),
|
|
/// No readable PID file exists.
|
|
NotRunning,
|
|
}
|
|
|
|
/// Checks daemon status without modifying the PID or lock file.
|
|
#[allow(dead_code)]
|
|
pub fn check_status<P: AsRef<Path>>(path: P) -> DaemonStatus {
|
|
let path = path.as_ref();
|
|
match read_pid_file_if_exists(path) {
|
|
Ok(Some(pid))
|
|
if daemon_lock_is_held(path).unwrap_or(false) && is_process_running(pid) =>
|
|
{
|
|
DaemonStatus::Running(pid)
|
|
}
|
|
Ok(Some(pid)) => DaemonStatus::Stale(pid),
|
|
Ok(None) | Err(_) => DaemonStatus::NotRunning,
|
|
}
|
|
}
|
|
|
|
fn daemon_lock_is_held(path: &Path) -> Result<bool, DaemonError> {
|
|
let lock_path = sibling_lock_path(path);
|
|
let anchor = match AnchoredPath::open_trusted_parent(path) {
|
|
Ok(anchor) => anchor,
|
|
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(false),
|
|
Err(error) => {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"cannot open trusted parent for {}: {}",
|
|
path.display(),
|
|
error
|
|
)));
|
|
}
|
|
};
|
|
let lock_name = lock_path.file_name().ok_or_else(|| {
|
|
DaemonError::PidFile(format!("lock path {} has no file name", lock_path.display()))
|
|
})?;
|
|
let file = match open_file_at(
|
|
&anchor,
|
|
lock_name,
|
|
OFlag::O_RDWR | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW,
|
|
0,
|
|
) {
|
|
Ok(file) => file,
|
|
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(false),
|
|
Err(error) => {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"cannot inspect lock {}: {}",
|
|
lock_path.display(),
|
|
error
|
|
)));
|
|
}
|
|
};
|
|
validate_regular_single_link(&file, &lock_path)?;
|
|
match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
|
|
Ok(_available) => Ok(false),
|
|
Err((_file, nix::errno::Errno::EWOULDBLOCK)) => Ok(true),
|
|
Err((_file, error)) => Err(DaemonError::PidFile(format!(
|
|
"cannot inspect lock ownership for {}: {}",
|
|
lock_path.display(),
|
|
error
|
|
))),
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn open_pidfd(pid: i32) -> Result<OwnedFd, DaemonError> {
|
|
// SAFETY: `pidfd_open` receives a validated positive PID and no pointer arguments.
|
|
let descriptor = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
|
|
if descriptor < 0 {
|
|
return Err(DaemonError::PidFile(format!(
|
|
"cannot open stable process handle for {}: {}",
|
|
pid,
|
|
std::io::Error::last_os_error()
|
|
)));
|
|
}
|
|
// SAFETY: a successful `pidfd_open` returns one newly owned descriptor.
|
|
Ok(unsafe { OwnedFd::from_raw_fd(descriptor as i32) })
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn signal_pidfd(
|
|
pidfd: &OwnedFd,
|
|
pid: i32,
|
|
signal: nix::sys::signal::Signal,
|
|
) -> Result<(), DaemonError> {
|
|
use std::os::fd::AsRawFd;
|
|
|
|
// SAFETY: the pidfd is owned and valid, and both optional pointer arguments are null.
|
|
let result = unsafe {
|
|
libc::syscall(
|
|
libc::SYS_pidfd_send_signal,
|
|
pidfd.as_raw_fd(),
|
|
signal as libc::c_int,
|
|
std::ptr::null::<libc::siginfo_t>(),
|
|
0,
|
|
)
|
|
};
|
|
if result == 0 {
|
|
Ok(())
|
|
} else {
|
|
Err(DaemonError::PidFile(format!(
|
|
"cannot signal process {} through stable handle: {}",
|
|
pid,
|
|
std::io::Error::last_os_error()
|
|
)))
|
|
}
|
|
}
|
|
|
|
fn is_process_running(pid: i32) -> bool {
|
|
nix::sys::signal::kill(Pid::from_raw(pid), None).is_ok()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|