fslock_guard/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_time_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45#![allow(clippy::collapsible_if)] // See arti#2342
46#![deny(clippy::unused_async)]
47//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
48
49use std::path::Path;
50
51/// A lock-file for which we hold the lock.
52///
53/// So long as this object exists, we hold the lock on this file.
54/// When it is dropped, we will release the lock.
55///
56/// # Semantics
57///
58/// * Only one `LockFileGuard` can exist at one time
59/// for any particular `path`.
60/// * This applies across all tasks and threads in all programs;
61/// other acquisitions of the lock in the same process are prevented.
62/// * This applies across even separate machines, if `path` is on a shared filesystem.
63///
64/// # Restrictions
65///
66/// * **`path` must only be deleted (or renamed) via the APIs in this module**
67/// * This restriction applies to all programs on the computer,
68/// so for example automatic file cleaning with `find` and `rm` is forbidden.
69/// * Cross-filesystem locking is broken on Linux before 2.6.12.
70#[derive(Debug)]
71pub struct LockFileGuard {
72 /// A locked [`fslock::LockFile`].
73 ///
74 /// This `LockFile` instance will remain locked for as long as this
75 /// LockFileGuard exists.
76 locked: fslock::LockFile,
77}
78
79impl LockFileGuard {
80 /// Try to construct a new [`LockFileGuard`] representing a lock we hold on
81 /// the file `path`.
82 ///
83 /// Blocks until we can get the lock.
84 pub fn lock<P>(path: P) -> Result<Self, fslock::Error>
85 where
86 P: AsRef<Path>,
87 {
88 let path = path.as_ref();
89 loop {
90 let mut lockfile = fslock::LockFile::open(path)?;
91 lockfile.lock()?;
92
93 if os::lockfile_has_path(&lockfile, path)? {
94 return Ok(Self { locked: lockfile });
95 }
96 }
97 }
98
99 /// Try to construct a new [`LockFileGuard`] representing a lock we hold on
100 /// the file `path`.
101 ///
102 /// Does not block; returns Ok(None) if somebody else holds the lock.
103 pub fn try_lock<P>(path: P) -> Result<Option<Self>, fslock::Error>
104 where
105 P: AsRef<Path>,
106 {
107 let path = path.as_ref();
108 let mut lockfile = fslock::LockFile::open(path)?;
109 if lockfile.try_lock()? && os::lockfile_has_path(&lockfile, path)? {
110 return Ok(Some(Self { locked: lockfile }));
111 }
112 Ok(None)
113 }
114
115 /// Try to delete the lock file that we hold.
116 ///
117 /// The provided `path` must be the same as was passed to `lock`.
118 pub fn delete_lock_file<P>(self, path: P) -> Result<(), std::io::Error>
119 where
120 P: AsRef<Path>,
121 {
122 let path = path.as_ref();
123 if os::lockfile_has_path(&self.locked, path)? {
124 std::fs::remove_file(path)
125 } else {
126 Err(std::io::Error::other(MismatchedPathError {}))
127 }
128 }
129}
130
131/// An error that we return when the path given to `delete_lock_file` does not
132/// match the file we have.
133///
134/// Since we wrap this in an `io::Error`, it doesn't need to be public or fancy.
135#[derive(thiserror::Error, Debug, Clone)]
136#[error("Called delete_lock_file with a mismatched path.")]
137struct MismatchedPathError {}
138
139// Note: This requires AsFd and AsHandle implementations for `LockFile`.
140// See https://github.com/brunoczim/fslock/pull/15
141// This is why we are using fslock-arti-fork in place of fslock.
142
143/// Platform module for locking protocol on Unix.
144///
145/// ### Locking protocol on Unix
146///
147/// The lock is held by an open-file iff:
148///
149/// * that open-file holds an `flock` `LOCK_EX` lock; and
150/// * the directory entry for `path` refers to the same file as the open-file
151///
152/// `path` may only refer to a plain file, or `ENOENT`.
153/// If `path` refers to a file,
154/// only the lockholder may cause it to no longer refer to that file.
155///
156/// In principle the open-file might be shared with subprocesses.
157/// Even a naive program can safely and correctly inherit and hold the lock,
158/// since the lockholder only needs to not close an fd.
159/// However uncontrolled leaking of the fd into other processes is undesirable,
160/// as it might cause delays or even deadlocks, if those processes' inheritors live too long.
161/// In our Rust implementation we don't support sharing the held lock
162/// with subprocesses or different process images (ie across exec);
163/// we use `O_CLOEXEC`.
164///
165/// #### Locking algorithm
166///
167/// 1. open the file with `O_CREAT|O_RDWR`
168/// 2. `flock LOCK_EX`
169/// 3. `fstat` the open-file and `lstat` the path
170/// 4. If the inode and device numbers don't match,
171/// close the fd and go back to the start.
172/// 5. Now we hold the lock.
173///
174/// Proof sketch:
175///
176/// If we get to point 5, we see that at point 3, we had the lock.
177/// No-one else could cause the conditions to become false
178/// in the meantime:
179/// no-one else ~~can~~ may make `path` refer to a different file
180/// since they don't hold the lock.
181/// And, no-one else can `flock` it since the kernel prevents
182/// a conflicting lock.
183/// So at step 5 we must still hold the lock.
184///
185/// #### Unlocking algorithm
186///
187/// 1. Close the fd.
188/// 2. Now we no longer hold the lock and others can acquire it.
189///
190/// This drops the open-file and
191/// leaves the lock available for another caller.
192///
193/// #### Deletion algorithm
194///
195/// 0. The lock must already be held
196/// 1. `unlink` the file
197/// 2. close the fd
198/// 3. Now we no longer hold the lock and others can acquire it.
199///
200/// Step 1 atomically falsifies the lock-holding condition.
201/// We are allowed to perform it because we hold the lock.
202///
203/// Concurrent lockers might open the old file,
204/// which we are about to delete.
205/// They will acquire their `flock` (locking step 2)
206/// after we close (deletion step 2)
207/// and then see that they have a stale file.
208#[cfg(unix)]
209mod os {
210 use std::{fs::File, os::fd::AsFd, os::unix::fs::MetadataExt as _, path::Path};
211
212 /// Return true if `lf` currently exists with the given `path`, and false otherwise.
213 pub(crate) fn lockfile_has_path(lf: &fslock::LockFile, path: &Path) -> std::io::Result<bool> {
214 let m1 = std::fs::metadata(path)?;
215 // TODO: This does an unnecessary dup().
216 let f_dup = File::from(lf.as_fd().try_clone_to_owned()?);
217 let m2 = f_dup.metadata()?;
218
219 Ok(m1.ino() == m2.ino() && m1.dev() == m2.dev())
220 }
221}
222
223/// Platform module for locking protocol on Windows.
224///
225/// The argument for correctness on Windows proceeds as for Unix, but with a
226/// higher degree of uncertainty, since we are not sufficient Windows experts to
227/// determine if our assumptions hold.
228///
229/// Here we assume as follows:
230/// * When `fslock` calls `CreateFileW`, it gets a `HANDLE` to an open file.
231/// As we use them, the `HANDLE` behaves
232/// similarly to the "fd" in the Unix argument above,
233/// and the open file behaves similarly to the "open-file".
234/// * We assume that any differences that exist in their behavior do not
235/// affect our correctness above.
236/// * When `fslock` calls `LockFileEx`, and it completes successfully,
237/// we now have a lock on the file.
238/// Only one lock can exist on a file at a time.
239/// * When we compare members of `handle.metadata()` and `path.metadata()`,
240/// the comparison will return equal if ~~and only if~~
241/// the two files are truly the same.
242/// * We rely on the property that a file cannot change its file_index while it is
243/// open.
244/// * Deleting the lock file will actually work, since `fslock` opened it with
245/// FILE_SHARE_DELETE.
246/// * When we delete the lock file, possibly-asynchronous ("deferred") deletion
247/// definitely won't mean that the OS kernel violates our rule that no-one but the lockholder
248/// is allowed to delete the file.
249/// * The above is true even if someone with read
250/// access to the file - eg the human user - opens it without the FILE_SHARE options.
251/// * The same is true even if there is a virus scanner.
252/// * The same is true even on a remote filesystem.
253/// * If someone with read access to the file - eg the human user - opens it for reading
254/// without FILE_SHARE options, the algorithm will still work and not fail
255/// with a file sharing violation io error.
256/// (Or, every program the user might use to randomly peer at files in arti's
257/// state directory, including the equivalents of `grep -R` and backup programs,
258/// will use suitable FILE_SHARE options.)
259/// (If this assumption is false, the consequence is not data loss;
260/// rather, arti would fall over. So that would be tolerable if we don't
261/// know how to do better, or if doing better is hard.)
262#[cfg(windows)]
263mod os {
264 use std::{fs::File, mem::MaybeUninit, os::windows::io::AsRawHandle, path::Path};
265 use winapi::um::fileapi::{BY_HANDLE_FILE_INFORMATION as Info, GetFileInformationByHandle};
266
267 /// Return true if `lf` currently exists with the given `path`, and false otherwise.
268 pub(crate) fn lockfile_has_path(lf: &fslock::LockFile, path: &Path) -> std::io::Result<bool> {
269 let mut m1: MaybeUninit<Info> = MaybeUninit::uninit();
270 let mut m2: MaybeUninit<Info> = MaybeUninit::uninit();
271
272 let f2 = File::open(path)?;
273
274 let (i1, i2) = unsafe {
275 if GetFileInformationByHandle(lf.as_raw_handle() as _, m1.as_mut_ptr()) == 0 {
276 return Err(std::io::Error::last_os_error());
277 }
278 if GetFileInformationByHandle(f2.as_raw_handle() as _, m2.as_mut_ptr()) == 0 {
279 return Err(std::io::Error::last_os_error());
280 }
281 (m1.assume_init(), m2.assume_init())
282 };
283
284 // This comparison is about the best we can do on Windows,
285 // though there are caveats.
286 //
287 // See Raymond Chen's writeup at
288 // https://devblogs.microsoft.com/oldnewthing/20220128-00/?p=106201
289 // and also see BurntSushi's caveats at
290 // https://github.com/BurntSushi/same-file/blob/master/src/win.rs
291 Ok(i1.nFileIndexHigh == i2.nFileIndexHigh
292 && i1.nFileIndexLow == i2.nFileIndexLow
293 && i1.dwVolumeSerialNumber == i2.dwVolumeSerialNumber)
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 // @@ begin test lint list maintained by maint/add_warning @@
300 #![allow(clippy::bool_assert_comparison)]
301 #![allow(clippy::clone_on_copy)]
302 #![allow(clippy::dbg_macro)]
303 #![allow(clippy::mixed_attributes_style)]
304 #![allow(clippy::print_stderr)]
305 #![allow(clippy::print_stdout)]
306 #![allow(clippy::single_char_pattern)]
307 #![allow(clippy::unwrap_used)]
308 #![allow(clippy::unchecked_time_subtraction)]
309 #![allow(clippy::useless_vec)]
310 #![allow(clippy::needless_pass_by_value)]
311 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
312
313 use crate::LockFileGuard;
314 use test_temp_dir::test_temp_dir;
315
316 #[test]
317 fn keep_lock_file_after_drop() {
318 test_temp_dir!().used_by(|dir| {
319 let file = dir.join("file");
320 let flock_guard = LockFileGuard::lock(&file).unwrap();
321 assert!(file.try_exists().unwrap());
322 drop(flock_guard);
323 assert!(file.try_exists().unwrap());
324 });
325 }
326
327 #[test]
328 fn delete_lock_file_if_requested() {
329 test_temp_dir!().used_by(|dir| {
330 let file = dir.join("file");
331 let flock_guard = LockFileGuard::lock(&file).unwrap();
332 assert!(file.try_exists().unwrap());
333 assert!(flock_guard.delete_lock_file(&file).is_ok());
334 assert!(!file.try_exists().unwrap());
335 });
336 }
337}