#
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::TryLockError;
    11use std::fs::{File, OpenOptions};
    12use std::io;
    13use std::io::{Read, Seek, SeekFrom, Write};
    14use std::path::{Display, Path, PathBuf};
    15
    16use crate::util::GlobalContext;
    17use crate::util::errors::CargoResult;
    18use crate::util::style;
    19use anyhow::Context as _;
    20use cargo_util::paths;
    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    /// Renames the file and updates the internal path.
    81    ///
    82    /// This method performs a filesystem rename operation using [`std::fs::rename`]
    83    /// while keeping the FileLock's internal path synchronized with the actual
    84    /// file location.
    85    ///
    86    /// ## Difference from `std::fs::rename`
    87    ///
    88    /// - `std::fs::rename(old, new)` only moves the file on the filesystem
    89    /// - `FileLock::rename(new)` moves the file AND updates `self.path` to point to the new location
    90    pub fn rename<P: AsRef<Path>>(&mut self, new_path: P) -> CargoResult<()> {
    91        let new_path = new_path.as_ref();
    92        std::fs::rename(&self.path, new_path).with_context(|| {
    93            format!(
    94                "failed to rename {} to {}",
    95                self.path.display(),
    96                new_path.display()
    97            )
    98        })?;
    99        self.path = new_path.to_path_buf();
    100        Ok(())
    101    }
    102}
    103
    104impl Read for FileLock {
    105    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
    106        self.file().read(buf)
    107    }
    108}
    109
    110impl Seek for FileLock {
    111    fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
    112        self.file().seek(to)
    113    }
    114}
    115
    116impl Write for FileLock {
    117    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
    118        self.file().write(buf)
    119    }
    120
    121    fn flush(&mut self) -> io::Result<()> {
    122        self.file().flush()
    123    }
    124}
    125
    126impl Drop for FileLock {
    127    fn drop(&mut self) {
    128        if let Some(f) = self.f.take() {
    129            if let Err(e) = f.unlock() {
    130                tracing::warn!("failed to release lock: {e:?}");
    131            }
    132        }
    133    }
    134}
    135
    136/// A "filesystem" is intended to be a globally shared, hence locked, resource
    137/// in Cargo.
    138///
    139/// The `Path` of a filesystem cannot be learned unless it's done in a locked
    140/// fashion, and otherwise functions on this structure are prepared to handle
    141/// concurrent invocations across multiple instances of Cargo.
    142///
    143/// The methods on `Filesystem` that open files return a [`FileLock`] which
    144/// holds the lock, and that type provides methods for accessing the
    145/// underlying file.
    146///
    147/// If the blocking methods (like [`Filesystem::open_ro_shared`]) detect that
    148/// they will block, then they will display a message to the user letting them
    149/// know it is blocked. There are non-blocking variants starting with the
    150/// `try_` prefix like [`Filesystem::try_open_ro_shared_create`].
    151///
    152/// The behavior of locks acquired by the `Filesystem` depend on the operating
    153/// system. On unix-like system, they are advisory using [`flock`], and thus
    154/// not enforced against processes which do not try to acquire the lock. On
    155/// Windows, they are mandatory using [`LockFileEx`], enforced against all
    156/// processes.
    157///
    158/// This **does not** guarantee that a lock is acquired. In some cases, for
    159/// example on filesystems that don't support locking, it will return a
    160/// [`FileLock`] even though the filesystem lock was not acquired. This is
    161/// intended to provide a graceful fallback instead of refusing to work.
    162/// Usually there aren't multiple processes accessing the same resource. In
    163/// that case, it is the user's responsibility to not run concurrent
    164/// processes.
    165///
    166/// [`flock`]: https://linux.die.net/man/2/flock
    167/// [`LockFileEx`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
    168#[derive(Clone, Debug, PartialEq, Eq)]
    169pub struct Filesystem {
    170    root: PathBuf,
    171}
    172
    173impl Filesystem {
    174    /// Creates a new filesystem to be rooted at the given path.
    175    pub fn new(path: PathBuf) -> Filesystem {
    176        Filesystem { root: path }
    177    }
    178
    179    /// Like `Path::join`, creates a new filesystem rooted at this filesystem
    180    /// joined with the given path.
    181    pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
    182        Filesystem::new(self.root.join(other))
    183    }
    184
    185    /// Like `Path::push`, pushes a new path component onto this filesystem.
    186    pub fn push<T: AsRef<Path>>(&mut self, other: T) {
    187        self.root.push(other);
    188    }
    189
    190    /// Consumes this filesystem and returns the underlying `PathBuf`.
    191    ///
    192    /// Note that this is a relatively dangerous operation and should be used
    193    /// with great caution!.
    194    pub fn into_path_unlocked(self) -> PathBuf {
    195        self.root
    196    }
    197
    198    /// Returns the underlying `Path`.
    199    ///
    200    /// Note that this is a relatively dangerous operation and should be used
    201    /// with great caution!.
    202    pub fn as_path_unlocked(&self) -> &Path {
    203        &self.root
    204    }
    205
    206    /// Creates the directory pointed to by this filesystem.
    207    ///
    208    /// Handles errors where other Cargo processes are also attempting to
    209    /// concurrently create this directory.
    210    pub fn create_dir(&self) -> CargoResult<()> {
    211        paths::create_dir_all(&self.root)
    212    }
    213
    214    /// Returns an adaptor that can be used to print the path of this
    215    /// filesystem.
    216    pub fn display(&self) -> Display<'_> {
    217        self.root.display()
    218    }
    219
    220    /// Opens read-write exclusive access to a file, returning the locked
    221    /// version of a file.
    222    ///
    223    /// This function will create a file at `path` if it doesn't already exist
    224    /// (including intermediate directories), and then it will acquire an
    225    /// exclusive lock on `path`. If the process must block waiting for the
    226    /// lock, the `msg` is printed to [`GlobalContext`].
    227    ///
    228    /// The returned file can be accessed to look at the path and also has
    229    /// read/write access to the underlying file.
    230    pub fn open_rw_exclusive_create<P>(
    231        &self,
    232        path: P,
    233        gctx: &GlobalContext,
    234        msg: &str,
    235    ) -> CargoResult<FileLock>
    236    where
    237        P: AsRef<Path>,
    238    {
    239        let mut opts = OpenOptions::new();
    240        opts.read(true).write(true).create(true);
    241        let (path, f) = self.open(path.as_ref(), &opts, true)?;
    242        acquire(gctx, msg, &path, &|| f.try_lock(), &|| f.lock())?;
    243        Ok(FileLock { f: Some(f), path })
    244    }
    245
    246    /// A non-blocking version of [`Filesystem::open_rw_exclusive_create`].
    247    ///
    248    /// Returns `None` if the operation would block due to another process
    249    /// holding the lock.
    250    pub fn try_open_rw_exclusive_create<P: AsRef<Path>>(
    251        &self,
    252        path: P,
    253    ) -> CargoResult<Option<FileLock>> {
    254        let mut opts = OpenOptions::new();
    255        opts.read(true).write(true).create(true);
    256        let (path, f) = self.open(path.as_ref(), &opts, true)?;
    257        if try_acquire(&path, &|| f.try_lock())? {
    258            Ok(Some(FileLock { f: Some(f), path }))
    259        } else {
    260            Ok(None)
    261        }
    262    }
    263
    264    /// Opens read-only shared access to a file, returning the locked version of a file.
    265    ///
    266    /// This function will fail if `path` doesn't already exist, but if it does
    267    /// then it will acquire a shared lock on `path`. If the process must block
    268    /// waiting for the lock, the `msg` is printed to [`GlobalContext`].
    269    ///
    270    /// The returned file can be accessed to look at the path and also has read
    271    /// access to the underlying file. Any writes to the file will return an
    272    /// error.
    273    pub fn open_ro_shared<P>(
    274        &self,
    275        path: P,
    276        gctx: &GlobalContext,
    277        msg: &str,
    278    ) -> CargoResult<FileLock>
    279    where
    280        P: AsRef<Path>,
    281    {
    282        let (path, f) = self.open(path.as_ref(), &OpenOptions::new().read(true), false)?;
    283        acquire(gctx, msg, &path, &|| f.try_lock_shared(), &|| {
    284            f.lock_shared()
    285        })?;
    286        Ok(FileLock { f: Some(f), path })
    287    }
    288
    289    /// Opens read-only shared access to a file, returning the locked version of a file.
    290    ///
    291    /// Compared to [`Filesystem::open_ro_shared`], this will create the file
    292    /// (and any directories in the parent) if the file does not already
    293    /// exist.
    294    pub fn open_ro_shared_create<P: AsRef<Path>>(
    295        &self,
    296        path: P,
    297        gctx: &GlobalContext,
    298        msg: &str,
    299    ) -> CargoResult<FileLock> {
    300        let mut opts = OpenOptions::new();
    301        opts.read(true).write(true).create(true);
    302        let (path, f) = self.open(path.as_ref(), &opts, true)?;
    303        acquire(gctx, msg, &path, &|| f.try_lock_shared(), &|| {
    304            f.lock_shared()
    305        })?;
    306        Ok(FileLock { f: Some(f), path })
    307    }
    308
    309    /// A non-blocking version of [`Filesystem::open_ro_shared_create`].
    310    ///
    311    /// Returns `None` if the operation would block due to another process
    312    /// holding the lock.
    313    pub fn try_open_ro_shared_create<P: AsRef<Path>>(
    314        &self,
    315        path: P,
    316    ) -> CargoResult<Option<FileLock>> {
    317        let mut opts = OpenOptions::new();
    318        opts.read(true).write(true).create(true);
    319        let (path, f) = self.open(path.as_ref(), &opts, true)?;
    320        if try_acquire(&path, &|| f.try_lock_shared())? {
    321            Ok(Some(FileLock { f: Some(f), path }))
    322        } else {
    323            Ok(None)
    324        }
    325    }
    326
    327    fn open(&self, path: &Path, opts: &OpenOptions, create: bool) -> CargoResult<(PathBuf, File)> {
    328        let path = self.root.join(path);
    329        let f = opts
    330            .open(&path)
    331            .or_else(|e| {
    332                // If we were requested to create this file, and there was a
    333                // NotFound error, then that was likely due to missing
    334                // intermediate directories. Try creating them and try again.
    335                if e.kind() == io::ErrorKind::NotFound && create {
    336                    paths::create_dir_all(path.parent().unwrap())?;
    337                    Ok(opts.open(&path)?)
    338                } else {
    339                    Err(anyhow::Error::from(e))
    340                }
    341            })
    342            .with_context(|| format!("failed to open: {}", path.display()))?;
    343        Ok((path, f))
    344    }
    345}
    346
    347impl PartialEq<Path> for Filesystem {
    348    fn eq(&self, other: &Path) -> bool {
    349        self.root == other
    350    }
    351}
    352
    353impl PartialEq<Filesystem> for Path {
    354    fn eq(&self, other: &Filesystem) -> bool {
    355        self == other.root
    356    }
    357}
    358
    359fn try_acquire(path: &Path, lock_try: &dyn Fn() -> Result<(), TryLockError>) -> CargoResult<bool> {
    360    // File locking on Unix is currently implemented via `flock`, which is known
    361    // to be broken on NFS. We could in theory just ignore errors that happen on
    362    // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
    363    // forever**, even if the "non-blocking" flag is passed!
    364    //
    365    // As a result, we just skip all file locks entirely on NFS mounts. That
    366    // should avoid calling any `flock` functions at all, and it wouldn't work
    367    // there anyway.
    368    //
    369    // [1]: https://github.com/rust-lang/cargo/issues/2615
    370    if is_on_nfs_mount(path) {
    371        tracing::debug!("{path:?} appears to be an NFS mount, not trying to lock");
    372        return Ok(true);
    373    }
    374
    375    match lock_try() {
    376        Ok(()) => Ok(true),
    377
    378        // In addition to ignoring NFS which is commonly not working we also
    379        // just ignore locking on filesystems that look like they don't
    380        // implement file locking.
    381        Err(TryLockError::Error(e)) if error_unsupported(&e) => Ok(true),
    382
    383        Err(TryLockError::Error(e)) => {
    384            let e = anyhow::Error::from(e);
    385            let cx = format!("failed to lock file: {}", path.display());
    386            Err(e.context(cx))
    387        }
    388
    389        Err(TryLockError::WouldBlock) => Ok(false),
    390    }
    391}
    392
    393/// Acquires a lock on a file in a "nice" manner.
    394///
    395/// Almost all long-running blocking actions in Cargo have a status message
    396/// associated with them as we're not sure how long they'll take. Whenever a
    397/// conflicted file lock happens, this is the case (we're not sure when the lock
    398/// will be released).
    399///
    400/// This function will acquire the lock on a `path`, printing out a nice message
    401/// to the console if we have to wait for it. It will first attempt to use `try`
    402/// to acquire a lock on the crate, and in the case of contention it will emit a
    403/// status message based on `msg` to [`GlobalContext`]'s shell, and then use `block` to
    404/// block waiting to acquire a lock.
    405///
    406/// Returns an error if the lock could not be acquired or if any error other
    407/// than a contention error happens.
    408fn acquire(
    409    gctx: &GlobalContext,
    410    msg: &str,
    411    path: &Path,
    412    lock_try: &dyn Fn() -> Result<(), TryLockError>,
    413    lock_block: &dyn Fn() -> io::Result<()>,
    414) -> CargoResult<()> {
    415    // Ensure `shell` is not already in use,
    416    // regardless of whether we hit contention or not
    417    gctx.debug_assert_shell_not_borrowed();
    418    if try_acquire(path, lock_try)? {
    419        return Ok(());
    420    }
    421    let msg = format!("waiting for file lock on {}", msg);
    422    gctx.shell()
    423        .status_with_color("Blocking", &msg, &style::NOTE)?;
    424
    425    lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
    426    Ok(())
    427}
    428
    429#[cfg(all(target_os = "linux", not(target_env = "musl")))]
    430fn is_on_nfs_mount(path: &Path) -> bool {
    431    use std::ffi::CString;
    432    use std::mem;
    433    use std::os::unix::prelude::*;
    434
    435    let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
    436        return false;
    437    };
    438
    439    unsafe {
    440        let mut buf: libc::statfs = mem::zeroed();
    441        let r = libc::statfs(path.as_ptr(), &mut buf);
    442
    443        r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
    444    }
    445}
    446
    447#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
    448fn is_on_nfs_mount(_path: &Path) -> bool {
    449    false
    450}
    451
    452#[cfg(unix)]
    453fn error_unsupported(err: &std::io::Error) -> bool {
    454    match err.raw_os_error() {
    455        // Unfortunately, depending on the target, these may or may not be the same.
    456        // For targets in which they are the same, the duplicate pattern causes a warning.
    457        #[allow(unreachable_patterns)]
    458        Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
    459        Some(libc::ENOSYS) => true,
    460        _ => err.kind() == std::io::ErrorKind::Unsupported,
    461    }
    462}
    463
    464#[cfg(windows)]
    465fn error_unsupported(err: &std::io::Error) -> bool {
    466    use windows_sys::Win32::Foundation::ERROR_INVALID_FUNCTION;
    467    match err.raw_os_error() {
    468        Some(code) if code == ERROR_INVALID_FUNCTION as i32 => true,
    469        _ => err.kind() == std::io::ErrorKind::Unsupported,
    470    }
    471}