use std::fs::{File, OpenOptions};
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Weak};
use std::{fmt, io};
use anyhow::{Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use fs4::{lock_contended_error, FileExt};
use tokio::sync::Mutex;
use crate::core::Config;
use crate::internal::fsx;
use crate::internal::lazy_directory_creator::LazyDirectoryCreator;
use crate::ui::Status;
const OK_FILE: &str = ".scarb-ok";
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum FileLockKind {
Shared,
Exclusive,
}
#[derive(Debug)]
pub struct FileLockGuard {
file: Option<File>,
path: Utf8PathBuf,
lock_kind: FileLockKind,
}
impl FileLockGuard {
pub fn path(&self) -> &Utf8Path {
self.path.as_path()
}
pub fn lock_kind(&self) -> FileLockKind {
self.lock_kind
}
}
impl Deref for FileLockGuard {
type Target = File;
fn deref(&self) -> &Self::Target {
self.file.as_ref().unwrap()
}
}
impl DerefMut for FileLockGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
self.file.as_mut().unwrap()
}
}
impl Drop for FileLockGuard {
fn drop(&mut self) {
if let Some(file) = self.file.take() {
let _ = file.unlock();
}
}
}
pub struct AdvisoryLock<'f> {
path: Utf8PathBuf,
description: String,
file_lock: Mutex<
Weak<FileLockGuard>,
>,
filesystem: &'f Filesystem<'f>,
config: &'f Config,
}
#[derive(Debug)]
pub struct AdvisoryLockGuard(Arc<FileLockGuard>);
impl<'f> AdvisoryLock<'f> {
pub async fn acquire_async(&self) -> Result<AdvisoryLockGuard> {
let mut slot = self.file_lock.lock().await;
let file_lock_arc = match slot.upgrade() {
Some(arc) => arc,
None => {
let arc = Arc::new(self.filesystem.open_rw(
&self.path,
&self.description,
self.config,
)?);
*slot = Arc::downgrade(&arc);
arc
}
};
Ok(AdvisoryLockGuard(file_lock_arc))
}
}
pub type RootFilesystem = Filesystem<'static>;
pub struct Filesystem<'a> {
root: LazyDirectoryCreator<'a>,
}
impl<'a> Filesystem<'a> {
pub fn new(root: Utf8PathBuf) -> Self {
Self {
root: LazyDirectoryCreator::new(root),
}
}
pub fn new_output_dir(root: Utf8PathBuf) -> Self {
Self {
root: LazyDirectoryCreator::new_output_dir(root),
}
}
pub fn child(&self, path: impl AsRef<Utf8Path>) -> Filesystem<'_> {
Filesystem {
root: self.root.child(path),
}
}
pub fn path_unchecked(&self) -> &Utf8Path {
self.root.as_unchecked()
}
pub fn path_existent(&self) -> Result<&Utf8Path> {
self.root.as_existent()
}
pub fn open_rw(
&self,
path: impl AsRef<Utf8Path>,
description: &str,
config: &Config,
) -> Result<FileLockGuard> {
self.open(
path.as_ref(),
OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.create(true),
FileLockKind::Exclusive,
description,
config,
)
}
pub fn open_ro(
&self,
path: impl AsRef<Utf8Path>,
description: &str,
config: &Config,
) -> Result<FileLockGuard> {
self.open(
path.as_ref(),
OpenOptions::new().read(true),
FileLockKind::Shared,
description,
config,
)
}
fn open(
&self,
path: &Utf8Path,
opts: &OpenOptions,
lock_kind: FileLockKind,
description: &str,
config: &Config,
) -> Result<FileLockGuard> {
let path = self.root.as_existent()?.join(path);
let file = opts
.open(&path)
.with_context(|| format!("failed to open: {path}"))?;
match lock_kind {
FileLockKind::Exclusive => {
acquire(
&file,
&path,
description,
config,
&FileExt::try_lock_exclusive,
&FileExt::lock_exclusive,
)?;
}
FileLockKind::Shared => {
acquire(
&file,
&path,
description,
config,
&FileExt::try_lock_shared,
&FileExt::lock_shared,
)?;
}
}
Ok(FileLockGuard {
file: Some(file),
path,
lock_kind,
})
}
pub fn advisory_lock(
&'a self,
path: impl AsRef<Utf8Path>,
description: impl ToString,
config: &'a Config,
) -> AdvisoryLock<'a> {
AdvisoryLock {
path: path.as_ref().to_path_buf(),
description: description.to_string(),
file_lock: Mutex::new(Weak::new()),
filesystem: self,
config,
}
}
pub(crate) unsafe fn recreate(&self) -> Result<()> {
if self.root.is_output_dir() {
panic!("cannot recreate output filesystems");
}
let path = self.root.as_unchecked();
if path.exists() {
fsx::remove_dir_all(path)?;
}
fsx::create_dir_all(path)?;
Ok(())
}
pub fn is_ok(&self) -> bool {
self.path_unchecked().join(OK_FILE).exists()
}
pub fn mark_ok(&self) -> Result<()> {
let _ = fsx::create(self.path_existent()?.join(OK_FILE))?;
Ok(())
}
}
impl<'a> fmt::Display for Filesystem<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.root)
}
}
impl<'a> fmt::Debug for Filesystem<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Filesystem").field(&self.root).finish()
}
}
fn acquire(
file: &File,
path: &Utf8Path,
description: &str,
config: &Config,
lock_try: &dyn Fn(&File) -> io::Result<()>,
lock_block: &dyn Fn(&File) -> io::Result<()>,
) -> Result<()> {
match lock_try(file) {
Ok(()) => return Ok(()),
Err(err) if err.kind() == io::ErrorKind::Unsupported => {
return Ok(());
}
Err(err) if is_lock_contended_error(&err) => {
}
Err(err) => {
Err(err).with_context(|| format!("failed to lock file: {path}"))?;
}
}
config.ui().print(Status::with_color(
"Blocking",
"cyan",
&format!("waiting for file lock on {description}"),
));
lock_block(file).with_context(|| format!("failed to lock file: {path}"))?;
Ok(())
}
fn is_lock_contended_error(err: &io::Error) -> bool {
let t = lock_contended_error();
err.raw_os_error() == t.raw_os_error() || err.kind() == t.kind()
}