use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(any(unix, windows))]
use std::time::Instant;
const WAIT_FOR_HOLDER: Duration = Duration::from_millis(750);
const RETRY_EVERY: Duration = Duration::from_millis(25);
#[derive(Debug)]
pub(crate) struct PathWriteLock {
#[allow(dead_code)]
file: std::fs::File,
held: bool,
}
impl PathWriteLock {
pub(crate) fn is_held(&self) -> bool {
self.held
}
}
fn lock_path_for(target: &Path) -> Option<PathBuf> {
let absolute = target
.canonicalize()
.unwrap_or_else(|_| target.to_path_buf());
let mut hasher = std::collections::hash_map::DefaultHasher::new();
std::hash::Hash::hash(&absolute, &mut hasher);
let key = format!("{:016x}", std::hash::Hasher::finish(&hasher));
let dir = crate::config::profile::resolve_profile_home().join("locks/paths");
if let Err(e) = std::fs::create_dir_all(&dir) {
tracing::debug!("path lock: cannot create {}: {e}", dir.display());
return None;
}
Some(dir.join(format!("{key}.lock")))
}
pub(crate) fn lock_path(target: &Path) -> Option<PathBuf> {
lock_path_for(target)
}
pub(crate) fn acquire_at(lock_path: &Path) -> Option<PathWriteLock> {
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(lock_path)
.ok()?;
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let deadline = Instant::now() + WAIT_FOR_HOLDER;
loop {
match crate::config::flock::exclusive(fd, true) {
crate::config::flock::FlockOutcome::Acquired => {
return Some(PathWriteLock { file, held: true });
}
crate::config::flock::FlockOutcome::Held => {
if Instant::now() >= deadline {
return Some(PathWriteLock { file, held: false });
}
std::thread::sleep(RETRY_EVERY);
}
crate::config::flock::FlockOutcome::Failed(e) => {
tracing::debug!(
"path lock: cannot lock {}: {e} — writing without it",
lock_path.display()
);
return Some(PathWriteLock { file, held: false });
}
}
}
}
#[cfg(windows)]
{
use std::os::windows::io::AsRawHandle;
let handle = file.as_raw_handle();
let deadline = Instant::now() + WAIT_FOR_HOLDER;
loop {
match crate::config::winlock::exclusive(handle, true) {
crate::config::winlock::LockOutcome::Acquired => {
return Some(PathWriteLock { file, held: true });
}
crate::config::winlock::LockOutcome::Held => {
if Instant::now() >= deadline {
return Some(PathWriteLock { file, held: false });
}
std::thread::sleep(RETRY_EVERY);
}
crate::config::winlock::LockOutcome::Failed(e) => {
tracing::debug!(
"path lock: cannot lock {}: {e} — writing without it",
lock_path.display()
);
return Some(PathWriteLock { file, held: false });
}
}
}
}
#[cfg(not(any(unix, windows)))]
{
Some(PathWriteLock { file, held: false })
}
}
pub(crate) fn acquire(target: &Path) -> Option<PathWriteLock> {
let lock_path = lock_path_for(target)?;
acquire_at(&lock_path)
}
pub(crate) fn contention_notice(target: &Path) -> String {
format!(
"\n\nNote: another agent was writing `{}` at the same time. Both writes went \
through, so this file may not contain what either intended — re-read it before \
relying on it.",
target.display()
)
}