Skip to main content

coven_foundation/files/
file_lock.rs

1//! OS file locks that work on every platform coven runs on.
2//!
3//! std's `File::lock` family returns `Unsupported` on Android: its platform
4//! list names `linux` but not `android`. Unix systems therefore take `flock`
5//! through rustix, the call std makes on Linux and Apple platforms, so the
6//! code Android runs is the code every Unix test runs. Windows keeps std,
7//! which locks with `LockFileEx`.
8
9use std::fs::{File, TryLockError};
10
11/// Whether other holders may share the lock.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum LockMode {
14    /// Coexists with other shared holders and excludes exclusive ones.
15    Shared,
16    /// Excludes every other holder.
17    Exclusive,
18}
19
20/// An OS lock on an open file, released when dropped. Separate opens of one
21/// file exclude each other, within a process and across processes.
22#[derive(Debug)]
23pub struct FileLock {
24    file: File,
25}
26
27impl FileLock {
28    /// Wait for the lock on `file`.
29    pub fn lock(file: File, mode: LockMode) -> std::io::Result<Self> {
30        os::lock(&file, mode)?;
31        Ok(Self { file })
32    }
33
34    /// Lock `file` unless another holder excludes it, which is
35    /// `TryLockError::WouldBlock`.
36    pub fn try_lock(file: File, mode: LockMode) -> Result<Self, TryLockError> {
37        os::try_lock(&file, mode)?;
38        Ok(Self { file })
39    }
40
41    #[cfg(test)]
42    pub(crate) fn inherited_file(&self) -> File {
43        self.file.try_clone().unwrap()
44    }
45}
46
47impl Drop for FileLock {
48    fn drop(&mut self) {
49        // Closing alone leaves the lock held while a concurrently forked child
50        // retains the open file before exec closes its inherited descriptors.
51        os::unlock(&self.file).expect("release a held file lock");
52    }
53}
54
55#[cfg(unix)]
56mod os {
57    use super::LockMode;
58    use rustix::fs::{flock, FlockOperation};
59    use std::fs::{File, TryLockError};
60
61    pub(super) fn lock(file: &File, mode: LockMode) -> std::io::Result<()> {
62        let operation = match mode {
63            LockMode::Shared => FlockOperation::LockShared,
64            LockMode::Exclusive => FlockOperation::LockExclusive,
65        };
66        Ok(flock(file, operation)?)
67    }
68
69    pub(super) fn try_lock(file: &File, mode: LockMode) -> Result<(), TryLockError> {
70        let operation = match mode {
71            LockMode::Shared => FlockOperation::NonBlockingLockShared,
72            LockMode::Exclusive => FlockOperation::NonBlockingLockExclusive,
73        };
74        flock(file, operation).map_err(|errno| {
75            if errno == rustix::io::Errno::WOULDBLOCK {
76                TryLockError::WouldBlock
77            } else {
78                TryLockError::Error(errno.into())
79            }
80        })
81    }
82
83    pub(super) fn unlock(file: &File) -> std::io::Result<()> {
84        Ok(flock(file, FlockOperation::Unlock)?)
85    }
86}
87
88#[cfg(windows)]
89#[expect(
90    clippy::disallowed_methods,
91    reason = "std locks Windows files with LockFileEx; only its Android stub is refused"
92)]
93mod os {
94    use super::LockMode;
95    use std::fs::{File, TryLockError};
96
97    pub(super) fn lock(file: &File, mode: LockMode) -> std::io::Result<()> {
98        match mode {
99            LockMode::Shared => file.lock_shared(),
100            LockMode::Exclusive => file.lock(),
101        }
102    }
103
104    pub(super) fn try_lock(file: &File, mode: LockMode) -> Result<(), TryLockError> {
105        match mode {
106            LockMode::Shared => file.try_lock_shared(),
107            LockMode::Exclusive => file.try_lock(),
108        }
109    }
110
111    pub(super) fn unlock(file: &File) -> std::io::Result<()> {
112        file.unlock()
113    }
114}
115
116#[cfg(test)]
117#[path = "file_lock_tests.rs"]
118mod tests;