use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
use fs4::fs_std::FileExt;
use crate::error::{self, Error, Result};
use crate::{home, ids};
pub const TIMEOUT_ENV: &str = "ONEVCS_LOCK_TIMEOUT_SECONDS";
pub const DEFAULT_TIMEOUT_SECONDS: f64 = 900.0;
#[derive(Debug)]
pub struct Guard {
file: File,
}
impl Drop for Guard {
fn drop(&mut self) {
let _ = FileExt::unlock(&self.file);
}
}
pub fn timeout_seconds() -> Result<f64> {
let Some(raw) = std::env::var_os(TIMEOUT_ENV) else {
return Ok(DEFAULT_TIMEOUT_SECONDS);
};
let raw = raw.to_string_lossy().into_owned();
let value: f64 = raw.trim().parse().map_err(|_| Error::Invalid {
reason: format!("{TIMEOUT_ENV} must be a number of seconds, not {raw:?}"),
})?;
if !value.is_finite() || value <= 0.0 {
return Err(Error::Invalid {
reason: format!(
"{TIMEOUT_ENV} must be a finite number of seconds above zero, not {raw:?}"
),
});
}
Ok(value)
}
pub fn path_for(identity: &str) -> Result<PathBuf> {
Ok(home::locks_dir()?.join(format!("{}.lock", ids::digest(identity))))
}
pub fn git_identity(common_dir: &Path) -> String {
format!("git:{}", common_dir.display())
}
pub fn exclusive(identity: &str) -> Result<Guard> {
acquire(identity, timeout_seconds()?)
}
pub fn try_shared(identity: &str) -> Result<Option<Guard>> {
try_acquire(identity, true)
}
pub fn try_exclusive(identity: &str) -> Result<Option<Guard>> {
try_acquire(identity, false)
}
fn open(identity: &str) -> Result<(PathBuf, File)> {
let path = path_for(identity)?;
home::ensure_dir(path.parent().unwrap_or(Path::new(".")))?;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(error::at("open the lock at", &path))?;
Ok((path, file))
}
fn try_acquire(identity: &str, is_shared: bool) -> Result<Option<Guard>> {
let (_, file) = open(identity)?;
let taken = if is_shared {
FileExt::try_lock_shared(&file)
} else {
FileExt::try_lock_exclusive(&file)
};
match taken {
Ok(true) => {
if !is_shared {
record_owner(&file);
}
Ok(Some(Guard { file }))
}
Ok(false) | Err(_) => Ok(None),
}
}
fn acquire(identity: &str, bound: f64) -> Result<Guard> {
let (path, file) = open(identity)?;
let (sender, receiver) = mpsc::channel();
std::thread::spawn(move || {
if FileExt::lock_exclusive(&file).is_ok() {
let _ = sender.send(file);
}
});
match receiver.recv_timeout(Duration::from_secs_f64(bound)) {
Ok(file) => {
record_owner(&file);
Ok(Guard { file })
}
Err(_) => Err(Error::Invalid {
reason: format!(
"timed out after {bound}s waiting for {identity}; owner: {} \
(raise {TIMEOUT_ENV} if this wait is legitimate)",
recorded_owner(&path)
),
}),
}
}
fn record_owner(file: &File) {
let mut handle = file;
let _ = handle.set_len(0);
let _ = write!(handle, "pid={}", std::process::id());
let _ = handle.flush();
}
fn recorded_owner(path: &Path) -> String {
std::fs::read_to_string(path)
.ok()
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "unknown owner".to_owned())
}