use std::collections::HashMap;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use crate::transport::{Transport, TransportError};
const INFO_NAME: &str = "/info";
#[derive(Debug)]
pub enum LockError {
AlreadyHeld,
NotHeld,
Corrupt(String),
Transport(TransportError),
}
impl std::fmt::Display for LockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LockError::AlreadyHeld => write!(f, "lock already held"),
LockError::NotHeld => write!(f, "lock not held"),
LockError::Corrupt(m) => write!(f, "corrupt lock info: {m}"),
LockError::Transport(e) => write!(f, "transport error: {e}"),
}
}
}
impl std::error::Error for LockError {}
impl From<TransportError> for LockError {
fn from(e: TransportError) -> Self {
LockError::Transport(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct LockHeldInfo {
pub pid: Option<u32>,
pub user: Option<String>,
pub nonce: Option<String>,
pub hostname: Option<String>,
pub start_time: Option<SystemTime>,
#[serde(flatten)]
pub extra_holder_info: HashMap<String, String>,
}
impl LockHeldInfo {
pub fn for_this_process(extra_holder_info: HashMap<String, String>) -> Self {
let hostname = match crate::osutils::get_host_name() {
Ok(h) => Some(h),
Err(e) => {
log::warn!("could not determine hostname for lock holder info: {e}");
None
}
};
LockHeldInfo {
hostname,
pid: Some(std::process::id()),
nonce: Some(crate::osutils::rand_chars(20)),
start_time: Some(SystemTime::now()),
user: Some(crate::osutils::get_user_name()),
extra_holder_info,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
serde_yaml::to_string(self)
.expect("LockHeldInfo serialization cannot fail")
.into_bytes()
}
pub fn from_info_file_bytes(bytes: &[u8]) -> Result<Self, LockError> {
let value: serde_yaml::Value = serde_yaml::from_slice(bytes)
.map_err(|e| LockError::Corrupt(format!("could not parse lock info: {e}")))?;
if value.is_null() {
return Ok(LockHeldInfo::default());
}
serde_yaml::from_value(value)
.map_err(|e| LockError::Corrupt(format!("could not parse lock info: {e}")))
}
pub fn is_locked_by_this_process(&self) -> bool {
self.hostname == crate::osutils::get_host_name().ok()
&& self.pid == Some(std::process::id())
&& self.user == Some(crate::osutils::get_user_name())
}
}
pub trait Lock {
fn attempt_lock(&mut self) -> Result<String, LockError>;
fn unlock(&mut self) -> Result<(), LockError>;
fn peek(&self) -> Result<Option<LockHeldInfo>, LockError>;
fn is_held(&self) -> bool;
}
pub struct LockDir<'t> {
transport: &'t dyn Transport,
path: String,
held_dir: String,
held_info_path: String,
nonce: Option<String>,
lock_held: bool,
extra_holder_info: HashMap<String, String>,
}
impl<'t> LockDir<'t> {
pub fn new(transport: &'t dyn Transport, path: &str) -> Self {
let held_dir = format!("{path}/held");
let held_info_path = format!("{held_dir}{INFO_NAME}");
LockDir {
transport,
path: path.to_string(),
held_dir,
held_info_path,
nonce: None,
lock_held: false,
extra_holder_info: HashMap::new(),
}
}
pub fn with_extra_holder_info(mut self, extra: HashMap<String, String>) -> Self {
self.extra_holder_info = extra;
self
}
pub fn create(&self) -> Result<(), LockError> {
self.transport.mkdir(&self.path)?;
Ok(())
}
fn create_pending_dir(&self, info: &LockHeldInfo) -> Result<String, LockError> {
let tmpname = format!("{}/{}.tmp", self.path, crate::osutils::rand_chars(10));
self.transport.mkdir(&tmpname)?;
self.transport
.put_bytes(&format!("{tmpname}{INFO_NAME}"), &info.to_bytes(), None)?;
Ok(tmpname)
}
fn remove_pending_dir(&self, tmpname: &str) {
if let Err(e) = self.transport.delete(&format!("{tmpname}{INFO_NAME}")) {
log::warn!("error removing pending lock info {tmpname}{INFO_NAME}: {e}");
}
if let Err(e) = self.transport.rmdir(tmpname) {
log::warn!("error removing pending lock dir {tmpname}: {e}");
}
}
fn read_info_at(&self, path: &str) -> Result<Option<LockHeldInfo>, LockError> {
match self.transport.get_bytes(path) {
Ok(bytes) => Ok(Some(LockHeldInfo::from_info_file_bytes(&bytes)?)),
Err(TransportError::NoSuchFile(_)) => Ok(None),
Err(e) => Err(e.into()),
}
}
}
impl Lock for LockDir<'_> {
fn attempt_lock(&mut self) -> Result<String, LockError> {
if self.lock_held {
return Err(LockError::AlreadyHeld);
}
let info = LockHeldInfo::for_this_process(self.extra_holder_info.clone());
let nonce = info
.nonce
.clone()
.expect("for_this_process always sets a nonce");
let tmpname = self.create_pending_dir(&info)?;
match self.transport.rename(&tmpname, &self.held_dir) {
Ok(()) => {}
Err(TransportError::Io {
kind: std::io::ErrorKind::AlreadyExists,
..
}) => {
self.remove_pending_dir(&tmpname);
return Err(LockError::AlreadyHeld);
}
Err(e) => {
self.remove_pending_dir(&tmpname);
return Err(e.into());
}
}
match self.peek()? {
Some(held) if held.nonce.as_deref() == Some(nonce.as_str()) => {
self.nonce = Some(nonce.clone());
self.lock_held = true;
Ok(nonce)
}
_ => Err(LockError::AlreadyHeld),
}
}
fn unlock(&mut self) -> Result<(), LockError> {
if !self.lock_held {
return Err(LockError::NotHeld);
}
let tmpname = format!(
"{}/releasing.{}.tmp",
self.path,
crate::osutils::rand_chars(20)
);
self.transport.rename(&self.held_dir, &tmpname)?;
self.lock_held = false;
self.nonce = None;
self.transport.delete(&format!("{tmpname}{INFO_NAME}"))?;
if let Err(e) = delete_tree(self.transport, &tmpname) {
log::warn!("error removing released lock dir {tmpname}: {e}");
}
Ok(())
}
fn peek(&self) -> Result<Option<LockHeldInfo>, LockError> {
self.read_info_at(&self.held_info_path)
}
fn is_held(&self) -> bool {
self.lock_held
}
}
impl Drop for LockDir<'_> {
fn drop(&mut self) {
if self.lock_held {
let _ = self.unlock();
}
}
}
fn delete_tree(transport: &dyn Transport, path: &str) -> Result<(), TransportError> {
let entries = match transport.list_dir(path) {
Ok(e) => e,
Err(TransportError::NoSuchFile(_)) => return Ok(()),
Err(e) => return Err(e),
};
for entry in entries {
let child = format!("{path}/{entry}");
match transport.delete(&child) {
Ok(()) | Err(TransportError::NoSuchFile(_)) => {}
Err(_) => delete_tree(transport, &child)?,
}
}
transport.rmdir(path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::LocalTransport;
fn temp_transport() -> (tempfile::TempDir, LocalTransport) {
let dir = tempfile::tempdir().unwrap();
let t = LocalTransport::new(dir.path());
(dir, t)
}
#[test]
fn held_info_round_trips() {
let info = LockHeldInfo::for_this_process(HashMap::new());
let bytes = info.to_bytes();
let parsed = LockHeldInfo::from_info_file_bytes(&bytes).unwrap();
assert_eq!(info, parsed);
}
#[test]
fn parses_lock_info_written_by_breezy() {
let real = concat!(
"pid: 1486936\n",
"user: jelmer\n",
"nonce: of7d51l3euf7pcrojq17\n",
"hostname: gwenhwyfar\n",
"start_time:\n",
" secs_since_epoch: 1780304488\n",
" nanos_since_epoch: 949318362\n",
)
.as_bytes();
let info = LockHeldInfo::from_info_file_bytes(real).unwrap();
assert_eq!(info.pid, Some(1486936));
assert_eq!(info.user.as_deref(), Some("jelmer"));
assert_eq!(info.nonce.as_deref(), Some("of7d51l3euf7pcrojq17"));
assert_eq!(info.hostname.as_deref(), Some("gwenhwyfar"));
assert!(info.start_time.is_some());
assert_eq!(
LockHeldInfo::from_info_file_bytes(&info.to_bytes()).unwrap(),
info
);
}
#[test]
fn empty_info_file_is_default() {
let parsed = LockHeldInfo::from_info_file_bytes(b"").unwrap();
assert_eq!(parsed, LockHeldInfo::default());
}
#[test]
fn take_and_release() {
let (_dir, t) = temp_transport();
let mut ld = LockDir::new(&t, "test_lock");
ld.create().unwrap();
assert_eq!(ld.peek().unwrap(), None);
let nonce = ld.attempt_lock().unwrap();
assert!(ld.is_held());
let held = ld.peek().unwrap().unwrap();
assert_eq!(held.nonce.as_deref(), Some(nonce.as_str()));
ld.unlock().unwrap();
assert!(!ld.is_held());
assert_eq!(ld.peek().unwrap(), None);
}
#[test]
fn second_holder_is_blocked() {
let (_dir, t) = temp_transport();
let mut a = LockDir::new(&t, "test_lock");
a.create().unwrap();
a.attempt_lock().unwrap();
let mut b = LockDir::new(&t, "test_lock");
match b.attempt_lock() {
Err(LockError::AlreadyHeld) => {}
other => panic!("expected AlreadyHeld, got {:?}", other.err()),
}
assert!(!b.is_held());
a.unlock().unwrap();
b.attempt_lock().unwrap();
assert!(b.is_held());
}
#[test]
fn drop_releases_lock() {
let (_dir, t) = temp_transport();
{
let mut a = LockDir::new(&t, "test_lock");
a.create().unwrap();
a.attempt_lock().unwrap();
}
let probe = LockDir::new(&t, "test_lock");
assert_eq!(probe.peek().unwrap(), None);
}
struct RenameFails<'a>(&'a dyn Transport);
impl Transport for RenameFails<'_> {
fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError> {
self.0.get_bytes(path)
}
fn put_file_non_atomic(
&self,
path: &str,
bytes: &[u8],
create_parent_dir: bool,
) -> Result<(), TransportError> {
self.0.put_file_non_atomic(path, bytes, create_parent_dir)
}
fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError> {
self.0.append_bytes(path, bytes)
}
fn mkdir(&self, path: &str) -> Result<(), TransportError> {
self.0.mkdir(path)
}
fn has(&self, path: &str) -> Result<bool, TransportError> {
self.0.has(path)
}
fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError> {
self.0.iter_files_recursive()
}
fn abspath(&self, path: &str) -> Result<String, TransportError> {
self.0.abspath(path)
}
fn delete(&self, path: &str) -> Result<(), TransportError> {
self.0.delete(path)
}
fn rmdir(&self, path: &str) -> Result<(), TransportError> {
self.0.rmdir(path)
}
fn rename(&self, _from: &str, _to: &str) -> Result<(), TransportError> {
Err(TransportError::Io {
kind: std::io::ErrorKind::PermissionDenied,
message: "denied".to_string(),
})
}
}
#[test]
fn non_contention_rename_error_is_not_already_held() {
let (_dir, t) = temp_transport();
let probe = LockDir::new(&t, "test_lock");
probe.create().unwrap();
let failing = RenameFails(&t);
let mut lock = LockDir::new(&failing, "test_lock");
match lock.attempt_lock() {
Err(LockError::Transport(TransportError::Io { kind, .. })) => {
assert_eq!(kind, std::io::ErrorKind::PermissionDenied);
}
other => panic!(
"expected the underlying transport error, got {:?}",
other.err()
),
}
assert!(!lock.is_held());
}
}