1use std::fs::{File, OpenOptions};
14use std::io::Write;
15use std::path::{Path, PathBuf};
16
17use nix::fcntl::{Flock, FlockArg};
18use nix::sys::signal::kill;
19use nix::unistd::Pid;
20
21use crate::error::{Error, Result};
22
23const LOCK_FILE: &str = ".handoff.lock";
24const PID_FILE: &str = ".handoff.pidfile";
25
26pub struct DataDirLock {
29 _flock: Flock<File>,
30 data_dir: PathBuf,
31 pid_path: PathBuf,
32}
33
34impl std::fmt::Debug for DataDirLock {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("DataDirLock")
37 .field("data_dir", &self.data_dir)
38 .finish()
39 }
40}
41
42impl DataDirLock {
43 pub fn data_dir(&self) -> &Path {
46 &self.data_dir
47 }
48}
49
50impl DataDirLock {
51 pub fn acquire(data_dir: &Path) -> Result<Self> {
54 std::fs::create_dir_all(data_dir)?;
55 let lock_path = data_dir.join(LOCK_FILE);
56 let pid_path = data_dir.join(PID_FILE);
57
58 let file = OpenOptions::new()
59 .read(true)
60 .write(true)
61 .create(true)
62 .truncate(false)
63 .open(&lock_path)?;
64
65 let flock = match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
66 Ok(flock) => flock,
67 Err((_file, nix::errno::Errno::EWOULDBLOCK)) => {
68 let holder = read_pidfile(&pid_path).unwrap_or(0);
69 return Err(Error::LockHeld { holder_pid: holder });
70 }
71 Err((_file, errno)) => return Err(Error::Nix(errno)),
72 };
73
74 write_pid_atomic(&pid_path, std::process::id())?;
75 Ok(Self {
76 _flock: flock,
77 data_dir: data_dir.to_path_buf(),
78 pid_path,
79 })
80 }
81
82 pub fn acquire_or_break_stale(data_dir: &Path) -> Result<Self> {
99 match Self::acquire(data_dir) {
100 Ok(lock) => Ok(lock),
101 Err(Error::LockHeld { holder_pid }) => {
102 if holder_pid != 0 && is_pid_alive(holder_pid) {
103 return Err(Error::StaleLockBreakRefused { holder_pid });
104 }
105 tracing::warn!(
106 holder_pid,
107 "data-dir flock appears stale (named holder dead); retrying acquire"
108 );
109 match Self::acquire(data_dir) {
110 Ok(lock) => Ok(lock),
111 Err(Error::LockHeld { holder_pid }) => {
112 Err(Error::StaleLockBreakRefused { holder_pid })
113 }
114 Err(e) => Err(e),
115 }
116 }
117 Err(e) => Err(e),
118 }
119 }
120}
121
122impl Drop for DataDirLock {
123 fn drop(&mut self) {
124 let _ = std::fs::remove_file(&self.pid_path);
128 }
129}
130
131fn read_pidfile(path: &Path) -> Option<i32> {
132 std::fs::read_to_string(path).ok()?.trim().parse().ok()
133}
134
135fn write_pid_atomic(path: &Path, pid: u32) -> Result<()> {
136 let tmp = path.with_extension("pidfile.tmp");
137 {
138 let mut f = OpenOptions::new()
139 .write(true)
140 .create(true)
141 .truncate(true)
142 .open(&tmp)?;
143 writeln!(f, "{pid}")?;
144 f.sync_all()?;
145 }
146 std::fs::rename(&tmp, path)?;
147 if let Some(parent) = path.parent() {
152 let target = if parent.as_os_str().is_empty() {
153 Path::new(".")
154 } else {
155 parent
156 };
157 File::open(target)?.sync_all()?;
158 }
159 Ok(())
160}
161
162fn is_pid_alive(pid: i32) -> bool {
163 if pid <= 0 {
164 return false;
165 }
166 matches!(kill(Pid::from_raw(pid), None), Ok(()))
167}
168
169#[cfg(test)]
170mod tests {
171 use std::os::fd::AsRawFd;
172
173 use super::*;
174
175 #[test]
176 fn acquire_succeeds_on_empty_dir() {
177 let dir = tempfile::tempdir().unwrap();
178 let lock = DataDirLock::acquire(dir.path()).unwrap();
179 drop(lock);
180 }
181
182 #[test]
183 fn second_acquire_returns_lock_held() {
184 let dir = tempfile::tempdir().unwrap();
185 let _lock = DataDirLock::acquire(dir.path()).unwrap();
186 match DataDirLock::acquire(dir.path()) {
187 Err(Error::LockHeld { holder_pid }) => {
188 assert_eq!(holder_pid as u32, std::process::id());
189 }
190 other => panic!("expected LockHeld, got {other:?}"),
191 }
192 }
193
194 #[test]
195 fn release_on_drop_allows_reacquire() {
196 let dir = tempfile::tempdir().unwrap();
197 {
198 let _lock = DataDirLock::acquire(dir.path()).unwrap();
199 }
200 let _lock = DataDirLock::acquire(dir.path()).unwrap();
201 }
202
203 #[test]
204 fn stale_break_refuses_for_live_pid() {
205 let dir = tempfile::tempdir().unwrap();
206 let _held = DataDirLock::acquire(dir.path()).unwrap();
207 match DataDirLock::acquire_or_break_stale(dir.path()) {
208 Err(Error::StaleLockBreakRefused { holder_pid }) => {
209 assert_eq!(holder_pid as u32, std::process::id());
210 }
211 other => panic!("expected refusal, got {other:?}"),
212 }
213 }
214
215 #[test]
216 fn stale_break_succeeds_when_kernel_released_flock() {
217 let dir = tempfile::tempdir().unwrap();
222 std::fs::write(dir.path().join(LOCK_FILE), b"").unwrap();
223 std::fs::write(dir.path().join(PID_FILE), format!("{}", i32::MAX)).unwrap();
224
225 let _new_lock = DataDirLock::acquire_or_break_stale(dir.path()).unwrap();
226 }
227
228 #[test]
229 fn stale_break_refuses_when_pidfile_lies_but_flock_held() {
230 let dir = tempfile::tempdir().unwrap();
235 let lock_path = dir.path().join(LOCK_FILE);
236 let pid_path = dir.path().join(PID_FILE);
237
238 let f = OpenOptions::new()
239 .read(true)
240 .write(true)
241 .create(true)
242 .truncate(false)
243 .open(&lock_path)
244 .unwrap();
245 let _other_flock = Flock::lock(f, FlockArg::LockExclusiveNonblock)
246 .map_err(|(_, e)| e)
247 .unwrap();
248 std::fs::write(&pid_path, format!("{}", i32::MAX)).unwrap();
249
250 match DataDirLock::acquire_or_break_stale(dir.path()) {
251 Err(Error::StaleLockBreakRefused { .. }) => {}
252 other => panic!("expected StaleLockBreakRefused, got {other:?}"),
253 }
254 assert!(_other_flock.as_raw_fd() >= 0);
257 }
258}