file_lock/
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//! extern crate file_lock;
15//!
16//! use file_lock::{FileLock, FileOptions};
17//! use std::fs::OpenOptions;
18//! use std::io::prelude::*;
19//!
20//! fn main() {
21//!     let should_we_block  = true;
22//!     let options = FileOptions::new()
23//!                         .write(true)
24//!                         .create(true)
25//!                         .append(true);
26//!
27//!     let mut filelock = match FileLock::lock("myfile.txt", should_we_block, options) {
28//!         Ok(lock) => lock,
29//!         Err(err) => panic!("Error getting write lock: {}", err),
30//!     };
31//!
32//!     filelock.file.write_all(b"Hello, World!").is_ok();
33//!
34//!     // Manually unlocking is optional as we unlock on Drop
35//!     filelock.unlock();
36//! }
37//! ```
38
39mod file_options;
40
41use libc::c_int;
42use std::fs::File;
43use std::io::Error;
44use std::os::fd::AsRawFd;
45use std::path::Path;
46
47pub use file_options::FileOptions;
48
49extern "C" {
50    fn c_lock(fd: i32, is_blocking: i32, is_writeable: i32) -> c_int;
51    fn c_unlock(fd: i32) -> c_int;
52}
53
54/// Represents the actually locked file
55#[derive(Debug)]
56pub struct FileLock {
57    /// the `std::fs::File` of the file that's locked
58    pub file: File,
59}
60
61impl FileLock {
62    /// Try to lock the specified file
63    ///
64    /// # Parameters
65    ///
66    /// `path` is the path of the file we want to lock on
67    ///
68    /// `is_blocking` is a flag to indicate if we should block if it's already locked
69    ///
70    /// `options` is a mutable reference to a [`std::fs::OpenOptions`] object to configure the underlying file
71    ///
72    /// # Examples
73    ///
74    ///```
75    ///extern crate file_lock;
76    ///
77    ///use file_lock::{FileLock, FileOptions};
78    ///use std::fs::OpenOptions;
79    ///use std::io::prelude::*;
80    ///
81    ///fn main() {
82    ///    let should_we_block  = true;
83    ///    let options = FileOptions::new()
84    ///                        .write(true)
85    ///                        .create(true)
86    ///                        .append(true);
87    ///
88    ///    let mut filelock = match FileLock::lock("myfile.txt", should_we_block, options) {
89    ///        Ok(lock) => lock,
90    ///        Err(err) => panic!("Error getting write lock: {}", err),
91    ///    };
92    ///
93    ///    filelock.file.write_all(b"Hello, World!").is_ok();
94    ///}
95    ///```
96    ///
97    pub fn lock<P: AsRef<Path>>(
98        path: P,
99        is_blocking: bool,
100        options: FileOptions,
101    ) -> Result<FileLock, Error> {
102        let file = options.open(path)?;
103        let is_writeable = options.writeable;
104
105        let errno = unsafe { c_lock(file.as_raw_fd(), is_blocking as i32, is_writeable as i32) };
106
107        match errno {
108            0 => Ok(FileLock { file }),
109            _ => Err(Error::from_raw_os_error(errno)),
110        }
111    }
112
113    /// Unlock our locked file
114    ///
115    /// *Note:* This method is optional as the file lock will be unlocked automatically when dropped
116    ///
117    /// # Examples
118    ///
119    ///```
120    ///extern crate file_lock;
121    ///
122    ///use file_lock::{FileLock, FileOptions};
123    ///use std::io::prelude::*;
124    ///
125    ///fn main() {
126    ///    let should_we_block  = true;
127    ///    let lock_for_writing = FileOptions::new().write(true).create(true);
128    ///
129    ///    let mut filelock = match FileLock::lock("myfile.txt", should_we_block, lock_for_writing) {
130    ///        Ok(lock) => lock,
131    ///        Err(err) => panic!("Error getting write lock: {}", err),
132    ///    };
133    ///
134    ///    filelock.file.write_all(b"Hello, World!").is_ok();
135    ///
136    ///    match filelock.unlock() {
137    ///        Ok(_)    => println!("Successfully unlocked the file"),
138    ///        Err(err) => panic!("Error unlocking the file: {}", err),
139    ///    };
140    ///}
141    ///```
142    ///
143    pub fn unlock(&self) -> Result<(), Error> {
144        let errno = unsafe { c_unlock(self.file.as_raw_fd()) };
145
146        match errno {
147            0 => Ok(()),
148            _ => Err(Error::from_raw_os_error(errno)),
149        }
150    }
151}
152
153impl Drop for FileLock {
154    fn drop(&mut self) {
155        let _ = self.unlock().is_ok();
156    }
157}
158
159#[cfg(test)]
160mod test {
161    use super::*;
162
163    use nix::unistd::fork;
164    use nix::unistd::ForkResult::{Child, Parent};
165    use std::fs::{remove_file, OpenOptions};
166    use std::process;
167    use std::thread::sleep;
168    use std::time::Duration;
169
170    fn standard_options(is_writable: &bool) -> FileOptions {
171        FileOptions::new()
172            .read(!*is_writable)
173            .write(*is_writable)
174            .create(*is_writable)
175    }
176
177    #[test]
178    fn lock_and_unlock() {
179        let filename = "filelock.test";
180
181        for already_exists in &[true, false] {
182            for already_locked in &[true, false] {
183                for already_writable in &[true, false] {
184                    for is_blocking in &[true, false] {
185                        for is_writable in &[true, false] {
186                            if !*already_exists && (*already_locked || *already_writable) {
187                                // nonsensical tests
188                                continue;
189                            }
190
191                            let _ = remove_file(&filename).is_ok();
192
193                            let parent_lock = match *already_exists {
194                                false => None,
195                                true => {
196                                    OpenOptions::new()
197                                        .write(true)
198                                        .create(true)
199                                        .open(&filename)
200                                        .expect("Test failed");
201
202                                    match *already_locked {
203                                        false => None,
204                                        true => {
205                                            let options = standard_options(already_writable);
206                                            match FileLock::lock(filename, true, options) {
207                                                Ok(lock) => Some(lock),
208                                                Err(err) => {
209                                                    panic!("Error creating parent lock ({})", err)
210                                                }
211                                            }
212                                        }
213                                    }
214                                }
215                            };
216
217                            unsafe {
218                                match fork() {
219                                    Ok(Parent { child: _ }) => {
220                                        sleep(Duration::from_millis(150));
221
222                                        if let Some(lock) = parent_lock {
223                                            lock.unlock().expect("Test failed");
224                                        }
225
226                                        sleep(Duration::from_millis(350));
227                                    }
228                                    Ok(Child) => {
229                                        let mut try_count = 0;
230                                        let mut locked = false;
231
232                                        match *already_locked {
233                                            true => match *is_blocking {
234                                                true => {
235                                                    let options = standard_options(is_writable);
236                                                    match FileLock::lock(filename, *is_blocking, options) {
237                                                    Ok(_)  => { locked = true },
238                                                    Err(_) => panic!("Error getting lock after wating for release"),
239                                                }
240                                                }
241                                                false => {
242                                                    for _ in 0..5 {
243                                                        let options = standard_options(is_writable);
244                                                        match FileLock::lock(
245                                                            filename,
246                                                            *is_blocking,
247                                                            options,
248                                                        ) {
249                                                            Ok(_) => {
250                                                                locked = true;
251                                                                break;
252                                                            }
253                                                            Err(_) => {
254                                                                sleep(Duration::from_millis(50));
255                                                                try_count += 1;
256                                                            }
257                                                        }
258                                                    }
259                                                }
260                                            },
261                                            false => {
262                                                let options = standard_options(is_writable);
263                                                match FileLock::lock(
264                                                    filename,
265                                                    *is_blocking,
266                                                    options,
267                                                ) {
268                                                    Ok(_) => locked = true,
269                                                    Err(_) => {
270                                                        match !*already_exists && !*is_writable {
271                                                            true => {}
272                                                            false => {
273                                                                panic!("Error getting lock with no competition")
274                                                            }
275                                                        }
276                                                    }
277                                                }
278                                            }
279                                        }
280
281                                        match !already_exists && !is_writable {
282                                            true => assert!(
283                                            !locked,
284                                            "Locking a non-existent file for reading should fail"
285                                        ),
286                                            false => {
287                                                assert!(locked, "Lock should have been successful")
288                                            }
289                                        }
290
291                                        match *is_blocking {
292                                        true  => assert_eq!(try_count, 0, "Try count should be zero when blocking"),
293                                        false => {
294                                            match *already_locked {
295                                                false => assert_eq!(try_count, 0, "Try count should be zero when no competition"),
296                                                true  => match !already_writable && !is_writable {
297                                                    true  => assert_eq!(try_count, 0, "Read lock when locked for reading should succeed first go"),
298                                                    false => assert!(try_count >= 3, "Try count should be >= 3"),
299                                                },
300                                            }
301                                        },
302                                    }
303
304                                        process::exit(7);
305                                    }
306                                    Err(_) => {
307                                        panic!("Error forking tests :(");
308                                    }
309                                }
310                            }
311
312                            let _ = remove_file(&filename).is_ok();
313                        }
314                    }
315                }
316            }
317        }
318    }
319
320    #[test]
321    fn lock_for_read_or_write_only() -> std::io::Result<()> {
322        let filename = "lock_for_read_only.test";
323        std::fs::write(filename, format!("Just at test\n"))?;
324        let lock = FileLock::lock(filename, false, FileOptions::new().read(true))?;
325        lock.unlock()?;
326        FileLock::lock(filename, false, FileOptions::new().write(true).read(false))?;
327        Ok(())
328    }
329}