use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use fs2::FileExt;
use crate::{Error, Result};
const LOCK_FILE_NAME: &str = ".zds_write.lock";
#[derive(Debug)]
pub struct LockInfo {
pub pid: u32,
pub hostname: String,
pub timestamp: String,
}
impl LockInfo {
fn current() -> Self {
LockInfo {
pid: std::process::id(),
hostname: hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string()),
timestamp: chrono::Utc::now().to_rfc3339(),
}
}
fn serialize(&self) -> String {
format!(
"pid={}\nhostname={}\ntimestamp={}\n",
self.pid, self.hostname, self.timestamp
)
}
fn deserialize(content: &str) -> Option<Self> {
let mut pid = None;
let mut hostname = None;
let mut timestamp = None;
for line in content.lines() {
if let Some(val) = line.strip_prefix("pid=") {
pid = val.parse().ok();
} else if let Some(val) = line.strip_prefix("hostname=") {
hostname = Some(val.to_string());
} else if let Some(val) = line.strip_prefix("timestamp=") {
timestamp = Some(val.to_string());
}
}
Some(LockInfo {
pid: pid?,
hostname: hostname?,
timestamp: timestamp?,
})
}
}
pub struct WriteLock {
#[allow(dead_code)]
file: File,
lock_path: PathBuf,
}
impl WriteLock {
pub fn acquire(root: &Path) -> Result<Self> {
let metadata_dir = root.join(".zds");
std::fs::create_dir_all(&metadata_dir)?;
let lock_path = metadata_dir.join(LOCK_FILE_NAME);
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&lock_path)?;
match file.try_lock_exclusive() {
Ok(()) => {
Self::write_lock_info(&lock_path)?;
Ok(WriteLock { file, lock_path })
}
Err(_) => {
let existing = Self::read_lock_info(&lock_path);
let msg = if let Some(info) = existing {
format!(
"ZDS store is locked by another process (pid={}, host={}, since={})",
info.pid, info.hostname, info.timestamp
)
} else {
"ZDS store is locked by another process".to_string()
};
Err(Error::WriteLock(msg))
}
}
}
pub fn release(self) {
drop(self);
}
fn write_lock_info(path: &Path) -> Result<()> {
let info = LockInfo::current();
let mut file = File::create(path)?;
file.write_all(info.serialize().as_bytes())?;
file.sync_all()?;
Ok(())
}
fn read_lock_info(path: &Path) -> Option<LockInfo> {
let mut file = File::open(path).ok()?;
let mut content = String::new();
file.read_to_string(&mut content).ok()?;
LockInfo::deserialize(&content)
}
}
impl Drop for WriteLock {
fn drop(&mut self) {
let _ = self.file.unlock();
let _ = std::fs::remove_file(&self.lock_path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_acquire_release() {
let tmp = TempDir::new().unwrap();
let lock = WriteLock::acquire(tmp.path()).unwrap();
let lock_path = tmp.path().join(".zds").join(LOCK_FILE_NAME);
assert!(lock_path.exists());
lock.release();
assert!(!lock_path.exists());
}
#[test]
fn test_double_lock_fails() {
let tmp = TempDir::new().unwrap();
let _lock1 = WriteLock::acquire(tmp.path()).unwrap();
let result = WriteLock::acquire(tmp.path());
assert!(result.is_err());
if let Err(Error::WriteLock(msg)) = result {
assert!(msg.contains("locked by another process"));
} else {
panic!("Expected WriteLock error");
}
}
#[test]
fn test_lock_info_serialization() {
let info = LockInfo {
pid: 12345,
hostname: "testhost".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
};
let serialized = info.serialize();
let deserialized = LockInfo::deserialize(&serialized).unwrap();
assert_eq!(deserialized.pid, 12345);
assert_eq!(deserialized.hostname, "testhost");
assert_eq!(deserialized.timestamp, "2025-01-01T00:00:00Z");
}
}