Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 31 additions & 190 deletions src/cargo/util/flock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! The [`FileLock`] type represents a locked file, and provides access to the
//! file.

use std::fs::TryLockError;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::{Read, Seek, SeekFrom, Write};
Expand All @@ -17,7 +18,6 @@ use crate::util::errors::CargoResult;
use crate::util::style;
use anyhow::Context as _;
use cargo_util::paths;
use sys::*;

/// A locked file.
///
Expand Down Expand Up @@ -103,7 +103,7 @@ impl Write for FileLock {
impl Drop for FileLock {
fn drop(&mut self) {
if let Some(f) = self.f.take() {
if let Err(e) = unlock(&f) {
if let Err(e) = f.unlock() {
tracing::warn!("failed to release lock: {e:?}");
}
}
Expand Down Expand Up @@ -216,9 +216,7 @@ impl Filesystem {
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true);
let (path, f) = self.open(path.as_ref(), &opts, true)?;
acquire(gctx, msg, &path, &|| try_lock_exclusive(&f), &|| {
lock_exclusive(&f)
})?;
acquire(gctx, msg, &path, &|| f.try_lock(), &|| f.lock())?;
Ok(FileLock { f: Some(f), path })
}

Expand All @@ -233,7 +231,7 @@ impl Filesystem {
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true);
let (path, f) = self.open(path.as_ref(), &opts, true)?;
if try_acquire(&path, &|| try_lock_exclusive(&f))? {
if try_acquire(&path, &|| f.try_lock())? {
Ok(Some(FileLock { f: Some(f), path }))
} else {
Ok(None)
Expand All @@ -259,8 +257,8 @@ impl Filesystem {
P: AsRef<Path>,
{
let (path, f) = self.open(path.as_ref(), &OpenOptions::new().read(true), false)?;
acquire(gctx, msg, &path, &|| try_lock_shared(&f), &|| {
lock_shared(&f)
acquire(gctx, msg, &path, &|| f.try_lock_shared(), &|| {
f.lock_shared()
})?;
Ok(FileLock { f: Some(f), path })
}
Expand All @@ -279,8 +277,8 @@ impl Filesystem {
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true);
let (path, f) = self.open(path.as_ref(), &opts, true)?;
acquire(gctx, msg, &path, &|| try_lock_shared(&f), &|| {
lock_shared(&f)
acquire(gctx, msg, &path, &|| f.try_lock_shared(), &|| {
f.lock_shared()
})?;
Ok(FileLock { f: Some(f), path })
}
Expand All @@ -296,7 +294,7 @@ impl Filesystem {
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true);
let (path, f) = self.open(path.as_ref(), &opts, true)?;
if try_acquire(&path, &|| try_lock_shared(&f))? {
if try_acquire(&path, &|| f.try_lock_shared())? {
Ok(Some(FileLock { f: Some(f), path }))
} else {
Ok(None)
Expand Down Expand Up @@ -335,7 +333,7 @@ impl PartialEq<Filesystem> for Path {
}
}

fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> CargoResult<bool> {
fn try_acquire(path: &Path, lock_try: &dyn Fn() -> Result<(), TryLockError>) -> CargoResult<bool> {
// File locking on Unix is currently implemented via `flock`, which is known
// to be broken on NFS. We could in theory just ignore errors that happen on
// NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
Expand All @@ -352,22 +350,21 @@ fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> CargoResul
}

match lock_try() {
Ok(()) => return Ok(true),
Ok(()) => Ok(true),

// In addition to ignoring NFS which is commonly not working we also
// just ignore locking on filesystems that look like they don't
// implement file locking.
Err(e) if error_unsupported(&e) => return Ok(true),
Err(TryLockError::Error(e)) if error_unsupported(&e) => Ok(true),

Err(e) => {
if !error_contended(&e) {
let e = anyhow::Error::from(e);
let cx = format!("failed to lock file: {}", path.display());
return Err(e.context(cx));
}
Err(TryLockError::Error(e)) => {
let e = anyhow::Error::from(e);
let cx = format!("failed to lock file: {}", path.display());
Err(e.context(cx))
}

Err(TryLockError::WouldBlock) => Ok(false),
}
Ok(false)
}

/// Acquires a lock on a file in a "nice" manner.
Expand All @@ -389,7 +386,7 @@ fn acquire(
gctx: &GlobalContext,
msg: &str,
path: &Path,
lock_try: &dyn Fn() -> io::Result<()>,
lock_try: &dyn Fn() -> Result<(), TryLockError>,
lock_block: &dyn Fn() -> io::Result<()>,
) -> CargoResult<()> {
if cfg!(debug_assertions) {
Expand Down Expand Up @@ -431,176 +428,20 @@ fn is_on_nfs_mount(_path: &Path) -> bool {
}

#[cfg(unix)]
mod sys {
use std::fs::File;
use std::io::{Error, Result};
use std::os::unix::io::AsRawFd;

#[cfg(not(target_os = "solaris"))]
const LOCK_SH: i32 = libc::LOCK_SH;
#[cfg(target_os = "solaris")]
const LOCK_SH: i32 = 1;
#[cfg(not(target_os = "solaris"))]
const LOCK_EX: i32 = libc::LOCK_EX;
#[cfg(target_os = "solaris")]
const LOCK_EX: i32 = 2;
#[cfg(not(target_os = "solaris"))]
const LOCK_NB: i32 = libc::LOCK_NB;
#[cfg(target_os = "solaris")]
const LOCK_NB: i32 = 4;
#[cfg(not(target_os = "solaris"))]
const LOCK_UN: i32 = libc::LOCK_UN;
#[cfg(target_os = "solaris")]
const LOCK_UN: i32 = 8;

pub(super) fn lock_shared(file: &File) -> Result<()> {
flock(file, LOCK_SH)
}

pub(super) fn lock_exclusive(file: &File) -> Result<()> {
flock(file, LOCK_EX)
}

pub(super) fn try_lock_shared(file: &File) -> Result<()> {
flock(file, LOCK_SH | LOCK_NB)
}

pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
flock(file, LOCK_EX | LOCK_NB)
}

pub(super) fn unlock(file: &File) -> Result<()> {
flock(file, LOCK_UN)
}

pub(super) fn error_contended(err: &Error) -> bool {
err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
}

pub(super) fn error_unsupported(err: &Error) -> bool {
match err.raw_os_error() {
// Unfortunately, depending on the target, these may or may not be the same.
// For targets in which they are the same, the duplicate pattern causes a warning.
#[allow(unreachable_patterns)]
Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
Some(libc::ENOSYS) => true,
_ => false,
}
}

#[cfg(not(target_os = "solaris"))]
fn flock(file: &File, flag: libc::c_int) -> Result<()> {
let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
if ret < 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}

#[cfg(target_os = "solaris")]
fn flock(file: &File, flag: libc::c_int) -> Result<()> {
// Solaris lacks flock(), so try to emulate using fcntl()
let mut flock = libc::flock {
l_type: 0,
l_whence: 0,
l_start: 0,
l_len: 0,
l_sysid: 0,
l_pid: 0,
l_pad: [0, 0, 0, 0],
};
flock.l_type = if flag & LOCK_UN != 0 {
libc::F_UNLCK
} else if flag & LOCK_EX != 0 {
libc::F_WRLCK
} else if flag & LOCK_SH != 0 {
libc::F_RDLCK
} else {
panic!("unexpected flock() operation")
};

let mut cmd = libc::F_SETLKW;
if (flag & LOCK_NB) != 0 {
cmd = libc::F_SETLK;
}

let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };

if ret < 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
fn error_unsupported(err: &std::io::Error) -> bool {
match err.raw_os_error() {
// Unfortunately, depending on the target, these may or may not be the same.
// For targets in which they are the same, the duplicate pattern causes a warning.
#[allow(unreachable_patterns)]
Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
Some(libc::ENOSYS) => true,
_ => false,
}
}

#[cfg(windows)]
mod sys {
use std::fs::File;
use std::io::{Error, Result};
use std::mem;
use std::os::windows::io::AsRawHandle;

use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
use windows_sys::Win32::Storage::FileSystem::{
LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx, UnlockFile,
};

pub(super) fn lock_shared(file: &File) -> Result<()> {
lock_file(file, 0)
}

pub(super) fn lock_exclusive(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
}

pub(super) fn try_lock_shared(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
}

pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
}

pub(super) fn error_contended(err: &Error) -> bool {
err.raw_os_error()
.map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
}

pub(super) fn error_unsupported(err: &Error) -> bool {
err.raw_os_error()
.map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
}

pub(super) fn unlock(file: &File) -> Result<()> {
unsafe {
let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
if ret == 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}
}

fn lock_file(file: &File, flags: u32) -> Result<()> {
unsafe {
let mut overlapped = mem::zeroed();
let ret = LockFileEx(
file.as_raw_handle() as HANDLE,
flags,
0,
!0,
!0,
&mut overlapped,
);
if ret == 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}
}
fn error_unsupported(err: &std::io::Error) -> bool {
use windows_sys::Win32::Foundation::ERROR_INVALID_FUNCTION;
err.raw_os_error()
.map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
}
Loading