use anyhow::{Context, Result, bail};
use std::io::{Read, Seek, Write};
use std::os::fd::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
pub const HOST_LOCK: &str = "host.lock";
pub const UP_LOCK: &str = "up.lock";
#[derive(Debug)]
pub struct Lock {
_file: std::fs::File,
}
pub fn acquire(dir: &Path, name: &str, command: &str) -> Result<Lock> {
let path = dir.join(name);
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(&path)
.with_context(|| format!("open {}", path.display()))?;
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EWOULDBLOCK) {
let mut holder = String::new();
let _ = file.read_to_string(&mut holder);
let holder = match holder.trim().parse::<u32>() {
Ok(pid) => format!("pid {pid}"),
Err(_) => "another process".to_string(),
};
bail!(
"{} is held by {holder}: another `{command}` serves this data directory",
path.display()
);
}
return Err(err).with_context(|| format!("lock {}", path.display()));
}
file.set_len(0)
.with_context(|| format!("truncate {}", path.display()))?;
file.rewind()?;
writeln!(file, "{}", std::process::id()).with_context(|| format!("write {}", path.display()))?;
Ok(Lock { _file: file })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_held_lock_refuses_a_second_holder_until_it_is_dropped() {
let dir = tempfile::tempdir().unwrap();
let first = acquire(dir.path(), HOST_LOCK, "hennery host run").unwrap();
let text = std::fs::read_to_string(dir.path().join(HOST_LOCK)).unwrap();
assert_eq!(text.trim(), std::process::id().to_string());
let err = acquire(dir.path(), HOST_LOCK, "hennery host run")
.unwrap_err()
.to_string();
assert!(err.contains("host.lock"), "{err}");
assert!(err.contains(&format!("pid {}", std::process::id())), "{err}");
assert!(err.contains("another `hennery host run`"), "{err}");
let text = std::fs::read_to_string(dir.path().join(HOST_LOCK)).unwrap();
assert_eq!(text.trim(), std::process::id().to_string());
drop(first);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while let Err(err) = acquire(dir.path(), HOST_LOCK, "hennery host run") {
assert!(std::time::Instant::now() < deadline, "{err}");
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
#[test]
fn the_lock_is_not_inherited_and_is_private() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let lock = acquire(dir.path(), UP_LOCK, "hennery up").unwrap();
let flags = unsafe { libc::fcntl(lock._file.as_raw_fd(), libc::F_GETFD) };
assert!(flags >= 0 && flags & libc::FD_CLOEXEC != 0, "flags {flags}");
let mode = std::fs::metadata(dir.path().join(UP_LOCK))
.unwrap()
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600, "{mode:o}");
}
}