file_locker/
lib.rs

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