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}