use std::io::{Read, Write};
use std::fs::{self, File, TryLockError};
use std::path::{Path, PathBuf};
use anyhow::{bail, Context};
pub const LOCK_FILE: &str = "LOCK";
pub struct PidLock {
_file: File,
path: PathBuf,
}
impl PidLock {
pub fn acquire(datadir: &Path) -> anyhow::Result<Self> {
fs::create_dir_all(datadir)
.context("failed to create datadir")?;
let path = datadir.join(LOCK_FILE);
let mut file = File::options()
.read(true)
.write(true)
.create(true)
.open(&path)
.with_context(|| format!("failed to open pid lock at {}", path.display()))?;
match file.try_lock() {
Ok(()) => {}
Err(TryLockError::WouldBlock) => {
let mut buffer = String::new();
let _ = file.read_to_string(&mut buffer);
bail!(
"Another process is already using this datadir ({})\n\
PID in lock file: {}\n",
path.display(),
buffer.trim(),
);
}
Err(TryLockError::Error(e)) => {
bail!("failed to acquire pid lock at {}: {}", path.display(), e);
}
}
file.set_len(0)
.context("failed to truncate pid lock")?;
write!(file, "{}", std::process::id())
.context("failed to write to pid lock")?;
file.flush()
.context("failed to flush pid lock")?;
Ok(PidLock { _file: file, path })
}
}
impl std::fmt::Debug for PidLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PidLock").field("path", &self.path).finish()
}
}
#[cfg(test)]
mod test {
use super::*;
fn tmp_datadir() -> PathBuf {
let dir = std::env::temp_dir()
.join(format!("bark-pid-test-{}", std::process::id()))
.join(format!("{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
let _ = fs::remove_dir_all(&dir);
dir
}
#[test]
fn acquire_creates_pid_file_with_current_pid() {
let dir = tmp_datadir();
let lock = PidLock::acquire(&dir).unwrap();
let contents = fs::read_to_string(dir.join(LOCK_FILE)).unwrap();
assert_eq!(contents, std::process::id().to_string());
drop(lock);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn second_acquire_is_refused() {
let dir = tmp_datadir();
let _lock = PidLock::acquire(&dir).unwrap();
let err = PidLock::acquire(&dir).unwrap_err();
assert!(
err.to_string().contains("Another process is already using this datadir"),
"unexpected error: {}", err,
);
drop(_lock);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn can_reacquire_after_drop() {
let dir = tmp_datadir();
let lock = PidLock::acquire(&dir).unwrap();
drop(lock);
let lock2 = PidLock::acquire(&dir).unwrap();
drop(lock2);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn creates_datadir_if_missing() {
let dir = tmp_datadir();
assert!(!dir.exists());
let lock = PidLock::acquire(&dir).unwrap();
assert!(dir.exists());
assert!(dir.join(LOCK_FILE).exists());
drop(lock);
let _ = fs::remove_dir_all(&dir);
}
}