use std::{
fs,
io::{Seek, Write},
os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt},
path::Path,
};
use crate::domain::errors::{AgentError, AgentResult, ErrorCode};
pub(super) const PRIVATE_FILE_MODE: u32 = 0o600;
pub(crate) struct RuntimeLock {
_file: fs::File,
}
impl std::fmt::Debug for RuntimeLock {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("RuntimeLock { redacted }")
}
}
impl RuntimeLock {
pub(crate) fn acquire(path: &Path) -> AgentResult<Self> {
let parent = path.parent().ok_or_else(lock_unavailable)?;
fs::create_dir_all(parent).map_err(|_| lock_unavailable())?;
let mut options = fs::OpenOptions::new();
options
.read(true)
.write(true)
.create(true)
.mode(PRIVATE_FILE_MODE)
.custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
let mut file = options.open(path).map_err(|_| lock_unavailable())?;
validate_lock_file(&file)?;
match rustix::fs::flock(&file, rustix::fs::FlockOperation::NonBlockingLockExclusive) {
Ok(()) => {
file.set_len(0).map_err(|_| lock_unavailable())?;
file.rewind().map_err(|_| lock_unavailable())?;
writeln!(file, "{}", std::process::id()).map_err(|_| lock_unavailable())?;
file.sync_data().map_err(|_| lock_unavailable())?;
Ok(Self { _file: file })
}
Err(error)
if error == rustix::io::Errno::WOULDBLOCK || error == rustix::io::Errno::AGAIN =>
{
Err(already_running())
}
Err(_) => Err(lock_unavailable()),
}
}
}
pub(super) fn open_existing_lock(path: &Path) -> AgentResult<Option<fs::File>> {
let mut options = fs::OpenOptions::new();
options
.read(true)
.write(true)
.custom_flags(rustix::fs::OFlags::NOFOLLOW.bits() as i32);
match options.open(path) {
Ok(file) => {
validate_lock_file(&file)?;
Ok(Some(file))
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(_) => Err(lock_unavailable()),
}
}
pub(super) fn validate_lock_file(file: &fs::File) -> AgentResult<()> {
let metadata = file.metadata().map_err(|_| lock_unavailable())?;
if !metadata.is_file()
|| metadata.permissions().mode() & 0o777 != PRIVATE_FILE_MODE
|| metadata.nlink() != 1
|| metadata.uid() != rustix::process::getuid().as_raw()
{
return Err(lock_unavailable());
}
Ok(())
}
fn already_running() -> AgentError {
AgentError::new(
ErrorCode::InvalidMessage,
"PC Agent server is already running for this data directory",
)
}
pub(super) fn lock_unavailable() -> AgentError {
AgentError::new(
ErrorCode::InvalidMessage,
"PC Agent server lock is unavailable",
)
}