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
#![deny(clippy::string_slice)] // See arti#2571
48
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
49

            
50
use std::{fs, path::Path};
51

            
52
/// A lock-file for which we hold the lock.
53
///
54
/// So long as this object exists, we hold the lock on this file.
55
/// When it is dropped, we will release the lock.
56
///
57
/// # Semantics
58
///
59
///  * Only one `LockFileGuard` can exist at one time
60
///    for any particular `path`.
61
///  * This applies across all tasks and threads in all programs;
62
///    other acquisitions of the lock in the same process are prevented.
63
///  * This applies across even separate machines, if `path` is on a shared filesystem.
64
///
65
/// # Restrictions
66
///
67
///  * **`path` must only be deleted (or renamed) via the APIs in this module**
68
///  * This restriction applies to all programs on the computer,
69
///    so for example automatic file cleaning with `find` and `rm` is forbidden.
70
///  * Cross-filesystem locking is broken on Linux before 2.6.12.
71
#[derive(Debug)]
72
pub struct LockFileGuard {
73
    /// A [`File`](fs::File) with its exclusive lock held.
74
    ///
75
    /// This `File` instance will remain locked for as long as this
76
    /// LockFileGuard exists.
77
    locked_file: fs::File,
78
}
79

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

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

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

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

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

            
153
/// Try to lock `f`, blocking if need be.
154
///
155
/// On non-android, this just calls [`fs::File::lock`].
156
#[cfg(not(target_os = "android"))]
157
4
fn do_lock(f: &fs::File) -> std::io::Result<()> {
158
4
    f.lock()
159
4
}
160

            
161
/// Try to lock `f`, without blocking.
162
///
163
/// On non-android, this just calls [`fs::File::try_lock`].
164
#[cfg(not(target_os = "android"))]
165
6432
fn do_try_lock(f: &fs::File) -> Result<(), std::fs::TryLockError> {
166
6432
    f.try_lock()
167
6432
}
168

            
169
/// Try to lock `f`, blocking if need be.
170
///
171
/// On android, we need to use flock manually, since Rust (as of May 2026)
172
/// always returns "not implemented" for `lock()` and `try_lock()`.
173
///
174
/// See <https://github.com/rust-lang/rust/issues/148325>.
175
/// Apparently,
176
/// although there are filesystems (specifically FUSE filesystems)
177
/// where flock won't work, it will correctly report ENOSYS
178
/// on those filesystems.
179
//
180
// TODO MSRV ????: we can remove this once Rust supports file locking on Android
181
// at our MSRV.  As of May 2026, https://github.com/rust-lang/rust/pull/157038/
182
// seems like the likeliest MR for that, but it has not been merged.
183
#[cfg(target_os = "android")]
184
fn do_lock(f: &fs::File) -> std::io::Result<()> {
185
    use std::os::fd::AsRawFd;
186

            
187
    let fd = f.as_raw_fd();
188
    // SAFETY: Since `f` is a file, it has a valid fd.
189
    let success = unsafe { libc::flock(fd, libc::LOCK_EX) } == 0;
190

            
191
    if success {
192
        Ok(())
193
    } else {
194
        Err(std::io::Error::last_os_error())
195
    }
196
}
197

            
198
/// Try to lock `f`, without blocking.
199
///
200
/// On android, we need to use flock manually, since Rust (as of May 2026)
201
/// always returns "not implemented" for `lock()` and `try_lock()`.
202
///
203
/// See <https://github.com/rust-lang/rust/issues/148325>.
204
/// Apparently,
205
/// although there are filesystems (specifically FUSE filesystems)
206
/// where flock won't work, it will correctly report ENOSYS
207
/// on those filesystems.
208
//
209
// TODO MSRV ????: See 'TODO MSRV' on do_lock above.
210
#[cfg(target_os = "android")]
211
fn do_try_lock(f: &fs::File) -> Result<(), std::fs::TryLockError> {
212
    use std::os::fd::AsRawFd;
213

            
214
    let fd = f.as_raw_fd();
215
    // SAFETY: Since `f` is a file, it has a valid fd.
216
    let success = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) } == 0;
217

            
218
    if success {
219
        Ok(())
220
    } else {
221
        let err = std::io::Error::last_os_error();
222
        if err.kind() == std::io::ErrorKind::WouldBlock {
223
            Err(std::fs::TryLockError::WouldBlock)
224
        } else {
225
            Err(std::fs::TryLockError::Error(err))
226
        }
227
    }
228
}
229

            
230
/// An error that we return when the path given to `delete_lock_file` does not
231
/// match the file we have.
232
///
233
/// Since we wrap this in an `io::Error`, it doesn't need to be public or fancy.
234
#[derive(thiserror::Error, Debug, Clone)]
235
#[error("Called delete_lock_file with a mismatched path.")]
236
struct MismatchedPathError {}
237

            
238
/// Platform module for locking protocol on Unix.
239
///
240
/// ### Locking protocol on Unix
241
///
242
/// The lock is held by an open-file iff:
243
///
244
///  * that open-file holds an `flock` `LOCK_EX` lock; and
245
///  * the directory entry for `path` refers to the same file as the open-file
246
///
247
/// `path` may only refer to a plain file, or `ENOENT`.
248
/// If `path` refers to a file,
249
/// only the lockholder may cause it to no longer refer to that file.
250
///
251
/// In principle the open-file might be shared with subprocesses.
252
/// Even a naive program can safely and correctly inherit and hold the lock,
253
/// since the lockholder only needs to not close an fd.
254
/// However uncontrolled leaking of the fd into other processes is undesirable,
255
/// as it might cause delays or even deadlocks, if those processes' inheritors live too long.
256
/// In our Rust implementation we don't support sharing the held lock
257
/// with subprocesses or different process images (ie across exec);
258
/// we use `O_CLOEXEC`.
259
///
260
/// #### Locking algorithm
261
///
262
///  1. open the file with `O_CREAT|O_RDWR`
263
///  2. `flock LOCK_EX`
264
///  3. `fstat` the open-file and `lstat` the path
265
///  4. If the inode and device numbers don't match,
266
///     close the fd and go back to the start.
267
///  5. Now we hold the lock.
268
///
269
/// Proof sketch:
270
///
271
/// If we get to point 5, we see that at point 3, we had the lock.
272
/// No-one else could cause the conditions to become false
273
/// in the meantime:
274
/// no-one else ~~can~~ may make `path` refer to a different file
275
/// since they don't hold the lock.
276
/// And, no-one else can `flock` it since the kernel prevents
277
/// a conflicting lock.
278
/// So at step 5 we must still hold the lock.
279
///
280
/// #### Unlocking algorithm
281
///
282
///  1. Close the fd.
283
///  2. Now we no longer hold the lock and others can acquire it.
284
///
285
/// This drops the open-file and
286
/// leaves the lock available for another caller.
287
///
288
/// #### Deletion algorithm
289
///
290
///  0. The lock must already be held
291
///  1. `unlink` the file
292
///  2. close the fd
293
///  3. Now we no longer hold the lock and others can acquire it.
294
///
295
/// Step 1 atomically falsifies the lock-holding condition.
296
/// We are allowed to perform it because we hold the lock.
297
///
298
/// Concurrent lockers might open the old file,
299
/// which we are about to delete.
300
/// They will acquire their `flock` (locking step 2)
301
/// after we close (deletion step 2)
302
/// and then see that they have a stale file.
303
#[cfg(unix)]
304
mod os {
305
    use std::{fs::File, os::unix::fs::MetadataExt as _, path::Path};
306

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

            
312
8118
        Ok(m1.ino() == m2.ino() && m1.dev() == m2.dev())
313
8118
    }
314
}
315

            
316
/// Platform module for locking protocol on Windows.
317
///
318
/// The argument for correctness on Windows proceeds as for Unix, but with a
319
/// higher degree of uncertainty, since we are not sufficient Windows experts to
320
/// determine if our assumptions hold.
321
///
322
/// Here we assume as follows:
323
/// * When `File::open` calls `CreateFileW`, it gets a `HANDLE` to an open file.
324
///   As we use them, the `HANDLE` behaves
325
///   similarly to the "fd" in the Unix argument above,
326
///   and the open file behaves similarly to the "open-file".
327
///   * We assume that any differences that exist in their behavior do not
328
///     affect our correctness above.
329
/// * When `File::lock` calls `LockFileEx`, and it completes successfully,
330
///   we now have a lock on the file.
331
///   Only one lock can exist on a file at a time.
332
/// * When we compare members of `handle.metadata()` and `path.metadata()`,
333
///   the comparison will return equal if ~~and only if~~
334
///   the two files are truly the same.
335
///   * We rely on the property that a file cannot change its file_index while it is
336
///     open.
337
/// * Deleting the lock file will actually work, since `File::open` opened it with
338
///   FILE_SHARE_DELETE.  (This is the default according to the documentation
339
///   for `OpenOptionsExt::share_mode`.)
340
/// * When we delete the lock file, possibly-asynchronous ("deferred") deletion
341
///   definitely won't mean that the OS kernel violates our rule that no-one but the lockholder
342
///   is allowed to delete the file.
343
/// * The above is true even if someone with read
344
///   access to the file - eg the human user - opens it without the FILE_SHARE options.
345
/// * The same is true even if there is a virus scanner.
346
/// * The same is true even on a remote filesystem.
347
/// * If someone with read access to the file - eg the human user - opens it for reading
348
///   without FILE_SHARE options, the algorithm will still work and not fail
349
///   with a file sharing violation io error.
350
///   (Or, every program the user might use to randomly peer at files in arti's
351
///   state directory, including the equivalents of `grep -R` and backup programs,
352
///   will use suitable FILE_SHARE options.)
353
///   (If this assumption is false, the consequence is not data loss;
354
///   rather, arti would fall over.  So that would be tolerable if we don't
355
///   know how to do better, or if doing better is hard.)
356
#[cfg(windows)]
357
mod os {
358
    use std::{fs::File, mem::MaybeUninit, os::windows::io::AsRawHandle, path::Path};
359
    use windows_sys::Win32::{
360
        Foundation::HANDLE,
361
        Storage::FileSystem::{FILE_ID_INFO, FileIdInfo, GetFileInformationByHandleEx},
362
    };
363

            
364
    /// Use `GetFileInformationByHandleEx` to return a FILE_ID_INFO data for `f`.
365
    ///
366
    /// `GetFileInformationByHandleEx` is supported in Vista and later, so it
367
    /// should be fine here.  Unlike GetFileInformationByHandle, it gives
368
    /// 128-bit identifiers which are supposedly even more unique.
369
    fn get_id_info(f: &File) -> std::io::Result<FILE_ID_INFO> {
370
        let handle = f.as_raw_handle() as HANDLE;
371
        let mut info: MaybeUninit<FILE_ID_INFO> = MaybeUninit::uninit();
372
        let buffersize: u32 = std::mem::size_of::<FILE_ID_INFO>()
373
            .try_into()
374
            .expect("sizeof(FILE_ID_INFO) is ridiculously large");
375

            
376
        let info = unsafe {
377
            // SAFETY: Since `size` is the size of info, this will not write to
378
            // uninitialized memory.
379
            let rv = GetFileInformationByHandleEx(
380
                handle,
381
                FileIdInfo,
382
                info.as_mut_ptr() as _,
383
                buffersize,
384
            );
385

            
386
            if rv == 0 {
387
                return Err(std::io::Error::last_os_error());
388
            }
389

            
390
            // SAFETY: since rv was nonzero, this value is initialized.
391
            info.assume_init()
392
        };
393
        Ok(info)
394
    }
395

            
396
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
397
    pub(crate) fn lockfile_has_path(lf: &File, path: &Path) -> std::io::Result<bool> {
398
        let f2 = File::open(path)?;
399

            
400
        // Note: we would like to just use the MetadataExt methods for index and
401
        // volume serial number, but they are currently available only on
402
        // nightly: https://github.com/rust-lang/rust/issues/63010
403
        //
404
        // If they stabilize at our MSRV, _and_ the file ID is expanded to the
405
        // 128-bit version, we can use them here instead.
406

            
407
        let i1 = get_id_info(lf)?;
408
        let i2 = get_id_info(&f2)?;
409

            
410
        // This comparison is about the best we can do on Windows,
411
        // though there are caveats.
412
        //
413
        // See Raymond Chen's writeup at
414
        //   https://devblogs.microsoft.com/oldnewthing/20220128-00/?p=106201
415
        // and also see BurntSushi's caveats at
416
        //   https://github.com/BurntSushi/same-file/blob/master/src/win.rs
417
        Ok(i1.VolumeSerialNumber == i2.VolumeSerialNumber
418
            && i1.FileId.Identifier == i2.FileId.Identifier)
419
    }
420
}
421

            
422
/// Non-windows, non-unix implementation for lockfile_has_path.
423
///
424
/// For now, this implementation always reports an error.
425
/// It exists so that we can build (but not run) on wasm.
426
#[cfg(all(not(windows), not(unix)))]
427
mod os {
428
    use std::path::Path;
429

            
430
    /// Return true if `lf` currently exists with the given `path`, and false otherwise.
431
    pub(crate) fn lockfile_has_path(_lf: &std::fs::File, _path: &Path) -> std::io::Result<bool> {
432
        Err(std::io::Error::other(
433
            "fslock-guard does not support this operating system".to_string(),
434
        ))
435
    }
436
}
437

            
438
#[cfg(test)]
439
mod tests {
440
    // @@ begin test lint list maintained by maint/add_warning @@
441
    #![allow(clippy::bool_assert_comparison)]
442
    #![allow(clippy::clone_on_copy)]
443
    #![allow(clippy::dbg_macro)]
444
    #![allow(clippy::mixed_attributes_style)]
445
    #![allow(clippy::print_stderr)]
446
    #![allow(clippy::print_stdout)]
447
    #![allow(clippy::single_char_pattern)]
448
    #![allow(clippy::unwrap_used)]
449
    #![allow(clippy::unchecked_time_subtraction)]
450
    #![allow(clippy::useless_vec)]
451
    #![allow(clippy::needless_pass_by_value)]
452
    #![allow(clippy::string_slice)] // See arti#2571
453
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
454

            
455
    use crate::LockFileGuard;
456
    use test_temp_dir::test_temp_dir;
457

            
458
    #[test]
459
    fn keep_lock_file_after_drop() {
460
        test_temp_dir!().used_by(|dir| {
461
            let file = dir.join("file");
462
            let flock_guard = LockFileGuard::lock(&file).unwrap();
463
            assert!(file.try_exists().unwrap());
464
            drop(flock_guard);
465
            assert!(file.try_exists().unwrap());
466
        });
467
    }
468

            
469
    #[test]
470
    fn delete_lock_file_if_requested() {
471
        test_temp_dir!().used_by(|dir| {
472
            let file = dir.join("file");
473
            let flock_guard = LockFileGuard::lock(&file).unwrap();
474
            assert!(file.try_exists().unwrap());
475
            assert!(flock_guard.delete_lock_file(&file).is_ok());
476
            assert!(!file.try_exists().unwrap());
477
        });
478
    }
479
}