cargo/util/
flock.rs

1//! File-locking support.
2//!
3//! This module defines the [`Filesystem`] type which is an abstraction over a
4//! filesystem, ensuring that access to the filesystem is only done through
5//! coordinated locks.
6//!
7//! The [`FileLock`] type represents a locked file, and provides access to the
8//! file.
9
10use std::fs::{File, OpenOptions};
11use std::io;
12use std::io::{Read, Seek, SeekFrom, Write};
13use std::path::{Display, Path, PathBuf};
14
15use crate::util::errors::CargoResult;
16use crate::util::style;
17use crate::util::GlobalContext;
18use anyhow::Context as _;
19use cargo_util::paths;
20use sys::*;
21
22/// A locked file.
23///
24/// This provides access to file while holding a lock on the file. This type
25/// implements the [`Read`], [`Write`], and [`Seek`] traits to provide access
26/// to the underlying file.
27///
28/// Locks are either shared (multiple processes can access the file) or
29/// exclusive (only one process can access the file).
30///
31/// This type is created via methods on the [`Filesystem`] type.
32///
33/// When this value is dropped, the lock will be released.
34#[derive(Debug)]
35pub struct FileLock {
36    f: Option<File>,
37    path: PathBuf,
38}
39
40impl FileLock {
41    /// Returns the underlying file handle of this lock.
42    pub fn file(&self) -> &File {
43        self.f.as_ref().unwrap()
44    }
45
46    /// Returns the underlying path that this lock points to.
47    ///
48    /// Note that special care must be taken to ensure that the path is not
49    /// referenced outside the lifetime of this lock.
50    pub fn path(&self) -> &Path {
51        &self.path
52    }
53
54    /// Returns the parent path containing this file
55    pub fn parent(&self) -> &Path {
56        self.path.parent().unwrap()
57    }
58
59    /// Removes all sibling files to this locked file.
60    ///
61    /// This can be useful if a directory is locked with a sentinel file but it
62    /// needs to be cleared out as it may be corrupt.
63    pub fn remove_siblings(&self) -> CargoResult<()> {
64        let path = self.path();
65        for entry in path.parent().unwrap().read_dir()? {
66            let entry = entry?;
67            if Some(&entry.file_name()[..]) == path.file_name() {
68                continue;
69            }
70            let kind = entry.file_type()?;
71            if kind.is_dir() {
72                paths::remove_dir_all(entry.path())?;
73            } else {
74                paths::remove_file(entry.path())?;
75            }
76        }
77        Ok(())
78    }
79}
80
81impl Read for FileLock {
82    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
83        self.file().read(buf)
84    }
85}
86
87impl Seek for FileLock {
88    fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
89        self.file().seek(to)
90    }
91}
92
93impl Write for FileLock {
94    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
95        self.file().write(buf)
96    }
97
98    fn flush(&mut self) -> io::Result<()> {
99        self.file().flush()
100    }
101}
102
103impl Drop for FileLock {
104    fn drop(&mut self) {
105        if let Some(f) = self.f.take() {
106            if let Err(e) = unlock(&f) {
107                tracing::warn!("failed to release lock: {e:?}");
108            }
109        }
110    }
111}
112
113/// A "filesystem" is intended to be a globally shared, hence locked, resource
114/// in Cargo.
115///
116/// The `Path` of a filesystem cannot be learned unless it's done in a locked
117/// fashion, and otherwise functions on this structure are prepared to handle
118/// concurrent invocations across multiple instances of Cargo.
119///
120/// The methods on `Filesystem` that open files return a [`FileLock`] which
121/// holds the lock, and that type provides methods for accessing the
122/// underlying file.
123///
124/// If the blocking methods (like [`Filesystem::open_ro_shared`]) detect that
125/// they will block, then they will display a message to the user letting them
126/// know it is blocked. There are non-blocking variants starting with the
127/// `try_` prefix like [`Filesystem::try_open_ro_shared_create`].
128///
129/// The behavior of locks acquired by the `Filesystem` depend on the operating
130/// system. On unix-like system, they are advisory using [`flock`], and thus
131/// not enforced against processes which do not try to acquire the lock. On
132/// Windows, they are mandatory using [`LockFileEx`], enforced against all
133/// processes.
134///
135/// This **does not** guarantee that a lock is acquired. In some cases, for
136/// example on filesystems that don't support locking, it will return a
137/// [`FileLock`] even though the filesystem lock was not acquired. This is
138/// intended to provide a graceful fallback instead of refusing to work.
139/// Usually there aren't multiple processes accessing the same resource. In
140/// that case, it is the user's responsibility to not run concurrent
141/// processes.
142///
143/// [`flock`]: https://linux.die.net/man/2/flock
144/// [`LockFileEx`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
145#[derive(Clone, Debug)]
146pub struct Filesystem {
147    root: PathBuf,
148}
149
150impl Filesystem {
151    /// Creates a new filesystem to be rooted at the given path.
152    pub fn new(path: PathBuf) -> Filesystem {
153        Filesystem { root: path }
154    }
155
156    /// Like `Path::join`, creates a new filesystem rooted at this filesystem
157    /// joined with the given path.
158    pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
159        Filesystem::new(self.root.join(other))
160    }
161
162    /// Like `Path::push`, pushes a new path component onto this filesystem.
163    pub fn push<T: AsRef<Path>>(&mut self, other: T) {
164        self.root.push(other);
165    }
166
167    /// Consumes this filesystem and returns the underlying `PathBuf`.
168    ///
169    /// Note that this is a relatively dangerous operation and should be used
170    /// with great caution!.
171    pub fn into_path_unlocked(self) -> PathBuf {
172        self.root
173    }
174
175    /// Returns the underlying `Path`.
176    ///
177    /// Note that this is a relatively dangerous operation and should be used
178    /// with great caution!.
179    pub fn as_path_unlocked(&self) -> &Path {
180        &self.root
181    }
182
183    /// Creates the directory pointed to by this filesystem.
184    ///
185    /// Handles errors where other Cargo processes are also attempting to
186    /// concurrently create this directory.
187    pub fn create_dir(&self) -> CargoResult<()> {
188        paths::create_dir_all(&self.root)
189    }
190
191    /// Returns an adaptor that can be used to print the path of this
192    /// filesystem.
193    pub fn display(&self) -> Display<'_> {
194        self.root.display()
195    }
196
197    /// Opens read-write exclusive access to a file, returning the locked
198    /// version of a file.
199    ///
200    /// This function will create a file at `path` if it doesn't already exist
201    /// (including intermediate directories), and then it will acquire an
202    /// exclusive lock on `path`. If the process must block waiting for the
203    /// lock, the `msg` is printed to [`GlobalContext`].
204    ///
205    /// The returned file can be accessed to look at the path and also has
206    /// read/write access to the underlying file.
207    pub fn open_rw_exclusive_create<P>(
208        &self,
209        path: P,
210        gctx: &GlobalContext,
211        msg: &str,
212    ) -> CargoResult<FileLock>
213    where
214        P: AsRef<Path>,
215    {
216        let mut opts = OpenOptions::new();
217        opts.read(true).write(true).create(true);
218        let (path, f) = self.open(path.as_ref(), &opts, true)?;
219        acquire(gctx, msg, &path, &|| try_lock_exclusive(&f), &|| {
220            lock_exclusive(&f)
221        })?;
222        Ok(FileLock { f: Some(f), path })
223    }
224
225    /// A non-blocking version of [`Filesystem::open_rw_exclusive_create`].
226    ///
227    /// Returns `None` if the operation would block due to another process
228    /// holding the lock.
229    pub fn try_open_rw_exclusive_create<P: AsRef<Path>>(
230        &self,
231        path: P,
232    ) -> CargoResult<Option<FileLock>> {
233        let mut opts = OpenOptions::new();
234        opts.read(true).write(true).create(true);
235        let (path, f) = self.open(path.as_ref(), &opts, true)?;
236        if try_acquire(&path, &|| try_lock_exclusive(&f))? {
237            Ok(Some(FileLock { f: Some(f), path }))
238        } else {
239            Ok(None)
240        }
241    }
242
243    /// Opens read-only shared access to a file, returning the locked version of a file.
244    ///
245    /// This function will fail if `path` doesn't already exist, but if it does
246    /// then it will acquire a shared lock on `path`. If the process must block
247    /// waiting for the lock, the `msg` is printed to [`GlobalContext`].
248    ///
249    /// The returned file can be accessed to look at the path and also has read
250    /// access to the underlying file. Any writes to the file will return an
251    /// error.
252    pub fn open_ro_shared<P>(
253        &self,
254        path: P,
255        gctx: &GlobalContext,
256        msg: &str,
257    ) -> CargoResult<FileLock>
258    where
259        P: AsRef<Path>,
260    {
261        let (path, f) = self.open(path.as_ref(), &OpenOptions::new().read(true), false)?;
262        acquire(gctx, msg, &path, &|| try_lock_shared(&f), &|| {
263            lock_shared(&f)
264        })?;
265        Ok(FileLock { f: Some(f), path })
266    }
267
268    /// Opens read-only shared access to a file, returning the locked version of a file.
269    ///
270    /// Compared to [`Filesystem::open_ro_shared`], this will create the file
271    /// (and any directories in the parent) if the file does not already
272    /// exist.
273    pub fn open_ro_shared_create<P: AsRef<Path>>(
274        &self,
275        path: P,
276        gctx: &GlobalContext,
277        msg: &str,
278    ) -> CargoResult<FileLock> {
279        let mut opts = OpenOptions::new();
280        opts.read(true).write(true).create(true);
281        let (path, f) = self.open(path.as_ref(), &opts, true)?;
282        acquire(gctx, msg, &path, &|| try_lock_shared(&f), &|| {
283            lock_shared(&f)
284        })?;
285        Ok(FileLock { f: Some(f), path })
286    }
287
288    /// A non-blocking version of [`Filesystem::open_ro_shared_create`].
289    ///
290    /// Returns `None` if the operation would block due to another process
291    /// holding the lock.
292    pub fn try_open_ro_shared_create<P: AsRef<Path>>(
293        &self,
294        path: P,
295    ) -> CargoResult<Option<FileLock>> {
296        let mut opts = OpenOptions::new();
297        opts.read(true).write(true).create(true);
298        let (path, f) = self.open(path.as_ref(), &opts, true)?;
299        if try_acquire(&path, &|| try_lock_shared(&f))? {
300            Ok(Some(FileLock { f: Some(f), path }))
301        } else {
302            Ok(None)
303        }
304    }
305
306    fn open(&self, path: &Path, opts: &OpenOptions, create: bool) -> CargoResult<(PathBuf, File)> {
307        let path = self.root.join(path);
308        let f = opts
309            .open(&path)
310            .or_else(|e| {
311                // If we were requested to create this file, and there was a
312                // NotFound error, then that was likely due to missing
313                // intermediate directories. Try creating them and try again.
314                if e.kind() == io::ErrorKind::NotFound && create {
315                    paths::create_dir_all(path.parent().unwrap())?;
316                    Ok(opts.open(&path)?)
317                } else {
318                    Err(anyhow::Error::from(e))
319                }
320            })
321            .with_context(|| format!("failed to open: {}", path.display()))?;
322        Ok((path, f))
323    }
324}
325
326impl PartialEq<Path> for Filesystem {
327    fn eq(&self, other: &Path) -> bool {
328        self.root == other
329    }
330}
331
332impl PartialEq<Filesystem> for Path {
333    fn eq(&self, other: &Filesystem) -> bool {
334        self == other.root
335    }
336}
337
338fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> CargoResult<bool> {
339    // File locking on Unix is currently implemented via `flock`, which is known
340    // to be broken on NFS. We could in theory just ignore errors that happen on
341    // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
342    // forever**, even if the "non-blocking" flag is passed!
343    //
344    // As a result, we just skip all file locks entirely on NFS mounts. That
345    // should avoid calling any `flock` functions at all, and it wouldn't work
346    // there anyway.
347    //
348    // [1]: https://github.com/rust-lang/cargo/issues/2615
349    if is_on_nfs_mount(path) {
350        tracing::debug!("{path:?} appears to be an NFS mount, not trying to lock");
351        return Ok(true);
352    }
353
354    match lock_try() {
355        Ok(()) => return Ok(true),
356
357        // In addition to ignoring NFS which is commonly not working we also
358        // just ignore locking on filesystems that look like they don't
359        // implement file locking.
360        Err(e) if error_unsupported(&e) => return Ok(true),
361
362        Err(e) => {
363            if !error_contended(&e) {
364                let e = anyhow::Error::from(e);
365                let cx = format!("failed to lock file: {}", path.display());
366                return Err(e.context(cx));
367            }
368        }
369    }
370    Ok(false)
371}
372
373/// Acquires a lock on a file in a "nice" manner.
374///
375/// Almost all long-running blocking actions in Cargo have a status message
376/// associated with them as we're not sure how long they'll take. Whenever a
377/// conflicted file lock happens, this is the case (we're not sure when the lock
378/// will be released).
379///
380/// This function will acquire the lock on a `path`, printing out a nice message
381/// to the console if we have to wait for it. It will first attempt to use `try`
382/// to acquire a lock on the crate, and in the case of contention it will emit a
383/// status message based on `msg` to [`GlobalContext`]'s shell, and then use `block` to
384/// block waiting to acquire a lock.
385///
386/// Returns an error if the lock could not be acquired or if any error other
387/// than a contention error happens.
388fn acquire(
389    gctx: &GlobalContext,
390    msg: &str,
391    path: &Path,
392    lock_try: &dyn Fn() -> io::Result<()>,
393    lock_block: &dyn Fn() -> io::Result<()>,
394) -> CargoResult<()> {
395    if cfg!(debug_assertions) {
396        // Force borrow to catch invalid borrows outside of contention situations
397        gctx.shell().verbosity();
398    }
399    if try_acquire(path, lock_try)? {
400        return Ok(());
401    }
402    let msg = format!("waiting for file lock on {}", msg);
403    gctx.shell()
404        .status_with_color("Blocking", &msg, &style::NOTE)?;
405
406    lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
407    Ok(())
408}
409
410#[cfg(all(target_os = "linux", not(target_env = "musl")))]
411fn is_on_nfs_mount(path: &Path) -> bool {
412    use std::ffi::CString;
413    use std::mem;
414    use std::os::unix::prelude::*;
415
416    let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
417        return false;
418    };
419
420    unsafe {
421        let mut buf: libc::statfs = mem::zeroed();
422        let r = libc::statfs(path.as_ptr(), &mut buf);
423
424        r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
425    }
426}
427
428#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
429fn is_on_nfs_mount(_path: &Path) -> bool {
430    false
431}
432
433#[cfg(unix)]
434mod sys {
435    use std::fs::File;
436    use std::io::{Error, Result};
437    use std::os::unix::io::AsRawFd;
438
439    #[cfg(not(target_os = "solaris"))]
440    const LOCK_SH: i32 = libc::LOCK_SH;
441    #[cfg(target_os = "solaris")]
442    const LOCK_SH: i32 = 1;
443    #[cfg(not(target_os = "solaris"))]
444    const LOCK_EX: i32 = libc::LOCK_EX;
445    #[cfg(target_os = "solaris")]
446    const LOCK_EX: i32 = 2;
447    #[cfg(not(target_os = "solaris"))]
448    const LOCK_NB: i32 = libc::LOCK_NB;
449    #[cfg(target_os = "solaris")]
450    const LOCK_NB: i32 = 4;
451    #[cfg(not(target_os = "solaris"))]
452    const LOCK_UN: i32 = libc::LOCK_UN;
453    #[cfg(target_os = "solaris")]
454    const LOCK_UN: i32 = 8;
455
456    pub(super) fn lock_shared(file: &File) -> Result<()> {
457        flock(file, LOCK_SH)
458    }
459
460    pub(super) fn lock_exclusive(file: &File) -> Result<()> {
461        flock(file, LOCK_EX)
462    }
463
464    pub(super) fn try_lock_shared(file: &File) -> Result<()> {
465        flock(file, LOCK_SH | LOCK_NB)
466    }
467
468    pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
469        flock(file, LOCK_EX | LOCK_NB)
470    }
471
472    pub(super) fn unlock(file: &File) -> Result<()> {
473        flock(file, LOCK_UN)
474    }
475
476    pub(super) fn error_contended(err: &Error) -> bool {
477        err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
478    }
479
480    pub(super) fn error_unsupported(err: &Error) -> bool {
481        match err.raw_os_error() {
482            // Unfortunately, depending on the target, these may or may not be the same.
483            // For targets in which they are the same, the duplicate pattern causes a warning.
484            #[allow(unreachable_patterns)]
485            Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
486            Some(libc::ENOSYS) => true,
487            _ => false,
488        }
489    }
490
491    #[cfg(not(target_os = "solaris"))]
492    fn flock(file: &File, flag: libc::c_int) -> Result<()> {
493        let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
494        if ret < 0 {
495            Err(Error::last_os_error())
496        } else {
497            Ok(())
498        }
499    }
500
501    #[cfg(target_os = "solaris")]
502    fn flock(file: &File, flag: libc::c_int) -> Result<()> {
503        // Solaris lacks flock(), so try to emulate using fcntl()
504        let mut flock = libc::flock {
505            l_type: 0,
506            l_whence: 0,
507            l_start: 0,
508            l_len: 0,
509            l_sysid: 0,
510            l_pid: 0,
511            l_pad: [0, 0, 0, 0],
512        };
513        flock.l_type = if flag & LOCK_UN != 0 {
514            libc::F_UNLCK
515        } else if flag & LOCK_EX != 0 {
516            libc::F_WRLCK
517        } else if flag & LOCK_SH != 0 {
518            libc::F_RDLCK
519        } else {
520            panic!("unexpected flock() operation")
521        };
522
523        let mut cmd = libc::F_SETLKW;
524        if (flag & LOCK_NB) != 0 {
525            cmd = libc::F_SETLK;
526        }
527
528        let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
529
530        if ret < 0 {
531            Err(Error::last_os_error())
532        } else {
533            Ok(())
534        }
535    }
536}
537
538#[cfg(windows)]
539mod sys {
540    use std::fs::File;
541    use std::io::{Error, Result};
542    use std::mem;
543    use std::os::windows::io::AsRawHandle;
544
545    use windows_sys::Win32::Foundation::HANDLE;
546    use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
547    use windows_sys::Win32::Storage::FileSystem::{
548        LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
549    };
550
551    pub(super) fn lock_shared(file: &File) -> Result<()> {
552        lock_file(file, 0)
553    }
554
555    pub(super) fn lock_exclusive(file: &File) -> Result<()> {
556        lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
557    }
558
559    pub(super) fn try_lock_shared(file: &File) -> Result<()> {
560        lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
561    }
562
563    pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
564        lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
565    }
566
567    pub(super) fn error_contended(err: &Error) -> bool {
568        err.raw_os_error()
569            .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
570    }
571
572    pub(super) fn error_unsupported(err: &Error) -> bool {
573        err.raw_os_error()
574            .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
575    }
576
577    pub(super) fn unlock(file: &File) -> Result<()> {
578        unsafe {
579            let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
580            if ret == 0 {
581                Err(Error::last_os_error())
582            } else {
583                Ok(())
584            }
585        }
586    }
587
588    fn lock_file(file: &File, flags: u32) -> Result<()> {
589        unsafe {
590            let mut overlapped = mem::zeroed();
591            let ret = LockFileEx(
592                file.as_raw_handle() as HANDLE,
593                flags,
594                0,
595                !0,
596                !0,
597                &mut overlapped,
598            );
599            if ret == 0 {
600                Err(Error::last_os_error())
601            } else {
602                Ok(())
603            }
604        }
605    }
606}