1#[cfg(feature = "json")]
21use crate::atomic::atomic_write;
22use crate::error::{FsError, Result};
23#[cfg(feature = "json")]
24use serde::{Deserialize, Serialize};
25#[cfg(not(unix))]
26use std::fs::TryLockError;
27use std::fs::{File, OpenOptions};
28use std::path::{Path, PathBuf};
29use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
33pub struct LockOwner {
34 pub pid: u32,
35 pub hostname: String,
36 pub label: String,
37 pub started_unix_ms: u64,
38}
39
40impl LockOwner {
41 pub fn current(label: &str) -> Self {
42 Self {
43 pid: std::process::id(),
44 hostname: hostname(),
45 label: label.to_string(),
46 started_unix_ms: SystemTime::now()
47 .duration_since(UNIX_EPOCH)
48 .map(|d| d.as_millis() as u64)
49 .unwrap_or(0),
50 }
51 }
52
53 pub fn describe(&self) -> String {
54 format!(
55 "pid {} on {} ({}) since unix-ms {}",
56 self.pid, self.hostname, self.label, self.started_unix_ms
57 )
58 }
59}
60
61#[derive(Clone, Debug)]
62pub struct LockOptions {
63 pub timeout: Duration,
65 pub poll: Duration,
66 pub label: String,
68}
69
70impl Default for LockOptions {
71 fn default() -> Self {
72 Self {
73 timeout: Duration::ZERO,
74 poll: Duration::from_millis(50),
75 label: String::new(),
76 }
77 }
78}
79
80impl LockOptions {
81 pub fn wait(timeout: Duration) -> Self {
82 Self {
83 timeout,
84 ..Self::default()
85 }
86 }
87
88 pub fn labeled(mut self, label: &str) -> Self {
89 self.label = label.to_string();
90 self
91 }
92}
93
94#[derive(Debug)]
96pub struct FileLock {
97 #[cfg_attr(unix, allow(dead_code))]
99 file: Handle,
100 path: PathBuf,
101 owner_path: Option<PathBuf>,
102 pub recovered_stale: Option<LockOwner>,
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108enum Mode {
109 Exclusive,
110 Shared,
111}
112
113#[cfg(unix)]
115type Handle = posix::Handle;
116#[cfg(not(unix))]
117type Handle = File;
118
119pub fn owner_path_for(lock_path: &Path) -> PathBuf {
120 let mut os = lock_path.as_os_str().to_os_string();
121 os.push(".owner");
122 PathBuf::from(os)
123}
124
125fn open_lock_file(path: &Path) -> Result<File> {
126 let mut options = OpenOptions::new();
127 options.read(true).write(true).create(true).truncate(false);
128 #[cfg(unix)]
129 {
130 use std::os::unix::fs::OpenOptionsExt;
131 options
132 .mode(0o600)
133 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
134 }
135 options.open(path).map_err(|e| FsError::io(path, e))
136}
137
138impl FileLock {
139 pub fn acquire(path: &Path, options: &LockOptions) -> Result<Self> {
143 let file = lock_with_retry(path, options, Mode::Exclusive)?;
144 let recovered_stale = read_owner(&owner_path_for(path));
145 let owner_path = record_owner(path, &options.label)?;
146 Ok(Self {
147 file,
148 path: path.to_path_buf(),
149 owner_path,
150 recovered_stale,
151 })
152 }
153
154 pub fn try_acquire(path: &Path) -> Result<Self> {
156 Self::acquire(path, &LockOptions::default())
157 }
158
159 pub fn acquire_shared(path: &Path, options: &LockOptions) -> Result<Self> {
163 let file = lock_with_retry(path, options, Mode::Shared)?;
164 Ok(Self {
165 file,
166 path: path.to_path_buf(),
167 owner_path: None,
168 recovered_stale: None,
169 })
170 }
171
172 pub fn path(&self) -> &Path {
173 &self.path
174 }
175
176 pub fn current_owner(lock_path: &Path) -> Option<LockOwner> {
178 read_owner(&owner_path_for(lock_path))
179 }
180}
181
182#[cfg(unix)]
184fn lock_with_retry(path: &Path, options: &LockOptions, mode: Mode) -> Result<Handle> {
185 let started = Instant::now();
186 loop {
187 if let Some(handle) = posix::try_lock(path, mode)? {
188 return Ok(handle);
189 }
190 wait_or_fail(path, options, started, mode)?;
191 }
192}
193
194#[cfg(not(unix))]
196fn lock_with_retry(path: &Path, options: &LockOptions, mode: Mode) -> Result<Handle> {
197 let file = open_lock_file(path)?;
198 let started = Instant::now();
199 loop {
200 let locked = match mode {
201 Mode::Exclusive => file.try_lock(),
202 Mode::Shared => file.try_lock_shared(),
203 };
204 match locked {
205 Ok(()) => return Ok(file),
206 Err(TryLockError::WouldBlock) => wait_or_fail(path, options, started, mode)?,
207 Err(TryLockError::Error(e)) => return Err(FsError::io(path, e)),
208 }
209 }
210}
211
212fn wait_or_fail(path: &Path, options: &LockOptions, started: Instant, mode: Mode) -> Result<()> {
214 if started.elapsed() >= options.timeout {
215 let waited_ms = started.elapsed().as_millis() as u64;
216 return Err(match mode {
217 Mode::Exclusive if options.timeout.is_zero() => {
218 let owner = read_owner(&owner_path_for(path))
219 .map(|o| o.describe())
220 .unwrap_or_else(|| "an unknown owner".into());
221 FsError::Locked {
222 path: path.to_path_buf(),
223 owner,
224 }
225 }
226 _ => FsError::LockTimeout {
227 path: path.to_path_buf(),
228 waited_ms,
229 },
230 });
231 }
232 std::thread::sleep(options.poll);
233 Ok(())
234}
235
236impl Drop for FileLock {
237 fn drop(&mut self) {
238 if let Some(owner) = &self.owner_path {
240 let _ = std::fs::remove_file(owner);
241 }
242 #[cfg(not(unix))]
244 let _ = self.file.unlock();
245 }
246}
247
248#[cfg(unix)]
259mod posix {
260 use super::{open_lock_file, Mode};
261 use crate::error::{FsError, Result};
262 use std::collections::BTreeMap;
263 use std::fs::File;
264 use std::path::{Path, PathBuf};
265 use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
266
267 struct Entry {
268 mode: Mode,
269 holders: usize,
270 file: Arc<File>,
271 }
272
273 fn registry() -> MutexGuard<'static, BTreeMap<PathBuf, Entry>> {
274 static REGISTRY: OnceLock<Mutex<BTreeMap<PathBuf, Entry>>> = OnceLock::new();
275 REGISTRY
276 .get_or_init(|| Mutex::new(BTreeMap::new()))
277 .lock()
278 .unwrap_or_else(|p| p.into_inner())
279 }
280
281 #[derive(Debug)]
283 pub struct Handle {
284 key: PathBuf,
285 file: Option<Arc<File>>,
288 }
289
290 pub fn try_lock(path: &Path, mode: Mode) -> Result<Option<Handle>> {
292 let key = registry_key(path);
293 let mut registry = registry();
294 if let Some(entry) = registry.get_mut(&key) {
295 if entry.mode == Mode::Shared && mode == Mode::Shared {
298 entry.holders += 1;
299 return Ok(Some(Handle {
300 key,
301 file: Some(entry.file.clone()),
302 }));
303 }
304 return Ok(None);
305 }
306 let file = Arc::new(open_lock_file(path)?);
307 if !set_lock(&file, mode, path)? {
308 return Ok(None);
311 }
312 registry.insert(
313 key.clone(),
314 Entry {
315 mode,
316 holders: 1,
317 file: file.clone(),
318 },
319 );
320 Ok(Some(Handle {
321 key,
322 file: Some(file),
323 }))
324 }
325
326 fn set_lock(file: &File, mode: Mode, path: &Path) -> Result<bool> {
328 let kind = match mode {
329 Mode::Exclusive => libc::F_WRLCK,
330 Mode::Shared => libc::F_RDLCK,
331 };
332 match set_record_lock(file, kind) {
333 Ok(granted) => Ok(granted),
334 Err(error) => Err(FsError::io(path, error)),
335 }
336 }
337
338 fn set_record_lock(file: &File, kind: libc::c_short) -> std::io::Result<bool> {
341 use std::os::fd::AsRawFd;
342 let request = libc::flock {
343 l_type: kind as _,
344 l_whence: libc::SEEK_SET as _,
345 l_start: 0,
346 l_len: 0,
347 l_pid: 0,
348 };
349 loop {
350 let rc = unsafe {
354 libc::fcntl(
355 file.as_raw_fd(),
356 libc::F_SETLK,
357 &request as *const libc::flock,
358 )
359 };
360 if rc != -1 {
361 return Ok(true);
362 }
363 let error = std::io::Error::last_os_error();
364 match error.raw_os_error() {
365 Some(libc::EINTR) => continue,
366 Some(libc::EAGAIN) | Some(libc::EACCES) => return Ok(false),
367 _ => return Err(error),
368 }
369 }
370 }
371
372 fn registry_key(path: &Path) -> PathBuf {
375 let parent = path
376 .parent()
377 .filter(|p| !p.as_os_str().is_empty())
378 .unwrap_or(Path::new("."));
379 match (parent.canonicalize(), path.file_name()) {
380 (Ok(dir), Some(name)) => dir.join(name),
381 _ => path.to_path_buf(),
382 }
383 }
384
385 impl Drop for Handle {
386 fn drop(&mut self) {
387 let mut registry = registry();
388 let last = match registry.get_mut(&self.key) {
389 Some(entry) => {
390 entry.holders -= 1;
391 entry.holders == 0
392 }
393 None => false,
394 };
395 if last {
396 registry.remove(&self.key);
397 }
398 drop(self.file.take());
399 }
400 }
401}
402
403pub fn with_lock<T>(path: &Path, options: &LockOptions, f: impl FnOnce() -> T) -> Result<T> {
405 let _guard = FileLock::acquire(path, options)?;
406 Ok(f())
407}
408
409#[cfg(feature = "json")]
411fn record_owner(lock_path: &Path, label: &str) -> Result<Option<PathBuf>> {
412 let owner_path = owner_path_for(lock_path);
413 let record = serde_json::to_vec_pretty(&LockOwner::current(label)).expect("owner serializes");
414 atomic_write(&owner_path, &record)?;
415 Ok(Some(owner_path))
416}
417
418#[cfg(not(feature = "json"))]
419fn record_owner(_lock_path: &Path, _label: &str) -> Result<Option<PathBuf>> {
420 Ok(None)
421}
422
423#[cfg(feature = "json")]
424fn read_owner(path: &Path) -> Option<LockOwner> {
425 serde_json::from_slice(&std::fs::read(path).ok()?).ok()
426}
427
428#[cfg(not(feature = "json"))]
429fn read_owner(_path: &Path) -> Option<LockOwner> {
430 None
431}
432
433pub fn hostname() -> String {
434 #[cfg(unix)]
435 {
436 let mut buf = [0u8; 256];
437 if unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) } == 0 {
439 let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len());
440 let name = String::from_utf8_lossy(&buf[..end]).trim().to_string();
441 if !name.is_empty() {
442 return name;
443 }
444 }
445 }
446 for var in ["COMPUTERNAME", "HOSTNAME"] {
447 if let Some(v) = std::env::var_os(var) {
448 let s = v.to_string_lossy().trim().to_string();
449 if !s.is_empty() {
450 return s;
451 }
452 }
453 }
454 "unknown".into()
455}