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}