Files
zesdex/src/model/session_lock.rs
T

93 lines
3.4 KiB
Rust
Raw Normal View History

//! PID-file based advisory lock preventing two processes from operating on
//! the same session directory concurrently.
use std::path::{Path, PathBuf};
use std::fs;
/// A PID-file lock (`<session_dir>/.lock`) tied to the current process,
/// auto-removed on drop.
pub struct SessionLock {
path: PathBuf,
pid: u32,
}
impl SessionLock {
/// Construct a lock handle for a session directory (does not acquire
/// the lock yet — call `try_lock`).
pub fn new(session_dir: &Path) -> Self {
SessionLock {
path: session_dir.join(".lock"),
pid: std::process::id(),
}
}
/// Attempt to acquire the session lock.
///
/// Flow: if `.lock` exists, read the PID inside it and check
/// `is_alive` — if that process is still running, fail to acquire →
/// otherwise (no lock file, unreadable PID, or dead owner) write our
/// own PID into `.lock` and succeed.
///
/// Why: a stale lock file from a crashed process must not permanently
/// block new sessions, so liveness is re-checked via `kill(pid, 0)`
/// rather than trusting the file's mere existence.
///
/// Return: `Ok(true)` if acquired, `Ok(false)` if another live
/// process holds it, `Err` on I/O failure.
pub fn try_lock(&self) -> std::io::Result<bool> {
if self.path.exists() {
let content = fs::read_to_string(&self.path).unwrap_or_default();
if let Ok(pid) = content.trim().parse::<u32>() {
if self.is_alive(pid) {
return Ok(false);
}
}
}
fs::write(&self.path, self.pid.to_string())?;
Ok(true)
}
/// Explicitly release the lock by removing the lock file.
pub fn unlock(&self) {
let _ = fs::remove_file(&self.path);
}
/// Check whether a process with the given PID is currently alive and
/// is actually a zesdex process (not a recycled PID from a different
/// program).
fn is_alive(&self, pid: u32) -> bool {
// SAFETY: `libc::kill(pid, 0)` does not send a signal; it only checks
// whether the process exists and the caller has permission to signal
// it. The integer argument is a PID already validated by `try_lock`.
if unsafe { libc::kill(pid as i32, 0) != 0 } {
return false;
}
// Extra check: verify the PID belongs to a zesdex process via
// /proc/<pid>/exe to mitigate the PID-reuse race (a recycled PID
// from a different program would answer kill but shouldn't hold
// our lock). This is best-effort — /proc may not be available
// on all platforms.
let proc_exe = std::path::PathBuf::from(format!("/proc/{}/exe", pid));
match std::fs::read_link(&proc_exe) {
Ok(target) => match std::env::current_exe() {
Ok(exe) => {
if target != exe {
return false;
}
}
Err(_) => { /* cannot resolve own exe, trust kill check */ }
},
Err(_) => { /* /proc unavailable, trust kill check */ }
}
true
}
}
impl Drop for SessionLock {
/// Release the lock automatically when the guard goes out of scope,
/// so an ungracefully-exited process doesn't leave a dangling lock.
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}