use std::{
fmt,
path::{Path, PathBuf},
time::Duration,
};
use gix_tempfile::{AutoRemove, ContainingDirectory};
use crate::{backoff, File, Marker, DOT_LOCK_SUFFIX};
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Fail {
#[default]
Immediately,
AfterDurationWithBackoff(Duration),
}
impl fmt::Display for Fail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Fail::Immediately => f.write_str("immediately"),
Fail::AfterDurationWithBackoff(duration) => {
write!(f, "after {:.02}s", duration.as_secs_f32())
}
}
}
}
impl From<Duration> for Fail {
fn from(value: Duration) -> Self {
if value.is_zero() {
Fail::Immediately
} else {
Fail::AfterDurationWithBackoff(value)
}
}
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Another IO error occurred while obtaining the lock")]
Io(#[from] std::io::Error),
#[error("The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{}' might need manual deletion.", super::DOT_LOCK_SUFFIX)]
PermanentlyLocked {
resource_path: PathBuf,
mode: Fail,
attempts: usize,
},
}
impl File {
pub fn acquire_to_update_resource(
at_path: impl AsRef<Path>,
mode: Fail,
boundary_directory: Option<PathBuf>,
) -> Result<File, Error> {
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
if let Some(permissions) = default_permissions() {
gix_tempfile::writable_at_with_permissions(p, d, c, permissions)
} else {
gix_tempfile::writable_at(p, d, c)
}
})?;
Ok(File {
inner: handle,
lock_path,
})
}
pub fn acquire_to_update_resource_with_permissions(
at_path: impl AsRef<Path>,
mode: Fail,
boundary_directory: Option<PathBuf>,
make_permissions: impl Fn() -> std::fs::Permissions,
) -> Result<File, Error> {
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
gix_tempfile::writable_at_with_permissions(p, d, c, make_permissions())
})?;
Ok(File {
inner: handle,
lock_path,
})
}
}
impl Marker {
pub fn acquire_to_hold_resource(
at_path: impl AsRef<Path>,
mode: Fail,
boundary_directory: Option<PathBuf>,
) -> Result<Marker, Error> {
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
if let Some(permissions) = default_permissions() {
gix_tempfile::mark_at_with_permissions(p, d, c, permissions)
} else {
gix_tempfile::mark_at(p, d, c)
}
})?;
Ok(Marker {
created_from_file: false,
inner: handle,
lock_path,
})
}
pub fn acquire_to_hold_resource_with_permissions(
at_path: impl AsRef<Path>,
mode: Fail,
boundary_directory: Option<PathBuf>,
make_permissions: impl Fn() -> std::fs::Permissions,
) -> Result<Marker, Error> {
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
gix_tempfile::mark_at_with_permissions(p, d, c, make_permissions())
})?;
Ok(Marker {
created_from_file: false,
inner: handle,
lock_path,
})
}
}
fn dir_cleanup(boundary: Option<PathBuf>) -> (ContainingDirectory, AutoRemove) {
match boundary {
None => (ContainingDirectory::Exists, AutoRemove::Tempfile),
Some(boundary_directory) => (
ContainingDirectory::CreateAllRaceProof(Default::default()),
AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory },
),
}
}
fn lock_with_mode<T>(
resource: &Path,
mode: Fail,
boundary_directory: Option<PathBuf>,
try_lock: &dyn Fn(&Path, ContainingDirectory, AutoRemove) -> std::io::Result<T>,
) -> Result<(PathBuf, T), Error> {
use std::io::ErrorKind::*;
let (directory, cleanup) = dir_cleanup(boundary_directory);
let lock_path = add_lock_suffix(resource);
let mut attempts = 1;
match mode {
Fail::Immediately => try_lock(&lock_path, directory, cleanup),
Fail::AfterDurationWithBackoff(time) => {
for wait in backoff::Exponential::default_with_random().until_no_remaining(time) {
attempts += 1;
match try_lock(&lock_path, directory, cleanup.clone()) {
Ok(v) => return Ok((lock_path, v)),
#[cfg(windows)]
Err(err) if err.kind() == AlreadyExists || err.kind() == PermissionDenied => {
std::thread::sleep(wait);
continue;
}
#[cfg(not(windows))]
Err(err) if err.kind() == AlreadyExists => {
std::thread::sleep(wait);
continue;
}
Err(err) => return Err(Error::from(err)),
}
}
try_lock(&lock_path, directory, cleanup)
}
}
.map(|v| (lock_path, v))
.map_err(|err| match err.kind() {
AlreadyExists => Error::PermanentlyLocked {
resource_path: resource.into(),
mode,
attempts,
},
_ => Error::Io(err),
})
}
fn add_lock_suffix(resource_path: &Path) -> PathBuf {
resource_path.with_extension(resource_path.extension().map_or_else(
|| DOT_LOCK_SUFFIX.chars().skip(1).collect(),
|ext| format!("{}{}", ext.to_string_lossy(), DOT_LOCK_SUFFIX),
))
}
fn default_permissions() -> Option<std::fs::Permissions> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Some(std::fs::Permissions::from_mode(0o666))
}
#[cfg(not(unix))]
{
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_lock_suffix_to_file_with_extension() {
assert_eq!(add_lock_suffix(Path::new("hello.ext")), Path::new("hello.ext.lock"));
}
#[test]
fn add_lock_suffix_to_file_without_extension() {
assert_eq!(add_lock_suffix(Path::new("hello")), Path::new("hello.lock"));
}
}