warg_client/
lock.rs

1//! Utility for concurrent process file locking.
2
3use self::sys::*;
4use anyhow::{anyhow, Context, Result};
5use std::fs::{File, OpenOptions};
6use std::io;
7use std::io::{Read, Seek, SeekFrom, Write};
8use std::path::{Path, PathBuf};
9
10/// A file system lock.
11///
12/// This implementation is modeled after the `FileLock` type in the `cargo`.
13#[derive(Debug)]
14pub struct FileLock {
15    file: File,
16    path: PathBuf,
17}
18
19#[derive(Debug, Copy, Clone, Eq, PartialEq)]
20enum Access {
21    Shared,
22    Exclusive,
23}
24
25impl FileLock {
26    /// Attempts to acquire exclusive access to a file, returning the locked
27    /// version of a file.
28    ///
29    /// This function will create a file at `path` if it doesn't already exist
30    /// (including intermediate directories), and then it will try to acquire an
31    /// exclusive lock on `path`.
32    ///
33    /// If the lock cannot be immediately acquired, `Ok(None)` is returned.
34    ///
35    /// The returned file can be accessed to look at the path and also has
36    /// read/write access to the underlying file.
37    pub fn try_open_rw(path: impl Into<PathBuf>) -> Result<Option<Self>> {
38        Self::open(
39            path.into(),
40            OpenOptions::new().read(true).write(true).create(true),
41            Access::Exclusive,
42            true,
43        )
44    }
45
46    /// Opens exclusive access to a file, returning the locked version of a
47    /// file.
48    ///
49    /// This function will create a file at `path` if it doesn't already exist
50    /// (including intermediate directories), and then it will acquire an
51    /// exclusive lock on `path`.
52    ///
53    /// If the lock cannot be acquired, this function will block until it is
54    /// acquired.
55    ///
56    /// The returned file can be accessed to look at the path and also has
57    /// read/write access to the underlying file.
58    pub fn open_rw(path: impl Into<PathBuf>) -> Result<Self> {
59        Ok(Self::open(
60            path.into(),
61            OpenOptions::new().read(true).write(true).create(true),
62            Access::Exclusive,
63            false,
64        )?
65        .unwrap())
66    }
67
68    /// Attempts to acquire shared access to a file, returning the locked version
69    /// of a file.
70    ///
71    /// This function will fail if `path` doesn't already exist, but if it does
72    /// then it will acquire a shared lock on `path`.
73    ///
74    /// If the lock cannot be immediately acquired, `Ok(None)` is returned.
75    ///
76    /// The returned file can be accessed to look at the path and also has read
77    /// access to the underlying file. Any writes to the file will return an
78    /// error.
79    pub fn try_open_ro(path: impl Into<PathBuf>) -> Result<Option<Self>> {
80        Self::open(
81            path.into(),
82            OpenOptions::new().read(true),
83            Access::Shared,
84            true,
85        )
86    }
87
88    /// Opens shared access to a file, returning the locked version of a file.
89    ///
90    /// This function will fail if `path` doesn't already exist, but if it does
91    /// then it will acquire a shared lock on `path`.
92    ///
93    /// If the lock cannot be acquired, this function will block until it is
94    /// acquired.
95    ///
96    /// The returned file can be accessed to look at the path and also has read
97    /// access to the underlying file. Any writes to the file will return an
98    /// error.
99    pub fn open_ro(path: impl Into<PathBuf>) -> Result<Self> {
100        Ok(Self::open(
101            path.into(),
102            OpenOptions::new().read(true),
103            Access::Shared,
104            false,
105        )?
106        .unwrap())
107    }
108
109    fn open(
110        path: PathBuf,
111        opts: &OpenOptions,
112        access: Access,
113        try_lock: bool,
114    ) -> Result<Option<Self>> {
115        // If we want an exclusive lock then if we fail because of NotFound it's
116        // likely because an intermediate directory didn't exist, so try to
117        // create the directory and then continue.
118        let file = opts
119            .open(&path)
120            .or_else(|e| {
121                if e.kind() == io::ErrorKind::NotFound && access == Access::Exclusive {
122                    std::fs::create_dir_all(path.parent().unwrap())?;
123                    Ok(opts.open(&path)?)
124                } else {
125                    Err(anyhow::Error::from(e))
126                }
127            })
128            .with_context(|| format!("failed to open `{path}`", path = path.display()))?;
129
130        let lock = Self { file, path };
131
132        // File locking on Unix is currently implemented via `flock`, which is known
133        // to be broken on NFS. We could in theory just ignore errors that happen on
134        // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
135        // forever**, even if the "non-blocking" flag is passed!
136        //
137        // As a result, we just skip all file locks entirely on NFS mounts. That
138        // should avoid calling any `flock` functions at all, and it wouldn't work
139        // there anyway.
140        //
141        // [1]: https://github.com/rust-lang/cargo/issues/2615
142        if is_on_nfs_mount(&lock.path) {
143            return Ok(Some(lock));
144        }
145
146        let res = match (access, try_lock) {
147            (Access::Shared, true) => try_lock_shared(&lock.file),
148            (Access::Exclusive, true) => try_lock_exclusive(&lock.file),
149            (Access::Shared, false) => lock_shared(&lock.file),
150            (Access::Exclusive, false) => lock_exclusive(&lock.file),
151        };
152
153        return match res {
154            Ok(_) => Ok(Some(lock)),
155
156            // In addition to ignoring NFS which is commonly not working we also
157            // just ignore locking on file systems that look like they don't
158            // implement file locking.
159            Err(e) if error_unsupported(&e) => Ok(Some(lock)),
160
161            // Check to see if it was a contention error
162            Err(e) if try_lock && error_contended(&e) => Ok(None),
163
164            Err(e) => Err(anyhow!(e).context(format!(
165                "failed to lock file `{path}`",
166                path = lock.path.display()
167            ))),
168        };
169
170        #[cfg(all(target_os = "linux", not(target_env = "musl")))]
171        fn is_on_nfs_mount(path: &Path) -> bool {
172            use std::ffi::CString;
173            use std::mem;
174            use std::os::unix::prelude::*;
175
176            let path = match CString::new(path.as_os_str().as_bytes()) {
177                Ok(path) => path,
178                Err(_) => return false,
179            };
180
181            unsafe {
182                let mut buf: libc::statfs = mem::zeroed();
183                let r = libc::statfs(path.as_ptr(), &mut buf);
184
185                r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
186            }
187        }
188
189        #[cfg(any(not(target_os = "linux"), target_env = "musl"))]
190        fn is_on_nfs_mount(_path: &Path) -> bool {
191            false
192        }
193    }
194
195    /// Returns the underlying file handle of this lock.
196    pub fn file(&self) -> &File {
197        &self.file
198    }
199
200    /// Returns the underlying path that this lock points to.
201    ///
202    /// Note that special care must be taken to ensure that the path is not
203    /// referenced outside the lifetime of this lock.
204    pub fn path(&self) -> &Path {
205        &self.path
206    }
207
208    /// Returns the parent path containing this file.
209    pub fn parent(&self) -> &Path {
210        self.path.parent().unwrap()
211    }
212}
213
214impl Read for FileLock {
215    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
216        self.file().read(buf)
217    }
218}
219
220impl Seek for FileLock {
221    fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
222        self.file().seek(to)
223    }
224}
225
226impl Write for FileLock {
227    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
228        self.file().write(buf)
229    }
230
231    fn flush(&mut self) -> io::Result<()> {
232        self.file().flush()
233    }
234}
235
236impl Drop for FileLock {
237    fn drop(&mut self) {
238        let _ = unlock(&self.file);
239    }
240}
241
242#[cfg(unix)]
243mod sys {
244    use std::fs::File;
245    use std::io::{Error, Result};
246    use std::os::unix::io::AsRawFd;
247
248    pub(super) fn lock_shared(file: &File) -> Result<()> {
249        flock(file, libc::LOCK_SH)
250    }
251
252    pub(super) fn lock_exclusive(file: &File) -> Result<()> {
253        flock(file, libc::LOCK_EX)
254    }
255
256    pub(super) fn try_lock_shared(file: &File) -> Result<()> {
257        flock(file, libc::LOCK_SH | libc::LOCK_NB)
258    }
259
260    pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
261        flock(file, libc::LOCK_EX | libc::LOCK_NB)
262    }
263
264    pub(super) fn unlock(file: &File) -> Result<()> {
265        flock(file, libc::LOCK_UN)
266    }
267
268    pub(super) fn error_contended(err: &Error) -> bool {
269        err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
270    }
271
272    pub(super) fn error_unsupported(err: &Error) -> bool {
273        match err.raw_os_error() {
274            // Unfortunately, depending on the target, these may or may not be the same.
275            // For targets in which they are the same, the duplicate pattern causes a warning.
276            #[allow(unreachable_patterns)]
277            Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
278            Some(libc::ENOSYS) => true,
279            _ => false,
280        }
281    }
282
283    #[cfg(not(target_os = "solaris"))]
284    fn flock(file: &File, flag: libc::c_int) -> Result<()> {
285        let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
286        if ret < 0 {
287            Err(Error::last_os_error())
288        } else {
289            Ok(())
290        }
291    }
292
293    #[cfg(target_os = "solaris")]
294    fn flock(file: &File, flag: libc::c_int) -> Result<()> {
295        // Solaris lacks flock(), so try to emulate using fcntl()
296        let mut flock = libc::flock {
297            l_type: 0,
298            l_whence: 0,
299            l_start: 0,
300            l_len: 0,
301            l_sysid: 0,
302            l_pid: 0,
303            l_pad: [0, 0, 0, 0],
304        };
305        flock.l_type = if flag & libc::LOCK_UN != 0 {
306            libc::F_UNLCK
307        } else if flag & libc::LOCK_EX != 0 {
308            libc::F_WRLCK
309        } else if flag & libc::LOCK_SH != 0 {
310            libc::F_RDLCK
311        } else {
312            panic!("unexpected flock() operation")
313        };
314
315        let mut cmd = libc::F_SETLKW;
316        if (flag & libc::LOCK_NB) != 0 {
317            cmd = libc::F_SETLK;
318        }
319
320        let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
321
322        if ret < 0 {
323            Err(Error::last_os_error())
324        } else {
325            Ok(())
326        }
327    }
328}
329
330#[cfg(windows)]
331mod sys {
332    use std::fs::File;
333    use std::io::{Error, Result};
334    use std::mem;
335    use std::os::windows::io::AsRawHandle;
336
337    use windows_sys::Win32::Foundation::HANDLE;
338    use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
339    use windows_sys::Win32::Storage::FileSystem::{
340        LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
341    };
342
343    pub(super) fn lock_shared(file: &File) -> Result<()> {
344        lock_file(file, 0)
345    }
346
347    pub(super) fn lock_exclusive(file: &File) -> Result<()> {
348        lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
349    }
350
351    pub(super) fn try_lock_shared(file: &File) -> Result<()> {
352        lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
353    }
354
355    pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
356        lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
357    }
358
359    pub(super) fn error_contended(err: &Error) -> bool {
360        err.raw_os_error()
361            .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
362    }
363
364    pub(super) fn error_unsupported(err: &Error) -> bool {
365        err.raw_os_error()
366            .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
367    }
368
369    pub(super) fn unlock(file: &File) -> Result<()> {
370        unsafe {
371            let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
372            if ret == 0 {
373                Err(Error::last_os_error())
374            } else {
375                Ok(())
376            }
377        }
378    }
379
380    fn lock_file(file: &File, flags: u32) -> Result<()> {
381        unsafe {
382            let mut overlapped = mem::zeroed();
383            let ret = LockFileEx(
384                file.as_raw_handle() as HANDLE,
385                flags,
386                0,
387                !0,
388                !0,
389                &mut overlapped,
390            );
391            if ret == 0 {
392                Err(Error::last_os_error())
393            } else {
394                Ok(())
395            }
396        }
397    }
398}