Skip to main content

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