use std::fs::{File, OpenOptions, TryLockError};
use std::io::Write as _;
use std::path::Path;
pub const FILE: &str = "ledger.lock";
#[derive(Debug)]
pub struct WorkspaceLock {
_file: File,
}
pub fn lock(workspace: &Path, front_end: &str) -> Result<WorkspaceLock, String> {
let path = workspace.join(FILE);
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(|e| format!("cannot open {}: {e}", path.display()))?;
match file.try_lock() {
Ok(()) => {
let _ = file
.set_len(0)
.and_then(|()| write!(file, "{front_end} {}", std::process::id()))
.and_then(|()| file.flush());
Ok(WorkspaceLock { _file: file })
}
Err(TryLockError::WouldBlock) => {
let holder = std::fs::read_to_string(&path)
.ok()
.and_then(|text| holder(&text));
Err(match holder {
Some((front_end, pid)) => {
format!("this profile's ledger is in use by bacnet {front_end} (pid {pid})")
}
None => "this profile's ledger is in use by another bacnet process".into(),
})
}
Err(TryLockError::Error(error)) => Err(format!("cannot lock {}: {error}", path.display())),
}
}
fn holder(text: &str) -> Option<(String, u32)> {
let mut words = text.split_whitespace();
let (front_end, pid) = match (words.next()?, words.next(), words.next()) {
(pid, None, None) => ("tui", pid),
(front_end, Some(pid), None) => (front_end, pid),
_ => return None,
};
let valid = !front_end.is_empty()
&& front_end
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch == '-');
valid
.then(|| pid.parse().ok())
.flatten()
.map(|pid| (front_end.to_string(), pid))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_second_holder_is_refused_until_the_first_lets_go() {
let dir = tempfile::tempdir().unwrap();
let first = lock(dir.path(), "tui").unwrap();
let refused = lock(dir.path(), "mcp").unwrap_err();
assert!(refused.contains("in use"), "{refused}");
#[cfg(unix)]
assert!(
refused.contains(&format!("bacnet tui (pid {})", std::process::id())),
"{refused}"
);
drop(first);
lock(dir.path(), "mcp").unwrap();
}
#[test]
fn the_holder_is_read_back() {
assert_eq!(holder("tui 42\n"), Some(("tui".into(), 42)));
assert_eq!(holder("mcp 7"), Some(("mcp".into(), 7)));
assert_eq!(
holder("42"),
Some(("tui".into(), 42)),
"an earlier release's file"
);
assert_eq!(holder(""), None);
assert_eq!(holder("tui"), None);
assert_eq!(holder("TUI 42"), None);
assert_eq!(holder("tui 42 extra"), None);
}
}