use std::{
fs,
io::{self, Write},
path::{Path, PathBuf},
process,
};
use fs4::{FileExt, TryLockError};
use tracing::warn;
use crate::bootstrap::{open_owner_only_lock, BootstrapError};
pub(crate) const RUN_DIR_LOCK_FILE_NAME: &str = "daemon.lock";
#[derive(Debug)]
pub(crate) struct RunDirLock {
_file: fs::File,
live_children_record: PathBuf,
}
impl RunDirLock {
pub(crate) fn acquire(live_children_record: &Path) -> Result<Self, BootstrapError> {
let run_dir = live_children_record
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let path = run_dir.join(RUN_DIR_LOCK_FILE_NAME);
let file = fs::create_dir_all(run_dir)
.and_then(|()| open_owner_only_lock(&path))
.map_err(|source| BootstrapError::RunDirLockCreate {
path: path.clone(),
source,
})?;
match FileExt::try_lock(&file) {
Ok(()) => {}
Err(TryLockError::WouldBlock) => {
return Err(BootstrapError::RunDirBusy {
holder_pid: read_holder_pid(&path),
path,
});
}
Err(TryLockError::Error(source)) => {
return Err(BootstrapError::RunDirLockCreate { path, source });
}
}
if let Err(error) = record_owner_pid(&file) {
warn!(path = %path.display(), %error, "could not record the run directory owner's pid");
}
Ok(Self {
_file: file,
live_children_record: live_children_record.to_path_buf(),
})
}
pub(crate) fn live_children_record(&self) -> &Path {
&self.live_children_record
}
}
fn record_owner_pid(file: &fs::File) -> io::Result<()> {
file.set_len(0)?;
let mut writer = file;
writer.write_all(format!("{}\n", process::id()).as_bytes())?;
writer.flush()
}
fn read_holder_pid(path: &Path) -> Option<u32> {
fs::read_to_string(path).ok()?.trim().parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
use subc_test_support::TestTempDir;
#[test]
fn a_held_run_dir_refuses_a_second_owner_and_names_the_holder() {
let dir = TestTempDir::new("run-dir-lock-held");
let record = crate::live_children::record_path(&dir);
let first = RunDirLock::acquire(&record).unwrap();
match RunDirLock::acquire(&record) {
Err(BootstrapError::RunDirBusy { path, holder_pid }) => {
assert_eq!(path, dir.join(RUN_DIR_LOCK_FILE_NAME));
#[cfg(unix)]
assert_eq!(holder_pid, Some(process::id()));
#[cfg(not(unix))]
let _ = holder_pid;
}
other => panic!("expected the run directory to be busy, got {other:?}"),
}
drop(first);
RunDirLock::acquire(&record).expect("a released run directory can be taken again");
}
#[test]
fn a_missing_run_dir_is_created() {
let dir = TestTempDir::new("run-dir-lock-missing");
let record = crate::live_children::record_path(&dir.join("run"));
let lock = RunDirLock::acquire(&record).unwrap();
assert_eq!(lock.live_children_record(), record);
assert!(dir.join("run").join(RUN_DIR_LOCK_FILE_NAME).exists());
}
}