use std::fs::{File, OpenOptions};
use std::io;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Display, Path, PathBuf};
use fs2::{lock_contended_error, FileExt};
use termcolor::Color::Cyan;
#[cfg(windows)]
use winapi::shared::winerror::ERROR_INVALID_FUNCTION;
use crate::util::errors::{CargoResult, CargoResultExt};
use crate::util::paths;
use crate::util::Config;
#[derive(Debug)]
pub struct FileLock {
f: Option<File>,
path: PathBuf,
state: State,
}
#[derive(PartialEq, Debug)]
enum State {
Unlocked,
Shared,
Exclusive,
}
impl FileLock {
pub fn file(&self) -> &File {
self.f.as_ref().unwrap()
}
pub fn path(&self) -> &Path {
assert_ne!(self.state, State::Unlocked);
&self.path
}
pub fn parent(&self) -> &Path {
assert_ne!(self.state, State::Unlocked);
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(())
}
}
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 self.state != State::Unlocked {
if let Some(f) = self.f.take() {
let _ = f.unlock();
}
}
}
}
#[derive(Clone, Debug)]
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)?;
Ok(())
}
pub fn display(&self) -> Display<'_> {
self.root.display()
}
pub fn open_rw<P>(&self, path: P, config: &Config, msg: &str) -> CargoResult<FileLock>
where
P: AsRef<Path>,
{
self.open(
path.as_ref(),
OpenOptions::new().read(true).write(true).create(true),
State::Exclusive,
config,
msg,
)
}
pub fn open_ro<P>(&self, path: P, config: &Config, msg: &str) -> CargoResult<FileLock>
where
P: AsRef<Path>,
{
self.open(
path.as_ref(),
OpenOptions::new().read(true),
State::Shared,
config,
msg,
)
}
fn open(
&self,
path: &Path,
opts: &OpenOptions,
state: State,
config: &Config,
msg: &str,
) -> CargoResult<FileLock> {
let path = self.root.join(path);
let f = opts
.open(&path)
.or_else(|e| {
if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
paths::create_dir_all(path.parent().unwrap())?;
Ok(opts.open(&path)?)
} else {
Err(anyhow::Error::from(e))
}
})
.chain_err(|| format!("failed to open: {}", path.display()))?;
match state {
State::Exclusive => {
acquire(config, msg, &path, &|| f.try_lock_exclusive(), &|| {
f.lock_exclusive()
})?;
}
State::Shared => {
acquire(config, msg, &path, &|| f.try_lock_shared(), &|| {
f.lock_shared()
})?;
}
State::Unlocked => {}
}
Ok(FileLock {
f: Some(f),
path,
state,
})
}
}
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 acquire(
config: &Config,
msg: &str,
path: &Path,
r#try: &dyn Fn() -> io::Result<()>,
block: &dyn Fn() -> io::Result<()>,
) -> CargoResult<()> {
if is_on_nfs_mount(path) {
return Ok(());
}
match r#try() {
Ok(()) => return Ok(()),
#[cfg(unix)]
Err(ref e) if e.raw_os_error() == Some(libc::ENOTSUP) => return Ok(()),
#[cfg(target_os = "linux")]
Err(ref e) if e.raw_os_error() == Some(libc::ENOSYS) => return Ok(()),
#[cfg(windows)]
Err(ref e) if e.raw_os_error() == Some(ERROR_INVALID_FUNCTION as i32) => return Ok(()),
Err(e) => {
if e.raw_os_error() != lock_contended_error().raw_os_error() {
let e = anyhow::Error::from(e);
let cx = format!("failed to lock file: {}", path.display());
return Err(e.context(cx).into());
}
}
}
let msg = format!("waiting for file lock on {}", msg);
config.shell().status_with_color("Blocking", &msg, Cyan)?;
block().chain_err(|| format!("failed to lock file: {}", path.display()))?;
return Ok(());
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
fn is_on_nfs_mount(path: &Path) -> bool {
use std::ffi::CString;
use std::mem;
use std::os::unix::prelude::*;
let path = match CString::new(path.as_os_str().as_bytes()) {
Ok(path) => path,
Err(_) => 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"))]
fn is_on_nfs_mount(_path: &Path) -> bool {
false
}
}