use std::fs::TryLockError;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Display, Path, PathBuf};
use crate::util::GlobalContext;
use crate::util::errors::CargoResult;
use crate::util::style;
use anyhow::Context as _;
use cargo_util::paths;
pub(crate) use self::imp::lock_exclusive;
pub(crate) use self::imp::lock_shared;
#[expect(unused_imports, reason = "for non-blocking lock callers")]
pub(crate) use self::imp::try_lock_exclusive;
#[expect(unused_imports, reason = "for non-blocking lock callers")]
pub(crate) use self::imp::try_lock_shared;
pub(crate) use self::imp::unlock;
#[derive(Debug)]
pub struct FileLock {
f: Option<File>,
path: PathBuf,
}
impl FileLock {
pub fn file(&self) -> &File {
self.f.as_ref().unwrap()
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn parent(&self) -> &Path {
self.path.parent().unwrap()
}
pub fn remove_siblings(&self) -> CargoResult<()> {
let path = self.path();
for entry in path.parent().unwrap().read_dir()? {
let entry = entry?;
if Some(&entry.file_name()[..]) == path.file_name() {
continue;
}
let kind = entry.file_type()?;
if kind.is_dir() {
paths::remove_dir_all(entry.path())?;
} else {
paths::remove_file(entry.path())?;
}
}
Ok(())
}
pub fn rename<P: AsRef<Path>>(&mut self, new_path: P) -> CargoResult<()> {
let new_path = new_path.as_ref();
std::fs::rename(&self.path, new_path).with_context(|| {
format!(
"failed to rename {} to {}",
self.path.display(),
new_path.display()
)
})?;
self.path = new_path.to_path_buf();
Ok(())
}
}
impl Read for FileLock {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.file().read(buf)
}
}
impl Seek for FileLock {
fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
self.file().seek(to)
}
}
impl Write for FileLock {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.file().flush()
}
}
impl Drop for FileLock {
fn drop(&mut self) {
if let Some(f) = self.f.take() {
if let Err(e) = imp::unlock(&f) {
tracing::warn!("failed to release lock: {e:?}");
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Filesystem {
root: PathBuf,
}
impl Filesystem {
pub fn new(path: PathBuf) -> Filesystem {
Filesystem { root: path }
}
pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
Filesystem::new(self.root.join(other))
}
pub fn push<T: AsRef<Path>>(&mut self, other: T) {
self.root.push(other);
}
pub fn into_path_unlocked(self) -> PathBuf {
self.root
}
pub fn as_path_unlocked(&self) -> &Path {
&self.root
}
pub fn create_dir(&self) -> CargoResult<()> {
paths::create_dir_all(&self.root)
}
pub fn display(&self) -> Display<'_> {
self.root.display()
}
pub fn open_rw_exclusive_create<P>(
&self,
path: P,
gctx: &GlobalContext,
msg: &str,
) -> CargoResult<FileLock>
where
P: AsRef<Path>,
{
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, &|| imp::try_lock_exclusive(&f), &|| {
imp::lock_exclusive(&f)
})?;
Ok(FileLock { f: Some(f), path })
}
pub fn try_open_rw_exclusive_create<P: AsRef<Path>>(
&self,
path: P,
) -> CargoResult<Option<FileLock>> {
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, &|| imp::try_lock_exclusive(&f))? {
Ok(Some(FileLock { f: Some(f), path }))
} else {
Ok(None)
}
}
pub fn open_ro_shared<P>(
&self,
path: P,
gctx: &GlobalContext,
msg: &str,
) -> CargoResult<FileLock>
where
P: AsRef<Path>,
{
let (path, f) = self.open(path.as_ref(), &OpenOptions::new().read(true), false)?;
acquire(gctx, msg, &path, &|| imp::try_lock_shared(&f), &|| {
imp::lock_shared(&f)
})?;
Ok(FileLock { f: Some(f), path })
}
pub fn open_ro_shared_create<P: AsRef<Path>>(
&self,
path: P,
gctx: &GlobalContext,
msg: &str,
) -> CargoResult<FileLock> {
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, &|| imp::try_lock_shared(&f), &|| {
imp::lock_shared(&f)
})?;
Ok(FileLock { f: Some(f), path })
}
pub fn try_open_ro_shared_create<P: AsRef<Path>>(
&self,
path: P,
) -> CargoResult<Option<FileLock>> {
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, &|| imp::try_lock_shared(&f))? {
Ok(Some(FileLock { f: Some(f), path }))
} else {
Ok(None)
}
}
fn open(&self, path: &Path, opts: &OpenOptions, create: bool) -> CargoResult<(PathBuf, File)> {
let path = self.root.join(path);
let f = opts
.open(&path)
.or_else(|e| {
if e.kind() == io::ErrorKind::NotFound && create {
paths::create_dir_all(path.parent().unwrap())?;
Ok(opts.open(&path)?)
} else {
Err(anyhow::Error::from(e))
}
})
.with_context(|| format!("failed to open: {}", path.display()))?;
Ok((path, f))
}
}
impl PartialEq<Path> for Filesystem {
fn eq(&self, other: &Path) -> bool {
self.root == other
}
}
impl PartialEq<Filesystem> for Path {
fn eq(&self, other: &Filesystem) -> bool {
self == other.root
}
}
fn try_acquire(path: &Path, lock_try: &dyn Fn() -> Result<(), TryLockError>) -> CargoResult<bool> {
if is_on_nfs_mount(path) {
tracing::debug!("{path:?} appears to be an NFS mount, not trying to lock");
return Ok(true);
}
match lock_try() {
Ok(()) => Ok(true),
Err(TryLockError::Error(e)) if error_unsupported(&e) => Ok(true),
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),
}
}
fn acquire(
gctx: &GlobalContext,
msg: &str,
path: &Path,
lock_try: &dyn Fn() -> Result<(), TryLockError>,
lock_block: &dyn Fn() -> io::Result<()>,
) -> CargoResult<()> {
gctx.debug_assert_shell_not_borrowed();
if try_acquire(path, lock_try)? {
return Ok(());
}
let msg = if gctx.extra_verbose() {
format!("waiting for file lock on {} ({})", msg, path.display())
} else {
format!("waiting for file lock on {}", msg)
};
gctx.shell()
.status_with_color("Blocking", &msg, &style::NOTE)?;
lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
Ok(())
}
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
pub fn is_on_nfs_mount(path: &Path) -> bool {
use std::ffi::CString;
use std::mem;
use std::os::unix::prelude::*;
let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
return false;
};
unsafe {
let mut buf: libc::statfs = mem::zeroed();
let r = libc::statfs(path.as_ptr(), &mut buf);
r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
}
}
#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
pub fn is_on_nfs_mount(_path: &Path) -> bool {
false
}
#[cfg(unix)]
fn error_unsupported(err: &std::io::Error) -> bool {
match err.raw_os_error() {
#[allow(unreachable_patterns)]
Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
Some(libc::ENOSYS) => true,
_ => err.kind() == std::io::ErrorKind::Unsupported,
}
}
#[cfg(windows)]
fn error_unsupported(err: &std::io::Error) -> bool {
use windows_sys::Win32::Foundation::ERROR_INVALID_FUNCTION;
match err.raw_os_error() {
Some(code) if code == ERROR_INVALID_FUNCTION as i32 => true,
_ => err.kind() == std::io::ErrorKind::Unsupported,
}
}
#[cfg(not(target_os = "solaris"))]
#[expect(
clippy::disallowed_methods,
reason = "the OS doesn't need the fcntl shim"
)]
mod imp {
use super::*;
pub fn try_lock_exclusive(file: &File) -> Result<(), TryLockError> {
file.try_lock()
}
pub fn lock_exclusive(file: &File) -> io::Result<()> {
file.lock()
}
pub fn try_lock_shared(file: &File) -> Result<(), TryLockError> {
file.try_lock_shared()
}
pub fn lock_shared(file: &File) -> io::Result<()> {
file.lock_shared()
}
pub fn unlock(file: &File) -> io::Result<()> {
file.unlock()
}
}
#[cfg(target_os = "solaris")]
mod imp {
use super::*;
use std::mem;
use std::os::unix::io::AsRawFd;
pub fn try_lock_exclusive(file: &File) -> Result<(), TryLockError> {
match fcntl_lock(file, libc::F_WRLCK, libc::F_SETLK) {
Ok(()) => Ok(()),
Err(e) if is_would_block(&e) => Err(TryLockError::WouldBlock),
Err(e) => Err(TryLockError::Error(e)),
}
}
pub fn lock_exclusive(file: &File) -> io::Result<()> {
fcntl_lock(file, libc::F_WRLCK, libc::F_SETLKW)
}
pub fn try_lock_shared(file: &File) -> Result<(), TryLockError> {
match fcntl_lock(file, libc::F_RDLCK, libc::F_SETLK) {
Ok(()) => Ok(()),
Err(e) if is_would_block(&e) => Err(TryLockError::WouldBlock),
Err(e) => Err(TryLockError::Error(e)),
}
}
pub fn lock_shared(file: &File) -> io::Result<()> {
fcntl_lock(file, libc::F_RDLCK, libc::F_SETLKW)
}
pub fn unlock(file: &File) -> io::Result<()> {
fcntl_lock_raw(file, libc::F_UNLCK, libc::F_SETLK)
}
fn fcntl_lock(file: &File, lock_type: libc::c_short, cmd: libc::c_int) -> io::Result<()> {
fcntl_lock_raw(file, lock_type, cmd)
}
fn fcntl_lock_raw(file: &File, lock_type: libc::c_short, cmd: libc::c_int) -> io::Result<()> {
let mut lock = flock_for_whole_file(lock_type);
loop {
let result = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &mut lock) };
if result != -1 {
return Ok(());
}
let error = io::Error::last_os_error();
if cmd == libc::F_SETLKW && error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(error);
}
}
fn flock_for_whole_file(lock_type: libc::c_short) -> libc::flock {
let mut lock = unsafe { mem::zeroed::<libc::flock>() };
lock.l_type = lock_type;
lock.l_whence = libc::SEEK_SET as libc::c_short;
lock.l_start = 0;
lock.l_len = 0;
lock
}
fn is_would_block(error: &io::Error) -> bool {
matches!(error.raw_os_error(), Some(libc::EACCES | libc::EAGAIN))
|| error.kind() == io::ErrorKind::WouldBlock
}
}