file_locker/lib.rs
1//! File locking via POSIX advisory record locks.
2//!
3//! This crate provides the facility to obtain a write-lock and unlock a file
4//! following the advisory record lock scheme as specified by UNIX IEEE Std 1003.1-2001
5//! (POSIX.1) via `fcntl()`.
6//!
7//! # Examples
8//!
9//! Please note that the examples use `tempfile` merely to quickly create a file
10//! which is removed automatically. In the common case, you would want to lock
11//! a file which is known to multiple processes.
12//!
13//! ```
14//! use file_locker::FileLock;
15//! use std::io::prelude::*;
16//! use std::io::Result;
17//!
18//! fn main() -> Result<()> {
19//! let mut filelock = FileLock::new("myfile.txt")
20//! .blocking(true)
21//! .writeable(true)
22//! .lock()?;
23//!
24//! filelock.file.write_all(b"Hello, World!")?;
25//!
26//! // Manually unlocking is optional as we unlock on Drop
27//! filelock.unlock()?;
28//! Ok(())
29//! }
30//! ```
31
32use nix::{
33 fcntl::{fcntl, FcntlArg},
34 libc,
35};
36use std::{
37 fs::{File, OpenOptions},
38 io::{prelude::*, Error, IoSlice, IoSliceMut, Result, SeekFrom},
39 os::unix::{
40 fs::FileExt,
41 io::{AsRawFd, RawFd},
42 },
43 path::Path,
44};
45
46/// Represents the actually locked file
47#[derive(Debug)]
48pub struct FileLock {
49 /// the `std::fs::File` of the file that's locked
50 pub file: File,
51}
52
53impl FileLock {
54 /// Create a [`FileLockBuilder`](struct.FileLockBuilder.html)
55 ///
56 /// blocking and writeable default to false
57 ///
58 /// # Examples
59 ///
60 ///```
61 ///use file_locker::FileLock;
62 ///use std::io::prelude::*;
63 ///use std::io::Result;
64 ///
65 ///fn main() -> Result<()> {
66 /// let mut filelock = FileLock::new("myfile.txt")
67 /// .writeable(true)
68 /// .blocking(true)
69 /// .lock()?;
70 ///
71 /// filelock.file.write_all(b"Hello, world")?;
72 /// Ok(())
73 ///}
74 ///```
75 ///
76 pub fn new<T: AsRef<Path>>(file_path: T) -> FileLockBuilder<T> {
77 FileLockBuilder {
78 file_path,
79 blocking: false,
80 writeable: false,
81 }
82 }
83
84 /// Try to lock the specified file
85 ///
86 /// # Parameters
87 ///
88 /// - `filename` is the path of the file we want to lock on
89 ///
90 /// - `is_blocking` is a flag to indicate if we should block if it's already locked
91 ///
92 /// If set, this call will block until the lock can be obtained.
93 /// If not set, this call will return immediately, giving an error if it would block
94 ///
95 /// - `is_writable` is a flag to indicate if we want to lock for writing
96 ///
97 /// # Examples
98 ///
99 ///```
100 ///use file_locker::FileLock;
101 ///use std::io::prelude::*;
102 ///use std::io::Result;
103 ///
104 ///fn main() -> Result<()> {
105 /// let mut filelock = FileLock::lock("myfile.txt", false, false)?;
106 ///
107 /// let mut buf = String::new();
108 /// filelock.file.read_to_string(&mut buf)?;
109 /// Ok(())
110 ///}
111 ///```
112 ///
113 pub fn lock(
114 file_path: impl AsRef<Path>,
115 blocking: bool,
116 writeable: bool,
117 ) -> Result<FileLock> {
118 let file = OpenOptions::new()
119 .read(true)
120 .write(writeable)
121 .create(writeable)
122 .open(&file_path)?;
123 let flock = libc::flock {
124 l_type: if writeable {
125 libc::F_WRLCK
126 } else {
127 libc::F_RDLCK
128 } as i16,
129 l_whence: libc::SEEK_SET as i16,
130 l_start: 0,
131 l_len: 0,
132 l_pid: 0,
133 #[cfg(target_os = "freebsd")]
134 l_sysid: 0,
135 };
136 let arg = if blocking {
137 FcntlArg::F_SETLKW(&flock)
138 } else {
139 FcntlArg::F_SETLK(&flock)
140 };
141 fcntl(file.as_raw_fd(), arg).map_err(cver)?;
142 Ok(Self { file })
143 }
144
145 /// Unlock our locked file
146 ///
147 /// *Note:* This method is optional as the file lock will be unlocked automatically when dropped
148 ///
149 /// # Examples
150 ///
151 ///```
152 ///use file_locker::FileLock;
153 ///use std::io::prelude::*;
154 ///use std::io::Result;
155 ///
156 ///fn main() -> Result<()> {
157 /// let mut filelock = FileLock::new("myfile.txt")
158 /// .writeable(true)
159 /// .blocking(true)
160 /// .lock()?;
161 ///
162 /// filelock.file.write_all(b"Hello, world")?;
163 ///
164 /// filelock.unlock()?;
165 /// Ok(())
166 ///}
167 ///```
168 ///
169 pub fn unlock(&self) -> Result<()> {
170 let flock = libc::flock {
171 l_type: libc::F_UNLCK as i16,
172 l_whence: libc::SEEK_SET as i16,
173 l_start: 0,
174 l_len: 0,
175 l_pid: 0,
176 #[cfg(target_os = "freebsd")]
177 l_sysid: 0,
178 };
179 fcntl(self.file.as_raw_fd(), FcntlArg::F_SETLK(&flock))
180 .map_err(cver)?;
181 Ok(())
182 }
183}
184
185impl Read for FileLock {
186 fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
187 self.file.read(buf)
188 }
189
190 fn read_vectored(&mut self, bufs: &mut [IoSliceMut]) -> Result<usize> {
191 self.file.read_vectored(bufs)
192 }
193}
194
195impl Write for FileLock {
196 fn write(&mut self, buf: &[u8]) -> Result<usize> {
197 self.file.write(buf)
198 }
199
200 fn flush(&mut self) -> Result<()> {
201 self.file.flush()
202 }
203
204 fn write_vectored(&mut self, bufs: &[IoSlice]) -> Result<usize> {
205 self.file.write_vectored(bufs)
206 }
207}
208
209impl Seek for FileLock {
210 fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
211 self.file.seek(pos)
212 }
213}
214
215impl AsRawFd for FileLock {
216 fn as_raw_fd(&self) -> RawFd {
217 self.file.as_raw_fd()
218 }
219}
220
221impl FileExt for FileLock {
222 fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<usize> {
223 self.file.read_at(buf, offset)
224 }
225
226 fn write_at(&self, buf: &[u8], offset: u64) -> Result<usize> {
227 self.file.write_at(buf, offset)
228 }
229}
230
231/// Builder to create [`FileLock`](struct.FileLock.html)
232///
233/// blocking and writeable default to false
234#[derive(Debug)]
235pub struct FileLockBuilder<T> {
236 file_path: T,
237 blocking: bool,
238 writeable: bool,
239}
240
241impl<T: AsRef<Path>> FileLockBuilder<T> {
242 /// Set lock to blocking mode
243 pub fn blocking(mut self, v: bool) -> Self {
244 self.blocking = v;
245 self
246 }
247
248 /// Open file as writeable and get exclusive lock
249 pub fn writeable(mut self, v: bool) -> Self {
250 self.writeable = v;
251 self
252 }
253
254 /// Create a [`FileLock`](struct.FileLock.html) with these parameters.
255 /// Calls [`FileLock::lock`](struct.FileLock.html#method.lock)
256 pub fn lock(self) -> Result<FileLock> {
257 FileLock::lock(self.file_path, self.blocking, self.writeable)
258 }
259}
260
261impl Drop for FileLock {
262 fn drop(&mut self) {
263 let _ = self.unlock();
264 }
265}
266
267fn cver(e: nix::Error) -> Error {
268 Error::from_raw_os_error(e as i32)
269}
270
271#[cfg(test)]
272mod test {
273 use super::*;
274
275 use nix::unistd::fork;
276 use nix::unistd::ForkResult::{Child, Parent};
277 use std::fs::remove_file;
278 use std::process;
279 use std::thread::sleep;
280 use std::time::Duration;
281
282 #[test]
283 fn lock_and_unlock() {
284 let filename = "filelock.test";
285
286 for already_exists in &[true, false] {
287 for already_locked in &[true, false] {
288 for already_writable in &[true, false] {
289 for is_blocking in &[true, false] {
290 for is_writable in &[true, false] {
291 if !*already_exists
292 && (*already_locked || *already_writable)
293 {
294 // nonsensical tests
295 continue;
296 }
297
298 let _ = remove_file(filename);
299
300 let parent_lock = match *already_exists {
301 false => None,
302 true => {
303 let _ = OpenOptions::new()
304 .write(true)
305 .create(true)
306 .truncate(true)
307 .open(filename);
308
309 match *already_locked {
310 false => None,
311 true => {
312 match FileLock::lock(
313 filename,
314 true,
315 *already_writable,
316 ) {
317 Ok(lock) => Some(lock),
318 Err(err) => {
319 panic!("Error creating parent lock ({})", err)
320 }
321 }
322 }
323 }
324 }
325 };
326
327 match unsafe { fork() } {
328 Ok(Parent { child: _ }) => {
329 sleep(Duration::from_millis(150));
330
331 if let Some(lock) = parent_lock {
332 let _ = lock.unlock();
333 }
334
335 sleep(Duration::from_millis(350));
336 }
337 Ok(Child) => {
338 let mut try_count = 0;
339 let mut locked = false;
340
341 match *already_locked {
342 true => match *is_blocking {
343 true => {
344 match FileLock::lock(filename, *is_blocking, *is_writable) {
345 Ok(_) => { locked = true },
346 Err(_) => panic!("Error getting lock after wating for release"),
347 }
348 }
349 false => {
350 for _ in 0..5 {
351 match FileLock::lock(
352 filename,
353 *is_blocking,
354 *is_writable,
355 ) {
356 Ok(_) => {
357 locked = true;
358 break;
359 }
360 Err(_) => {
361 sleep(Duration::from_millis(50));
362 try_count += 1;
363 }
364 }
365 }
366 }
367 },
368 false => match FileLock::lock(
369 filename,
370 *is_blocking,
371 *is_writable,
372 ) {
373 Ok(_) => locked = true,
374 Err(_) => match !*already_exists
375 && !*is_writable
376 {
377 true => {}
378 false => {
379 panic!("Error getting lock with no competition")
380 }
381 },
382 },
383 }
384
385 match !*already_exists && !is_writable {
386 true => assert!(
387 !locked,
388 "Locking a non-existent file for reading should fail"
389 ),
390 false => assert!(
391 locked,
392 "Lock should have been successful"
393 ),
394 }
395
396 match *is_blocking {
397 true => assert!(try_count == 0, "Try count should be zero when blocking"),
398 false => {
399 match *already_locked {
400 false => assert!(try_count == 0, "Try count should be zero when no competition"),
401 true => match !*already_writable && !is_writable {
402 true => assert!(try_count == 0, "Read lock when locked for reading should succeed first go"),
403 false => assert!(try_count >= 3, "Try count should be >= 3"),
404 },
405 }
406 },
407 }
408
409 process::exit(7);
410 }
411 Err(_) => {
412 panic!("Error forking tests :(");
413 }
414 }
415
416 let _ = remove_file(filename);
417 }
418 }
419 }
420 }
421 }
422 }
423}