1use std::fs::{File, OpenOptions};
10use std::io;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, thiserror::Error)]
15pub enum LockError {
16 #[error("lock path is a symlink (refused): {0}")]
18 Symlink(PathBuf),
19 #[error("lock path is not a regular file: {0}")]
21 NotARegularFile(PathBuf),
22 #[error("another inferd-daemon already holds the lock at {0}")]
24 AlreadyHeld(PathBuf),
25 #[error("io: {0}")]
27 Io(#[from] io::Error),
28}
29
30#[derive(Debug)]
38pub struct Lock {
39 _file: File,
40 path: PathBuf,
41}
42
43impl Lock {
44 pub fn acquire(path: impl AsRef<Path>) -> Result<Self, LockError> {
53 let path = path.as_ref();
54
55 if let Ok(meta) = std::fs::symlink_metadata(path) {
56 if meta.file_type().is_symlink() {
57 return Err(LockError::Symlink(path.to_path_buf()));
58 }
59 if !meta.file_type().is_file() {
60 return Err(LockError::NotARegularFile(path.to_path_buf()));
61 }
62 }
63
64 let file = OpenOptions::new()
65 .read(true)
66 .write(true)
67 .create(true)
68 .truncate(false)
69 .open(path)?;
70
71 match file.try_lock() {
72 Ok(()) => Ok(Lock {
73 _file: file,
74 path: path.to_path_buf(),
75 }),
76 Err(std::fs::TryLockError::WouldBlock) => {
77 Err(LockError::AlreadyHeld(path.to_path_buf()))
78 }
79 Err(std::fs::TryLockError::Error(e)) => Err(LockError::Io(e)),
80 }
81 }
82
83 pub fn path(&self) -> &Path {
85 &self.path
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use tempfile::tempdir;
93
94 #[test]
95 fn acquire_then_release_succeeds() {
96 let dir = tempdir().unwrap();
97 let path = dir.path().join("inferd.lock");
98 let lock = Lock::acquire(&path).unwrap();
99 assert_eq!(lock.path(), &path);
100 drop(lock);
101 let _again = Lock::acquire(&path).unwrap();
103 }
104
105 #[test]
106 fn second_acquire_while_held_fails() {
107 let dir = tempdir().unwrap();
108 let path = dir.path().join("inferd.lock");
109 let _first = Lock::acquire(&path).unwrap();
110 let err = Lock::acquire(&path).unwrap_err();
111 assert!(matches!(err, LockError::AlreadyHeld(_)));
112 }
113
114 #[cfg(unix)]
120 #[test]
121 fn pre_existing_symlink_is_refused() {
122 let dir = tempdir().unwrap();
123 let target = dir.path().join("target.bin");
124 std::fs::write(&target, b"x").unwrap();
125 let symlink = dir.path().join("inferd.lock");
126 std::os::unix::fs::symlink(&target, &symlink).unwrap();
127
128 let err = Lock::acquire(&symlink).unwrap_err();
129 assert!(matches!(err, LockError::Symlink(_)));
130 }
131
132 #[test]
133 fn directory_at_lock_path_refused() {
134 let dir = tempdir().unwrap();
135 let bad = dir.path().join("inferd.lock");
136 std::fs::create_dir(&bad).unwrap();
137 let err = Lock::acquire(&bad).unwrap_err();
138 assert!(matches!(err, LockError::NotARegularFile(_)));
139 }
140}