use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
const LOCK_FILE: &str = "LOCK";
#[derive(Debug)]
pub struct DirLock {
path: PathBuf,
pid: u32,
}
impl DirLock {
pub fn acquire(data_dir: &Path) -> io::Result<DirLock> {
let path = data_dir.join(LOCK_FILE);
let me = std::process::id();
if let Ok(contents) = fs::read_to_string(&path) {
if let Ok(owner) = contents.trim().parse::<u32>() {
if owner != me && pid_is_alive(owner) {
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!(
"data directory {} is already open by process {owner}; \
close that instance first (or delete {} if it is gone)",
data_dir.display(),
path.display()
),
));
}
}
}
let mut f = fs::File::create(&path)?;
f.write_all(me.to_string().as_bytes())?;
let _ = f.sync_all();
Ok(DirLock { path, pid: me })
}
}
impl Drop for DirLock {
fn drop(&mut self) {
if let Ok(contents) = fs::read_to_string(&self.path) {
if contents.trim().parse::<u32>().ok() == Some(self.pid) {
let _ = fs::remove_file(&self.path);
}
}
}
}
#[cfg(unix)]
fn pid_is_alive(pid: u32) -> bool {
if pid == 0 {
return false;
}
if unsafe { libc::kill(pid as libc::pid_t, 0) } == 0 {
return true;
}
io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(not(unix))]
fn pid_is_alive(_pid: u32) -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn acquire_then_drop_removes_lock_file() {
let dir = tempfile::tempdir().unwrap();
{
let _lock = DirLock::acquire(dir.path()).unwrap();
assert!(dir.path().join(LOCK_FILE).exists());
}
assert!(
!dir.path().join(LOCK_FILE).exists(),
"drop should remove it"
);
}
#[test]
fn same_process_reacquire_is_allowed() {
let dir = tempfile::tempdir().unwrap();
let first = DirLock::acquire(dir.path()).unwrap();
std::mem::forget(first); assert!(DirLock::acquire(dir.path()).is_ok());
}
#[test]
fn dead_owner_pid_is_a_dead_pid() {
let mut child = std::process::Command::new("true").spawn().unwrap();
let pid = child.id();
child.wait().unwrap();
assert!(!pid_is_alive(pid));
}
}