use std::fs;
use std::io;
use std::path::Path;
#[cfg(not(unix))]
use std::path::PathBuf;
pub struct Lock {
#[cfg(unix)]
_file: fs::File,
#[cfg(not(unix))]
path: PathBuf,
}
#[cfg(unix)]
const LOCK_MAGIC: &str = "RDARLOCK2";
impl Lock {
pub fn acquire(root: &Path) -> io::Result<Lock> {
let dir = crate::cache::radar_dir(root);
fs::create_dir_all(&dir)?;
let path = dir.join("lock");
acquire_platform(&path)
}
}
#[cfg(unix)]
fn acquire_platform(path: &Path) -> io::Result<Lock> {
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::fd::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
let open = |create_new| {
let mut options = fs::OpenOptions::new();
options
.read(true)
.write(true)
.create_new(create_new)
.custom_flags(O_NOFOLLOW);
options.open(path)
};
let (mut file, created) = match open(true) {
Ok(file) => (file, true),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => (open(false)?, false),
Err(error) => return Err(error),
};
if !file.metadata()?.file_type().is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
".radar/lock is not a regular file",
));
}
let result = unsafe { flock(file.as_raw_fd(), 2 | 4) };
if result != 0 {
let error = io::Error::last_os_error();
return if error.kind() == io::ErrorKind::WouldBlock {
Err(io::Error::new(
io::ErrorKind::WouldBlock,
"another radar run holds the lock (.radar/lock)",
))
} else {
Err(error)
};
}
if !created {
let mut prefix = [0u8; LOCK_MAGIC.len()];
if file.read_exact(&mut prefix).is_err() || prefix != LOCK_MAGIC.as_bytes() {
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
"a pre-upgrade radar lock exists; stop the old writer or remove .radar/lock",
));
}
}
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
write!(file, "{LOCK_MAGIC} {}", std::process::id())?;
Ok(Lock { _file: file })
}
#[cfg(any(target_os = "linux", target_os = "android"))]
const O_NOFOLLOW: std::os::raw::c_int = 0o400000;
#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
const O_NOFOLLOW: std::os::raw::c_int = 0x100;
#[cfg(unix)]
unsafe extern "C" {
fn flock(fd: std::os::raw::c_int, operation: std::os::raw::c_int) -> std::os::raw::c_int;
}
#[cfg(not(unix))]
fn acquire_platform(path: &Path) -> io::Result<Lock> {
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
{
Ok(mut file) => {
use std::io::Write;
write!(file, "{}", std::process::id())?;
Ok(Lock {
path: path.to_path_buf(),
})
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new(
io::ErrorKind::WouldBlock,
"another radar run holds the lock (.radar/lock)",
)),
Err(error) => Err(error),
}
}
#[cfg(not(unix))]
impl Drop for Lock {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exclusive_then_released() {
let dir = std::env::temp_dir().join(format!("radar-lock-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("mkdir");
let first = Lock::acquire(&dir).expect("first lock");
let second = Lock::acquire(&dir);
assert!(matches!(second, Err(ref error) if error.kind() == io::ErrorKind::WouldBlock));
drop(first);
let third = Lock::acquire(&dir).expect("re-acquire after release");
drop(third);
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn existing_unowned_persistent_file_is_reclaimed_without_deletion() {
let dir = std::env::temp_dir().join(format!("radar-dead-lock-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(crate::cache::radar_dir(&dir)).expect("mkdir");
let path = crate::cache::radar_dir(&dir).join("lock");
fs::write(&path, format!("{LOCK_MAGIC} old-owner")).expect("write old lock file");
let lock = Lock::acquire(&dir).expect("kernel lock has no live owner");
assert!(path.exists(), "advisory lock path stays stable");
drop(lock);
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn legacy_lock_is_blocked_and_untouched() {
let dir = std::env::temp_dir().join(format!("radar-legacy-lock-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(crate::cache::radar_dir(&dir)).expect("mkdir");
let path = crate::cache::radar_dir(&dir).join("lock");
fs::write(&path, "12345").expect("write legacy lock");
let result = Lock::acquire(&dir);
assert!(matches!(result, Err(ref error) if error.kind() == io::ErrorKind::WouldBlock));
assert_eq!(fs::read_to_string(&path).unwrap(), "12345");
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn lock_symlink_is_rejected_without_touching_target() {
use std::os::unix::fs::symlink;
let dir = std::env::temp_dir().join(format!("radar-link-lock-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(crate::cache::radar_dir(&dir)).expect("mkdir");
let victim = dir.join("victim");
fs::write(&victim, "keep me").expect("write victim");
symlink(&victim, crate::cache::radar_dir(&dir).join("lock")).expect("link lock");
assert!(Lock::acquire(&dir).is_err());
assert_eq!(fs::read_to_string(&victim).unwrap(), "keep me");
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn oversized_legacy_lock_is_checked_with_a_fixed_prefix() {
let dir = std::env::temp_dir().join(format!("radar-large-lock-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(crate::cache::radar_dir(&dir)).expect("mkdir");
let path = crate::cache::radar_dir(&dir).join("lock");
let file = fs::File::create(&path).expect("create lock");
file.set_len(1 << 30).expect("sparse lock");
let result = Lock::acquire(&dir);
assert!(matches!(result, Err(ref error) if error.kind() == io::ErrorKind::WouldBlock));
assert_eq!(fs::metadata(&path).unwrap().len(), 1 << 30);
let _ = fs::remove_dir_all(&dir);
}
}