1use std::fs::{File, OpenOptions};
14use std::path::Path;
15use std::time::Duration;
16
17use fs2::FileExt;
18
19use crate::error::{CoreError, Result};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum LockKind {
23 Shared,
24 Exclusive,
25}
26
27pub struct FileLock {
29 file: File,
30 kind: LockKind,
31}
32
33impl FileLock {
34 fn acquire(path: &Path, kind: LockKind, timeout: Duration) -> Result<Self> {
35 if let Some(parent) = path.parent() {
36 std::fs::create_dir_all(parent)?;
37 }
38 let file = OpenOptions::new()
39 .create(true)
40 .read(true)
41 .write(true)
42 .truncate(false)
43 .open(path)?;
44
45 let deadline = std::time::Instant::now() + timeout;
46 loop {
47 let acquired = match kind {
48 LockKind::Shared => file.try_lock_shared().is_ok(),
49 LockKind::Exclusive => file.try_lock_exclusive().is_ok(),
50 };
51 if acquired {
52 return Ok(Self { file, kind });
53 }
54 if std::time::Instant::now() >= deadline {
56 let secs = timeout.as_secs();
57 tracing::warn!(lock = %path.display(), "lock acquire timed out");
58 return Err(CoreError::LockTimeout(secs.max(1)));
59 }
60 std::thread::sleep(Duration::from_millis(25));
61 }
62 }
63
64 pub fn kind(&self) -> LockKind {
65 self.kind
66 }
67}
68
69impl Drop for FileLock {
70 fn drop(&mut self) {
71 let _ = self.file.unlock();
72 }
73}
74
75pub fn acquire(path: &Path, kind: LockKind, timeout: Duration) -> Result<FileLock> {
78 FileLock::acquire(path, kind, timeout)
79}
80
81pub fn is_locked(path: &Path) -> bool {
84 let Ok(file) = OpenOptions::new()
85 .read(true)
86 .write(true)
87 .create(false)
88 .open(path)
89 else {
90 return false;
91 };
92 file.try_lock_exclusive().is_err()
93}