use std::error::Error;
use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write as _};
use std::path::{Path, PathBuf};
use crate::record::Record;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Role {
lock: PathBuf,
record: PathBuf,
}
impl Role {
#[must_use]
pub fn new(dir: impl AsRef<Path>, name: &str) -> Self {
let dir = dir.as_ref();
Self {
lock: dir.join(format!("{name}.lock")),
record: dir.join(format!("{name}.claim")),
}
}
pub fn claim(&self) -> Result<Tenancy, ClaimError> {
if let Some(parent) = self.lock.parent() {
fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&self.lock)?;
match file.try_lock() {
Ok(()) => Ok(Tenancy {
_lock: file,
record: self.record.clone(),
}),
Err(fs::TryLockError::WouldBlock) => Err(ClaimError::Occupied),
Err(fs::TryLockError::Error(source)) => Err(ClaimError::Io(source)),
}
}
pub fn occupancy(&self) -> io::Result<Occupancy> {
let file = match OpenOptions::new().read(true).write(true).open(&self.lock) {
Ok(file) => file,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Occupancy::Free),
Err(error) => return Err(error),
};
match file.try_lock() {
Ok(()) => Ok(Occupancy::Free),
Err(fs::TryLockError::WouldBlock) => Ok(self.identify_holder()),
Err(fs::TryLockError::Error(source)) => Err(source),
}
}
#[must_use]
pub fn lock_path(&self) -> &Path {
&self.lock
}
#[must_use]
pub fn record_path(&self) -> &Path {
&self.record
}
fn identify_holder(&self) -> Occupancy {
fs::read_to_string(&self.record)
.ok()
.and_then(|text| Record::parse(&text).ok())
.map_or(Occupancy::HeldAnonymously, Occupancy::HeldBy)
}
}
#[derive(Debug)]
pub struct Tenancy {
_lock: File,
record: PathBuf,
}
impl Tenancy {
pub fn publish(&self, record: &Record) -> io::Result<()> {
let mut temporary = self.record.clone().into_os_string();
temporary.push(format!(".{}.tmp", std::process::id()));
let temporary = PathBuf::from(temporary);
let mut file = File::create(&temporary)?;
file.write_all(record.to_text().as_bytes())?;
file.sync_all()?;
drop(file);
fs::rename(&temporary, &self.record).inspect_err(|_| {
let _ = fs::remove_file(&temporary);
})
}
}
impl Drop for Tenancy {
fn drop(&mut self) {
let _ = fs::remove_file(&self.record);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Occupancy {
Free,
HeldBy(Record),
HeldAnonymously,
}
impl fmt::Display for Occupancy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Free => f.write_str("free"),
Self::HeldBy(record) => write!(f, "held by {record}"),
Self::HeldAnonymously => f.write_str("held by an unidentified tenant"),
}
}
}
#[derive(Debug)]
pub enum ClaimError {
Occupied,
Io(io::Error),
}
impl fmt::Display for ClaimError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Occupied => f.write_str("the role is already held"),
Self::Io(_) => f.write_str("the role's lock file could not be taken"),
}
}
}
impl Error for ClaimError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Occupied => None,
Self::Io(source) => Some(source),
}
}
}
impl From<io::Error> for ClaimError {
fn from(source: io::Error) -> Self {
Self::Io(source)
}
}