use std::{
fs::{File, OpenOptions},
io::{self, Write},
path::Path,
};
use fs2::{FileExt, lock_contended_error};
use super::data_dir::restrict_file;
pub(crate) struct Lock {
file: File,
}
impl Lock {
pub(crate) fn write_fingerprint(&mut self) {
let _ = self.file.set_len(0);
let _ = writeln!(self.file, "{}{}", std::process::id(), start_time_suffix());
let _ = self.file.flush();
}
}
impl Drop for Lock {
fn drop(&mut self) {
let _ = FileExt::unlock(&self.file);
}
}
pub(crate) fn try_exclusive(path: &Path) -> io::Result<Option<Lock>> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
restrict_file(path)?;
match file.try_lock_exclusive() {
Ok(()) => Ok(Some(Lock { file })),
Err(error) if is_lock_contended(&error) => Ok(None),
Err(error) => Err(io::Error::new(
error.kind(),
format!("{}: {error}", path.display()),
)),
}
}
pub(crate) fn exclusive(path: &Path) -> io::Result<Lock> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
restrict_file(path)?;
file.lock_exclusive()
.map_err(|error| io::Error::new(error.kind(), format!("{}: {error}", path.display())))?;
Ok(Lock { file })
}
pub(crate) fn is_held(path: &Path) -> bool {
matches!(try_exclusive(path), Ok(None))
}
pub(crate) fn is_lock_contended(error: &io::Error) -> bool {
let expected = lock_contended_error();
match (error.raw_os_error(), expected.raw_os_error()) {
(Some(actual), Some(expected)) => actual == expected,
_ => error.kind() == expected.kind(),
}
}
#[cfg(target_os = "linux")]
fn start_time_suffix() -> String {
std::fs::read_to_string("/proc/self/stat")
.ok()
.and_then(|stat| {
let (_, after) = stat.rsplit_once(')')?;
let start = after.split_whitespace().nth(19)?;
Some(format!(" {start}"))
})
.unwrap_or_default()
}
#[cfg(not(target_os = "linux"))]
fn start_time_suffix() -> String {
String::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_contention_error_is_recognized_across_platforms() {
assert!(is_lock_contended(&lock_contended_error()));
}
#[test]
fn a_held_lock_is_contended_and_a_dropped_one_is_free() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("attach.lock");
let held = try_exclusive(&path).unwrap().expect("first holder");
assert!(is_held(&path));
drop(held);
assert!(!is_held(&path));
assert!(try_exclusive(&path).unwrap().is_some());
}
#[test]
fn the_fingerprint_is_written_without_recreating_the_lock_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("attach.lock");
let mut held = try_exclusive(&path).unwrap().expect("holder");
held.write_fingerprint();
let written = std::fs::read_to_string(&path).unwrap();
assert!(
written.starts_with(&std::process::id().to_string()),
"{written}"
);
assert!(is_held(&path));
}
}