#
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, PartialEq, Eq)]
    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}