bark/
pid_lock.rs

1use std::io::{Read, Write};
2use std::fs::{self, File, TryLockError};
3use std::path::{Path, PathBuf};
4
5use anyhow::{bail, Context};
6
7/// File name of the PID lock file.
8pub const LOCK_FILE: &str = "LOCK";
9
10/// A guard that holds an exclusive advisory lock on `LOCK`.
11///
12/// Uses OS-level file locking (`flock` on Unix, `LockFileEx` on Windows)
13/// so the lock is automatically released when the process exits, even
14/// on SIGKILL or a crash.
15pub struct PidLock {
16	// Keep the file handle open to hold the advisory lock.
17	_file: File,
18	path: PathBuf,
19}
20
21impl PidLock {
22	/// Acquire a PID lock in the given directory.
23	///
24	/// Opens (or creates) `LOCK` and takes an exclusive advisory lock.
25	/// If another process already holds the lock, acquisition fails.
26	/// Creates the directory if it does not yet exist.
27	pub fn acquire(datadir: &Path) -> anyhow::Result<Self> {
28		fs::create_dir_all(datadir)
29			.context("failed to create datadir")?;
30
31		let path = datadir.join(LOCK_FILE);
32
33		let mut file = File::options()
34			.read(true)
35			.write(true)
36			.create(true)
37			.open(&path)
38			.with_context(|| format!("failed to open pid lock at {}", path.display()))?;
39
40		match file.try_lock() {
41			Ok(()) => {}
42			Err(TryLockError::WouldBlock) => {
43				let mut buffer = String::new();
44				let _ = file.read_to_string(&mut buffer);
45				bail!(
46					"Another process is already using this datadir ({})\n\
47					 PID in lock file: {}\n",
48					path.display(),
49					buffer.trim(),
50				);
51			}
52			Err(TryLockError::Error(e)) => {
53				bail!("failed to acquire pid lock at {}: {}", path.display(), e);
54			}
55		}
56
57		// Write our PID (truncate any stale content first).
58		file.set_len(0)
59			.context("failed to truncate pid lock")?;
60		write!(file, "{}", std::process::id())
61			.context("failed to write to pid lock")?;
62		file.flush()
63			.context("failed to flush pid lock")?;
64
65		Ok(PidLock { _file: file, path })
66	}
67}
68
69impl std::fmt::Debug for PidLock {
70	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71		f.debug_struct("PidLock").field("path", &self.path).finish()
72	}
73}
74
75#[cfg(test)]
76mod test {
77	use super::*;
78
79	fn tmp_datadir() -> PathBuf {
80		let dir = std::env::temp_dir()
81			.join(format!("bark-pid-test-{}", std::process::id()))
82			.join(format!("{}", std::time::SystemTime::now()
83				.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
84		// Ensure clean state.
85		let _ = fs::remove_dir_all(&dir);
86		dir
87	}
88
89	#[test]
90	fn acquire_creates_pid_file_with_current_pid() {
91		let dir = tmp_datadir();
92		let lock = PidLock::acquire(&dir).unwrap();
93
94		let contents = fs::read_to_string(dir.join(LOCK_FILE)).unwrap();
95		assert_eq!(contents, std::process::id().to_string());
96
97		drop(lock);
98		let _ = fs::remove_dir_all(&dir);
99	}
100
101	#[test]
102	fn second_acquire_is_refused() {
103		let dir = tmp_datadir();
104		let _lock = PidLock::acquire(&dir).unwrap();
105
106		let err = PidLock::acquire(&dir).unwrap_err();
107		assert!(
108			err.to_string().contains("Another process is already using this datadir"),
109			"unexpected error: {}", err,
110		);
111
112		drop(_lock);
113		let _ = fs::remove_dir_all(&dir);
114	}
115
116	#[test]
117	fn can_reacquire_after_drop() {
118		let dir = tmp_datadir();
119
120		let lock = PidLock::acquire(&dir).unwrap();
121		drop(lock);
122
123		// Should succeed now that the previous lock was dropped.
124		let lock2 = PidLock::acquire(&dir).unwrap();
125		drop(lock2);
126
127		let _ = fs::remove_dir_all(&dir);
128	}
129
130	#[test]
131	fn creates_datadir_if_missing() {
132		let dir = tmp_datadir();
133		assert!(!dir.exists());
134
135		let lock = PidLock::acquire(&dir).unwrap();
136		assert!(dir.exists());
137		assert!(dir.join(LOCK_FILE).exists());
138
139		drop(lock);
140		let _ = fs::remove_dir_all(&dir);
141	}
142}