1use std::fs::File;
12use std::os::unix::io::AsRawFd;
13
14use crate::core::{LuciError, Result};
15
16const PENDING_BYTE: u64 = 49;
19const RESERVED_BYTE: u64 = 50;
20const SHARED_FIRST: u64 = 51;
21const SHARED_SIZE: u64 = 510;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
25pub enum LockLevel {
26 Unlocked = 0,
27 Shared = 1,
28 Reserved = 2,
29 Pending = 3,
30 Exclusive = 4,
31}
32
33pub struct FileLock {
42 fd: i32,
43 level: LockLevel,
44}
45
46impl FileLock {
47 pub fn new(file: &File) -> Self {
49 Self {
50 fd: file.as_raw_fd(),
51 level: LockLevel::Unlocked,
52 }
53 }
54
55 pub fn level(&self) -> LockLevel {
57 self.level
58 }
59
60 pub fn lock_shared(&mut self) -> Result<()> {
65 assert_eq!(
66 self.level,
67 LockLevel::Unlocked,
68 "lock_shared requires UNLOCKED state"
69 );
70
71 fcntl_lock(self.fd, libc::F_RDLCK, PENDING_BYTE, 1)?;
74
75 let result = fcntl_lock(self.fd, libc::F_RDLCK, SHARED_FIRST, SHARED_SIZE);
77
78 let _ = fcntl_lock(self.fd, libc::F_UNLCK, PENDING_BYTE, 1);
80
81 result?;
82 self.level = LockLevel::Shared;
83 Ok(())
84 }
85
86 pub fn lock_reserved(&mut self, timeout: std::time::Duration) -> Result<()> {
94 assert_eq!(
95 self.level,
96 LockLevel::Shared,
97 "lock_reserved requires SHARED state"
98 );
99
100 let deadline = std::time::Instant::now() + timeout;
101 let mut backoff = std::time::Duration::from_millis(1);
102 let max_backoff = std::time::Duration::from_millis(100);
103
104 loop {
105 match fcntl_try_lock(self.fd, libc::F_WRLCK, RESERVED_BYTE, 1) {
106 Ok(()) => break,
107 Err(LuciError::WriterLocked) => {
108 if std::time::Instant::now() >= deadline {
109 return Err(LuciError::WriterLocked);
110 }
111 std::thread::sleep(backoff);
112 backoff = (backoff * 2).min(max_backoff);
113 }
114 Err(e) => return Err(e),
115 }
116 }
117
118 self.level = LockLevel::Reserved;
119 Ok(())
120 }
121
122 pub fn lock_exclusive(&mut self) -> Result<()> {
130 assert!(
131 self.level == LockLevel::Reserved || self.level == LockLevel::Pending,
132 "lock_exclusive requires RESERVED or PENDING state"
133 );
134
135 if self.level == LockLevel::Reserved {
136 fcntl_lock(self.fd, libc::F_WRLCK, PENDING_BYTE, 1)?;
140 self.level = LockLevel::Pending;
141 }
142
143 fcntl_lock(self.fd, libc::F_WRLCK, SHARED_FIRST, SHARED_SIZE)?;
147 self.level = LockLevel::Exclusive;
148 Ok(())
149 }
150
151 pub fn downgrade_to_shared(&mut self) -> Result<()> {
153 assert!(
154 self.level >= LockLevel::Reserved,
155 "downgrade_to_shared requires RESERVED or higher"
156 );
157
158 fcntl_lock(self.fd, libc::F_RDLCK, SHARED_FIRST, SHARED_SIZE)?;
160
161 fcntl_lock(self.fd, libc::F_UNLCK, PENDING_BYTE, 2)?;
163
164 self.level = LockLevel::Shared;
165 Ok(())
166 }
167
168 pub fn unlock(&mut self) -> Result<()> {
170 if self.level == LockLevel::Unlocked {
171 return Ok(());
172 }
173
174 fcntl_lock(self.fd, libc::F_UNLCK, 0, 0)?;
177 self.level = LockLevel::Unlocked;
178 Ok(())
179 }
180}
181
182impl Drop for FileLock {
183 fn drop(&mut self) {
184 let _ = self.unlock();
185 }
186}
187
188fn fcntl_lock(fd: i32, lock_type: i16, start: u64, len: u64) -> Result<()> {
190 let fl = libc::flock {
191 l_type: lock_type,
192 l_whence: libc::SEEK_SET as i16,
193 l_start: start as i64,
194 l_len: len as i64,
195 l_pid: 0,
196 };
197 let ret = unsafe { libc::fcntl(fd, libc::F_SETLKW, &fl) };
198 if ret == -1 {
199 let err = std::io::Error::last_os_error();
200 return Err(LuciError::Io(err));
201 }
202 Ok(())
203}
204
205fn fcntl_try_lock(fd: i32, lock_type: i16, start: u64, len: u64) -> Result<()> {
207 let fl = libc::flock {
208 l_type: lock_type,
209 l_whence: libc::SEEK_SET as i16,
210 l_start: start as i64,
211 l_len: len as i64,
212 l_pid: 0,
213 };
214 let ret = unsafe { libc::fcntl(fd, libc::F_SETLK, &fl) };
215 if ret == -1 {
216 let err = std::io::Error::last_os_error();
217 if err.raw_os_error() == Some(libc::EAGAIN) || err.raw_os_error() == Some(libc::EACCES) {
218 return Err(LuciError::WriterLocked);
219 }
220 return Err(LuciError::Io(err));
221 }
222 Ok(())
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use std::fs::OpenOptions;
229
230 fn test_file(name: &str) -> (std::path::PathBuf, File) {
231 let path =
232 std::env::temp_dir().join(format!("luci_lock_test_{}_{name}", std::process::id()));
233 let _ = std::fs::remove_file(&path);
234 let file = OpenOptions::new()
235 .read(true)
236 .write(true)
237 .create(true)
238 .truncate(true)
239 .open(&path)
240 .unwrap();
241 file.set_len(4096).unwrap();
243 (path, file)
244 }
245
246 #[test]
247 fn shared_lock_roundtrip() {
248 let (path, file) = test_file("shared");
249 let mut lock = FileLock::new(&file);
250 assert_eq!(lock.level(), LockLevel::Unlocked);
251
252 lock.lock_shared().unwrap();
253 assert_eq!(lock.level(), LockLevel::Shared);
254
255 lock.unlock().unwrap();
256 assert_eq!(lock.level(), LockLevel::Unlocked);
257
258 std::fs::remove_file(path).ok();
259 }
260
261 #[test]
262 fn full_escalation_and_downgrade() {
263 let (path, file) = test_file("escalation");
264 let mut lock = FileLock::new(&file);
265
266 lock.lock_shared().unwrap();
267 lock.lock_reserved(std::time::Duration::from_secs(5))
268 .unwrap();
269 assert_eq!(lock.level(), LockLevel::Reserved);
270
271 lock.lock_exclusive().unwrap();
272 assert_eq!(lock.level(), LockLevel::Exclusive);
273
274 lock.downgrade_to_shared().unwrap();
275 assert_eq!(lock.level(), LockLevel::Shared);
276
277 lock.unlock().unwrap();
278 std::fs::remove_file(path).ok();
279 }
280
281 #[test]
282 fn same_process_fds_share_locks() {
283 let (path, file1) = test_file("same_process");
287 let file2 = OpenOptions::new()
288 .read(true)
289 .write(true)
290 .open(&path)
291 .unwrap();
292
293 let mut lock1 = FileLock::new(&file1);
294 let mut lock2 = FileLock::new(&file2);
295
296 lock1.lock_shared().unwrap();
297 lock1
298 .lock_reserved(std::time::Duration::from_secs(5))
299 .unwrap();
300
301 lock2.lock_shared().unwrap();
303 lock2
304 .lock_reserved(std::time::Duration::from_secs(5))
305 .unwrap();
306
307 lock1.downgrade_to_shared().unwrap();
308 lock1.unlock().unwrap();
309 lock2.downgrade_to_shared().unwrap();
310 lock2.unlock().unwrap();
311 std::fs::remove_file(path).ok();
312 }
313
314 #[test]
315 fn drop_releases_locks() {
316 let (path, file1) = test_file("drop_release");
317 let file2 = OpenOptions::new()
318 .read(true)
319 .write(true)
320 .open(&path)
321 .unwrap();
322
323 {
324 let mut lock1 = FileLock::new(&file1);
325 lock1.lock_shared().unwrap();
326 lock1
327 .lock_reserved(std::time::Duration::from_secs(5))
328 .unwrap();
329 }
331
332 let mut lock2 = FileLock::new(&file2);
334 lock2.lock_shared().unwrap();
335 lock2
336 .lock_reserved(std::time::Duration::from_secs(5))
337 .unwrap();
338 lock2.downgrade_to_shared().unwrap();
339 lock2.unlock().unwrap();
340 std::fs::remove_file(path).ok();
341 }
342}