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}