1use std::io::{Read, Write};
2use std::fs::{self, File, TryLockError};
3use std::path::{Path, PathBuf};
4
5use anyhow::{bail, Context};
6
7pub const LOCK_FILE: &str = "LOCK";
9
10pub struct PidLock {
16 _file: File,
18 path: PathBuf,
19}
20
21impl PidLock {
22 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 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 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 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}