1#![expect(missing_docs)]
16
17mod backoff;
18#[cfg(unix)]
19mod unix;
20#[cfg(windows)]
21mod windows;
22
23use std::io;
24use std::path::PathBuf;
25
26use thiserror::Error;
27
28#[cfg(unix)]
29pub use self::unix::FileLock;
30#[cfg(windows)]
31pub use self::windows::FileLock;
32
33#[derive(Debug, Error)]
34#[error("{message}: {path}")]
35pub struct FileLockError {
36 pub message: &'static str,
37 pub path: PathBuf,
38 #[source]
39 pub err: io::Error,
40}
41
42#[cfg(test)]
43mod tests {
44 use std::cmp::max;
45 use std::fs;
46 use std::thread;
47 use std::time::Duration;
48
49 use super::*;
50 use crate::tests::new_temp_dir;
51
52 #[test]
53 fn lock_basic() {
54 let temp_dir = new_temp_dir();
55 let lock_path = temp_dir.path().join("test.lock");
56 assert!(!lock_path.exists());
57 {
58 let _lock = FileLock::lock(lock_path.clone()).unwrap();
59 assert!(lock_path.exists());
60 }
61 assert!(!lock_path.exists());
62 }
63
64 #[test]
65 fn lock_concurrent() {
66 let temp_dir = new_temp_dir();
67 let data_path = temp_dir.path().join("test");
68 let lock_path = temp_dir.path().join("test.lock");
69 fs::write(&data_path, 0_u32.to_le_bytes()).unwrap();
70 let num_threads = max(num_cpus::get(), 4);
71 thread::scope(|s| {
72 for _ in 0..num_threads {
73 s.spawn(|| {
74 let _lock = FileLock::lock(lock_path.clone()).unwrap();
75 let data = fs::read(&data_path).unwrap();
76 let value = u32::from_le_bytes(data.try_into().unwrap());
77 thread::sleep(Duration::from_millis(1));
78 fs::write(&data_path, (value + 1).to_le_bytes()).unwrap();
79 });
80 }
81 });
82 let data = fs::read(&data_path).unwrap();
83 let value = u32::from_le_bytes(data.try_into().unwrap());
84 assert_eq!(value, num_threads as u32);
85 }
86}