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

            
49
use std::{fs, 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)]
71
pub struct LockFileGuard {
72
    /// A [`File`](fs::File) with its exclusive lock held.
73
    ///
74
    /// This `File` instance will remain locked for as long as this
75
    /// LockFileGuard exists.
76
    locked_file: fs::File,
77
}
78

            
79
impl LockFileGuard {
80
    /// Try to open `path` with options suitable for using it as a lockfile,
81
    /// creating it as necessary.
82
2657
    fn open<P>(path: P) -> Result<fs::File, std::io::Error>
83
2657
    where
84
2657
        P: AsRef<Path>,
85
    {
86
2657
        fs::OpenOptions::new()
87
2657
            .read(true)
88
2657
            .write(true)
89
2657
            .create(true)
90
2657
            .truncate(false)
91
2657
            .open(&path)
92
2657
    }
93

            
94
    /// Try to construct a new [`LockFileGuard`] representing a lock we hold on
95
    /// the file `path`.
96
    ///
97
    /// Blocks until we can get the lock.
98
4
    pub fn lock<P>(path: P) -> Result<Self, std::io::Error>
99
4
    where
100
4
        P: AsRef<Path>,
101
    {
102
4
        let path = path.as_ref();
103
        loop {
104
4
            let file = Self::open(path)?;
105
4
            file.lock()?;
106

            
107
4
            if os::lockfile_has_path(&file, path)? {
108
4
                return Ok(Self { locked_file: file });
109
            }
110
        }
111
4
    }
112

            
113
    /// Try to construct a new [`LockFileGuard`] representing a lock we hold on
114
    /// the file `path`.
115
    ///
116
    /// Does not block; returns Ok(None) if somebody else holds the lock.
117
2653
    pub fn try_lock<P>(path: P) -> Result<Option<Self>, std::io::Error>
118
2653
    where
119
2653
        P: AsRef<Path>,
120
    {
121
2653
        let path = path.as_ref();
122
2653
        let file = Self::open(path)?;
123
2653
        match file.try_lock() {
124
            Ok(()) => {
125
2649
                if os::lockfile_has_path(&file, path)? {
126
2649
                    Ok(Some(Self { locked_file: file }))
127
                } else {
128
                    Ok(None)
129
                }
130
            }
131
4
            Err(fs::TryLockError::WouldBlock) => Ok(None),
132
            Err(fs::TryLockError::Error(e)) => Err(e),
133
        }
134
2653
    }
135

            
136
    /// Try to delete the lock file that we hold.
137
    ///
138
    /// The provided `path` must be the same as was passed to `lock`.
139
76
    pub fn delete_lock_file<P>(self, path: P) -> Result<(), std::io::Error>
140
76
    where
141
76
        P: AsRef<Path>,
142
    {
143
76
        let path = path.as_ref();
144
76
        if os::lockfile_has_path(&self.locked_file, path)? {
145
76
            std::fs::remove_file(path)
146
        } else {
147
            Err(std::io::Error::other(MismatchedPathError {}))
148
        }
149
76
    }
150
}
151

            
152
/// An error that we return when the path given to `delete_lock_file` does not
153
/// match the file we have.
154
///
155
/// Since we wrap this in an `io::Error`, it doesn't need to be public or fancy.
156
#[derive(thiserror::Error, Debug, Clone)]
157
#[error("Called delete_lock_file with a mismatched path.")]
158
struct MismatchedPathError {}
159

            
160
/// Platform module for locking protocol on Unix.
161
///
162
/// ### Locking protocol on Unix
163
///
164
/// The lock is held by an open-file iff:
165
///
166
///  * that open-file holds an `flock` `LOCK_EX` lock; and
167
///  * the directory entry for `path` refers to the same file as the open-file
168
///
169
/// `path` may only refer to a plain file, or `ENOENT`.
170
/// If `path` refers to a file,
171
/// only the lockholder may cause it to no longer refer to that file.
172
///
173
/// In principle the open-file might be shared with subprocesses.
174
/// Even a naive program can safely and correctly inherit and hold the lock,
175
/// since the lockholder only needs to not close an fd.
176
/// However uncontrolled leaking of the fd into other processes is undesirable,
177
/// as it might cause delays or even deadlocks, if those processes' inheritors live too long.
178
/// In our Rust implementation we don't support sharing the held lock
179
/// with subprocesses or different process images (ie across exec);
180
/// we use `O_CLOEXEC`.
181
///
182
/// #### Locking algorithm
183
///
184
///  1. open the file with `O_CREAT|O_RDWR`
185
///  2. `flock LOCK_EX`
186
///  3. `fstat` the open-file and `lstat` the path
187
///  4. If the inode and device numbers don't match,
188
///     close the fd and go back to the start.
189
///  5. Now we hold the lock.
190
///
191
/// Proof sketch:
192
///
193
/// If we get to point 5, we see that at point 3, we had the lock.
194
/// No-one else could cause the conditions to become false
195
/// in the meantime:
196
/// no-one else ~~can~~ may make `path` refer to a different file
197
/// since they don't hold the lock.
198
/// And, no-one else can `flock` it since the kernel prevents
199
/// a conflicting lock.
200
/// So at step 5 we must still hold the lock.
201
///
202
/// #### Unlocking algorithm
203
///
204
///  1. Close the fd.
205
///  2. Now we no longer hold the lock and others can acquire it.
206
///
207
/// This drops the open-file and
208
/// leaves the lock available for another caller.
209
///
210
/// #### Deletion algorithm
211
///
212
///  0. The lock must already be held
213
///  1. `unlink` the file
214
///  2. close the fd
215
///  3. Now we no longer hold the lock and others can acquire it.
216
///
217
/// Step 1 atomically falsifies the lock-holding condition.
218
/// We are allowed to perform it because we hold the lock.
219
///
220
/// Concurrent lockers might open the old file,
221
/// which we are about to delete.
222
/// They will acquire their `flock` (locking step 2)
223
/// after we close (deletion step 2)
224
/// and then see that they have a stale file.
225
#[cfg(unix)]
226
mod os {
227
    use std::{fs::File, os::unix::fs::MetadataExt as _, path::Path};
228

            
229
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
230
8118
    pub(crate) fn lockfile_has_path(lf: &File, path: &Path) -> std::io::Result<bool> {
231
8118
        let m1 = std::fs::metadata(path)?;
232
8118
        let m2 = lf.metadata()?;
233

            
234
8118
        Ok(m1.ino() == m2.ino() && m1.dev() == m2.dev())
235
8118
    }
236
}
237

            
238
/// Platform module for locking protocol on Windows.
239
///
240
/// The argument for correctness on Windows proceeds as for Unix, but with a
241
/// higher degree of uncertainty, since we are not sufficient Windows experts to
242
/// determine if our assumptions hold.
243
///
244
/// Here we assume as follows:
245
/// * When `File::open` calls `CreateFileW`, it gets a `HANDLE` to an open file.
246
///   As we use them, the `HANDLE` behaves
247
///   similarly to the "fd" in the Unix argument above,
248
///   and the open file behaves similarly to the "open-file".
249
///   * We assume that any differences that exist in their behavior do not
250
///     affect our correctness above.
251
/// * When `File::lock` calls `LockFileEx`, and it completes successfully,
252
///   we now have a lock on the file.
253
///   Only one lock can exist on a file at a time.
254
/// * When we compare members of `handle.metadata()` and `path.metadata()`,
255
///   the comparison will return equal if ~~and only if~~
256
///   the two files are truly the same.
257
///   * We rely on the property that a file cannot change its file_index while it is
258
///     open.
259
/// * Deleting the lock file will actually work, since `File::open` opened it with
260
///   FILE_SHARE_DELETE.  (This is the default according to the documentation
261
///   for `OpenOptionsExt::share_mode`.)
262
/// * When we delete the lock file, possibly-asynchronous ("deferred") deletion
263
///   definitely won't mean that the OS kernel violates our rule that no-one but the lockholder
264
///   is allowed to delete the file.
265
/// * The above is true even if someone with read
266
///   access to the file - eg the human user - opens it without the FILE_SHARE options.
267
/// * The same is true even if there is a virus scanner.
268
/// * The same is true even on a remote filesystem.
269
/// * If someone with read access to the file - eg the human user - opens it for reading
270
///   without FILE_SHARE options, the algorithm will still work and not fail
271
///   with a file sharing violation io error.
272
///   (Or, every program the user might use to randomly peer at files in arti's
273
///   state directory, including the equivalents of `grep -R` and backup programs,
274
///   will use suitable FILE_SHARE options.)
275
///   (If this assumption is false, the consequence is not data loss;
276
///   rather, arti would fall over.  So that would be tolerable if we don't
277
///   know how to do better, or if doing better is hard.)
278
#[cfg(windows)]
279
mod os {
280
    use std::{fs::File, mem::MaybeUninit, os::windows::io::AsRawHandle, path::Path};
281
    use winapi::um::fileapi::{BY_HANDLE_FILE_INFORMATION as Info, GetFileInformationByHandle};
282

            
283
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
284
    pub(crate) fn lockfile_has_path(lf: &File, path: &Path) -> std::io::Result<bool> {
285
        let mut m1: MaybeUninit<Info> = MaybeUninit::uninit();
286
        let mut m2: MaybeUninit<Info> = MaybeUninit::uninit();
287

            
288
        let f2 = File::open(path)?;
289

            
290
        // Note: we would like to just use the MetadataExt methods for index and
291
        // volume serial number, but they are currently available only on nightly:
292
        // https://github.com/rust-lang/rust/issues/63010
293
        //
294
        // If and when they stabilize at our MSRV, we can use them here instead.
295

            
296
        let (i1, i2) = unsafe {
297
            // TODO: I am told that there is a GetFileInformationByHandleEx
298
            // that can return 128-bit IDs.
299
            if GetFileInformationByHandle(lf.as_raw_handle() as _, m1.as_mut_ptr()) == 0 {
300
                return Err(std::io::Error::last_os_error());
301
            }
302
            if GetFileInformationByHandle(f2.as_raw_handle() as _, m2.as_mut_ptr()) == 0 {
303
                return Err(std::io::Error::last_os_error());
304
            }
305
            (m1.assume_init(), m2.assume_init())
306
        };
307

            
308
        // This comparison is about the best we can do on Windows,
309
        // though there are caveats.
310
        //
311
        // See Raymond Chen's writeup at
312
        //   https://devblogs.microsoft.com/oldnewthing/20220128-00/?p=106201
313
        // and also see BurntSushi's caveats at
314
        //   https://github.com/BurntSushi/same-file/blob/master/src/win.rs
315
        Ok(i1.nFileIndexHigh == i2.nFileIndexHigh
316
            && i1.nFileIndexLow == i2.nFileIndexLow
317
            && i1.dwVolumeSerialNumber == i2.dwVolumeSerialNumber)
318
    }
319
}
320

            
321
/// Non-windows, non-unix implementation for lockfile_has_path.
322
///
323
/// For now, this implementation always reports an error.
324
/// It exists so that we can build (but not run) on wasm.
325
#[cfg(all(not(windows), not(unix)))]
326
mod os {
327
    use std::path::Path;
328

            
329
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
330
    pub(crate) fn lockfile_has_path(_lf: &std::fs::File, _path: &Path) -> std::io::Result<bool> {
331
        Err(std::io::Error::other(
332
            "fslock-guard does not support this operating system".to_string(),
333
        ))
334
    }
335
}
336

            
337
#[cfg(test)]
338
mod tests {
339
    // @@ begin test lint list maintained by maint/add_warning @@
340
    #![allow(clippy::bool_assert_comparison)]
341
    #![allow(clippy::clone_on_copy)]
342
    #![allow(clippy::dbg_macro)]
343
    #![allow(clippy::mixed_attributes_style)]
344
    #![allow(clippy::print_stderr)]
345
    #![allow(clippy::print_stdout)]
346
    #![allow(clippy::single_char_pattern)]
347
    #![allow(clippy::unwrap_used)]
348
    #![allow(clippy::unchecked_time_subtraction)]
349
    #![allow(clippy::useless_vec)]
350
    #![allow(clippy::needless_pass_by_value)]
351
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
352

            
353
    use crate::LockFileGuard;
354
    use test_temp_dir::test_temp_dir;
355

            
356
    #[test]
357
    fn keep_lock_file_after_drop() {
358
        test_temp_dir!().used_by(|dir| {
359
            let file = dir.join("file");
360
            let flock_guard = LockFileGuard::lock(&file).unwrap();
361
            assert!(file.try_exists().unwrap());
362
            drop(flock_guard);
363
            assert!(file.try_exists().unwrap());
364
        });
365
    }
366

            
367
    #[test]
368
    fn delete_lock_file_if_requested() {
369
        test_temp_dir!().used_by(|dir| {
370
            let file = dir.join("file");
371
            let flock_guard = LockFileGuard::lock(&file).unwrap();
372
            assert!(file.try_exists().unwrap());
373
            assert!(flock_guard.delete_lock_file(&file).is_ok());
374
            assert!(!file.try_exists().unwrap());
375
        });
376
    }
377
}