1use std::fs;
9use std::fs::File;
10use std::io;
11use std::path::Path;
12use std::path::PathBuf;
13
14use fs2::FileExt;
15
16use crate::errors::IoResultExt;
17use crate::utils;
18
19pub struct ScopedFileLock<'a> {
21 file: &'a mut File,
22}
23
24impl<'a> ScopedFileLock<'a> {
25 pub fn new(file: &'a mut File, exclusive: bool) -> io::Result<Self> {
26 if exclusive {
27 file.lock_exclusive()?;
28 } else {
29 file.lock_shared()?;
30 }
31 Ok(ScopedFileLock { file })
32 }
33}
34
35impl<'a> AsRef<File> for ScopedFileLock<'a> {
36 fn as_ref(&self) -> &File {
37 self.file
38 }
39}
40
41impl<'a> AsMut<File> for ScopedFileLock<'a> {
42 fn as_mut(&mut self) -> &mut File {
43 self.file
44 }
45}
46
47impl<'a> Drop for ScopedFileLock<'a> {
48 fn drop(&mut self) {
49 self.file.unlock().expect("unlock");
50 }
51}
52
53pub struct ScopedDirLock {
55 file: File,
56 path: PathBuf,
57}
58
59pub struct DirLockOptions {
61 pub exclusive: bool,
62 pub non_blocking: bool,
63 pub file_name: &'static str,
64}
65
66pub(crate) static READER_LOCK_OPTS: DirLockOptions = DirLockOptions {
75 exclusive: false,
76 non_blocking: false,
77 file_name: "rlock",
84};
85
86impl ScopedDirLock {
87 pub fn new(path: &Path) -> crate::Result<Self> {
89 const DEFAULT_OPTIONS: DirLockOptions = DirLockOptions {
90 exclusive: true,
91 non_blocking: false,
92 file_name: "",
93 };
94 Self::new_with_options(path, &DEFAULT_OPTIONS)
95 }
96
97 pub fn new_with_options(dir: &Path, opts: &DirLockOptions) -> crate::Result<Self> {
107 let (path, file) = if opts.file_name.is_empty() {
108 let file = utils::open_dir(dir).context(dir, "cannot open for locking")?;
109 (dir.to_path_buf(), file)
110 } else {
111 let path = dir.join(opts.file_name);
112
113 let file = match fs::OpenOptions::new().read(true).open(&path) {
116 Ok(f) => f,
117 Err(e) if e.kind() == io::ErrorKind::NotFound => {
118 utils::mkdir_p(dir)?;
120 fs::OpenOptions::new()
121 .write(true)
122 .create(true)
123 .open(&path)
124 .context(&path, "cannot create for locking")?
125 }
126 Err(e) => {
127 return Err(e).context(&path, "cannot open for locking");
128 }
129 };
130 (path, file)
131 };
132
133 match (opts.exclusive, opts.non_blocking) {
135 (true, false) => file.lock_exclusive(),
136 (true, true) => file.try_lock_exclusive(),
137 (false, false) => file.lock_shared(),
138 (false, true) => file.try_lock_shared(),
139 }
140 .context(&path, || {
141 format!(
142 "cannot lock (exclusive: {}, non_blocking: {})",
143 opts.exclusive, opts.non_blocking,
144 )
145 })?;
146
147 let result = Self { file, path };
148 Ok(result)
149 }
150
151 pub fn path(&self) -> &Path {
153 &self.path
154 }
155}
156
157impl Drop for ScopedDirLock {
158 fn drop(&mut self) {
159 self.file.unlock().expect("unlock");
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use std::fs::OpenOptions;
166 use std::io::Read;
167 use std::io::Seek;
168 use std::io::SeekFrom;
169 use std::io::Write;
170 use std::thread;
171
172 use tempfile::tempdir;
173
174 use super::*;
175
176 #[test]
177 fn test_file_lock() {
178 let dir = tempdir().unwrap();
179 let _file = OpenOptions::new()
180 .write(true)
181 .create(true)
182 .open(dir.path().join("f"))
183 .unwrap();
184
185 const N: usize = 40;
186
187 let threads: Vec<_> = (0..N)
189 .map(|i| {
190 let i = i;
191 let path = dir.path().join("f");
192 thread::spawn(move || {
193 let write = i % 2 == 0;
194 let mut file = OpenOptions::new()
195 .write(write)
196 .read(true)
197 .open(path)
198 .unwrap();
199 let mut lock = ScopedFileLock::new(&mut file, write).unwrap();
200 let len = lock.as_mut().seek(SeekFrom::End(0)).unwrap();
201 let ptr1 = lock.as_mut() as *const File;
202 let ptr2 = lock.as_ref() as *const File;
203 assert_eq!(ptr1, ptr2);
204 assert_eq!(len % 227, 0);
205 if write {
206 for j in 0..227 {
207 lock.as_mut().write_all(&[j]).expect("write");
208 lock.as_mut().flush().expect("flush");
209 }
210 }
211 })
212 })
213 .collect();
214
215 for thread in threads {
217 thread.join().expect("joined");
218 }
219
220 let mut file = OpenOptions::new()
222 .read(true)
223 .open(dir.path().join("f"))
224 .unwrap();
225 let mut buf = [0u8; 227];
226 let expected: Vec<u8> = (0..227).collect();
227 for _ in 0..(N / 2) {
228 file.read_exact(&mut buf).expect("read");
229 assert_eq!(&buf[..], &expected[..]);
230 }
231 }
232
233 #[test]
234 fn test_dir_lock() {
235 let dir = tempdir().unwrap();
236 let _file = OpenOptions::new()
237 .write(true)
238 .create(true)
239 .open(dir.path().join("f"))
240 .unwrap();
241
242 const N: usize = 40;
243
244 let threads: Vec<_> = (0..N)
246 .map(|i| {
247 let i = i;
248 let path = dir.path().join("f");
249 let dir_path = dir.path().to_path_buf();
250 thread::spawn(move || {
251 let write = i % 2 == 0;
252 let mut _lock = ScopedDirLock::new(&dir_path).unwrap();
253 let mut file = OpenOptions::new()
254 .write(write)
255 .read(true)
256 .open(path)
257 .unwrap();
258 let len = file.seek(SeekFrom::End(0)).unwrap();
259 assert_eq!(len % 227, 0);
260 if write {
261 for j in 0..227 {
262 file.write_all(&[j]).expect("write");
263 file.flush().expect("flush");
264 }
265 }
266 })
267 })
268 .collect();
269
270 for thread in threads {
272 thread.join().expect("joined");
273 }
274
275 let mut file = OpenOptions::new()
277 .read(true)
278 .open(dir.path().join("f"))
279 .unwrap();
280 let mut buf = [0u8; 227];
281 let expected: Vec<u8> = (0..227).collect();
282 for _ in 0..(N / 2) {
283 file.read_exact(&mut buf).expect("read");
284 assert_eq!(&buf[..], &expected[..]);
285 }
286 }
287
288 #[test]
289 fn test_dir_lock_with_options() {
290 let dir = tempdir().unwrap();
291 let path = dir.path();
292 let opts = DirLockOptions {
293 file_name: "foo",
294 exclusive: false,
295 non_blocking: false,
296 };
297
298 let l1 = ScopedDirLock::new_with_options(path, &opts).unwrap();
300 let l2 = ScopedDirLock::new_with_options(path, &opts).unwrap();
301
302 let opts = DirLockOptions {
303 non_blocking: true,
304 ..opts
305 };
306 let l3 = ScopedDirLock::new_with_options(path, &opts).unwrap();
307
308 let opts = DirLockOptions {
310 exclusive: true,
311 ..opts
312 };
313 assert!(ScopedDirLock::new_with_options(path, &opts).is_err());
314
315 drop((l1, l2, l3));
317 let l4 = ScopedDirLock::new_with_options(path, &opts).unwrap();
318
319 assert!(ScopedDirLock::new_with_options(path, &opts).is_err());
321
322 let opts = DirLockOptions {
324 file_name: "bar",
325 ..opts
326 };
327 assert!(ScopedDirLock::new_with_options(path, &opts).is_ok());
328
329 drop(l4);
330 }
331}