use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use nix::fcntl::{flock, FlockArg};
use super::{paths::CachePaths, DaemonError, Result};
#[derive(Debug)]
pub struct PidLock {
file: File,
path: PathBuf,
}
impl PidLock {
pub fn acquire(paths: &CachePaths) -> Result<Self> {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(&paths.pid_file)?;
match flock(file.as_raw_fd(), FlockArg::LockExclusiveNonblock) {
Ok(()) => {}
Err(nix::errno::Errno::EWOULDBLOCK) => {
let mut buf = String::new();
file.read_to_string(&mut buf).ok();
let pid = buf.trim().parse::<i32>().unwrap_or(0);
return Err(DaemonError::AlreadyRunning(pid));
}
Err(e) => return Err(e.into()),
}
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
writeln!(file, "{}", std::process::id())?;
file.flush()?;
super::paths::ensure_file_600(&paths.pid_file)?;
Ok(Self {
file,
path: paths.pid_file.clone(),
})
}
pub fn path(&self) -> &std::path::Path {
&self.path
}
}
impl Drop for PidLock {
fn drop(&mut self) {
let _ = self.file.set_len(0);
}
}
pub fn acquire(paths: &CachePaths) -> Result<PidLock> {
PidLock::acquire(paths)
}
pub fn read_pid(paths: &CachePaths) -> Option<i32> {
let mut s = String::new();
let mut f = File::open(&paths.pid_file).ok()?;
f.read_to_string(&mut s).ok()?;
s.trim().parse::<i32>().ok().filter(|p| *p > 0)
}
pub fn pid_alive(pid: i32) -> bool {
if pid <= 0 {
return false;
}
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
matches!(kill(Pid::from_raw(pid), None::<Signal>), Ok(()))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn acquire_succeeds_first_time() {
let tmp = TempDir::new().unwrap();
let paths = CachePaths::with_root(tmp.path().join("zshrs"));
paths.ensure_dirs().unwrap();
let guard = acquire(&paths).unwrap();
assert_eq!(guard.path(), paths.pid_file);
let pid = read_pid(&paths).unwrap();
assert_eq!(pid, std::process::id() as i32);
}
#[test]
fn second_acquire_blocks_with_already_running() {
let tmp = TempDir::new().unwrap();
let paths = CachePaths::with_root(tmp.path().join("zshrs"));
paths.ensure_dirs().unwrap();
let _guard = acquire(&paths).unwrap();
let err = acquire(&paths).unwrap_err();
match err {
DaemonError::AlreadyRunning(pid) => {
assert_eq!(pid, std::process::id() as i32);
}
other => panic!("expected AlreadyRunning, got {:?}", other),
}
}
#[test]
fn re_acquire_after_drop_succeeds() {
let tmp = TempDir::new().unwrap();
let paths = CachePaths::with_root(tmp.path().join("zshrs"));
paths.ensure_dirs().unwrap();
{
let _guard = acquire(&paths).unwrap();
}
let _again = acquire(&paths).unwrap();
}
#[test]
fn pid_alive_self() {
assert!(pid_alive(std::process::id() as i32));
}
#[test]
fn pid_alive_zero_is_false() {
assert!(!pid_alive(0));
}
}