use {
std::{
io,
mem::forget,
num::ParseIntError,
path::{
Path,
PathBuf,
},
sync::Arc,
thread,
time::Duration,
},
sysinfo::{
Pid,
ProcessRefreshKind,
ProcessesToUpdate,
},
thiserror::Error,
tokio::{
fs,
time::sleep,
},
};
#[must_use = "should call the drop_async method to unlock"]
pub struct DirLock(PathBuf);
#[derive(Debug, Error, Clone)]
#[allow(missing_docs)]
pub enum Error {
#[error("I/O error{}: {}", if let Some(path) = .1 { format!(" at {}", path.display()) } else { String::default() }, .0)] Io(#[source] Arc<io::Error>, Option<PathBuf>),
#[error(transparent)] ParseInt(#[from] ParseIntError),
}
trait IoResultExt {
type T;
fn at(self, path: impl AsRef<Path>) -> Self::T;
}
impl IoResultExt for io::Error {
type T = Error;
fn at(self, path: impl AsRef<Path>) -> Error {
Error::Io(Arc::new(self), Some(path.as_ref().to_owned()))
}
}
impl<T, E: IoResultExt> IoResultExt for Result<T, E> {
type T = Result<T, E::T>;
fn at(self, path: impl AsRef<Path>) -> Result<T, E::T> {
self.map_err(|e| e.at(path))
}
}
impl DirLock {
pub async fn new(path: impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref().to_owned();
loop {
match fs::create_dir(&path).await {
Ok(()) => {
let pidfile = path.join("pid");
fs::write(&pidfile, format!("{}\n", std::process::id())).await.at(pidfile)?;
return Ok(Self(path))
}
Err(e) => match e.kind() {
io::ErrorKind::AlreadyExists => {
let pidfile = path.join("pid");
if match fs::read_to_string(&pidfile).await {
Ok(buf) => {
!buf.is_empty() && !pid_exists(buf.trim().parse()?)
}
Err(e) => if e.kind() == io::ErrorKind::NotFound {
false
} else {
return Err(e.at(path.join("pid")))
},
} {
clean_up_path(&path).await?;
}
sleep(Duration::from_secs(1)).await;
continue
}
_ => return Err(e.at(path)),
},
}
}
}
pub fn new_sync(path: &impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref().to_owned();
loop {
match std::fs::create_dir(&path) {
Ok(()) => {
let pidfile = path.join("pid");
std::fs::write(&pidfile, format!("{}\n", std::process::id())).at(pidfile)?;
return Ok(Self(path))
}
Err(e) => match e.kind() {
io::ErrorKind::AlreadyExists => {
let pidfile = path.join("pid");
if match std::fs::read_to_string(&pidfile) {
Ok(buf) => {
!buf.is_empty() && !pid_exists(buf.trim().parse()?)
}
Err(e) => if e.kind() == io::ErrorKind::NotFound {
false
} else {
return Err(e.at(path.join("pid")))
},
} {
clean_up_path_sync(&path)?;
}
thread::sleep(Duration::from_secs(1));
continue
}
_ => return Err(e.at(path)),
},
}
}
}
pub fn path(&self) -> &Path {
self.0.as_path()
}
pub async fn drop_async(self) -> Result<(), Error> {
self.clean_up().await?;
forget(self);
Ok(())
}
async fn clean_up(&self) -> Result<(), Error> {
clean_up_path(&self.0).await
}
fn clean_up_sync(&self) -> Result<(), Error> {
clean_up_path_sync(&self.0)
}
}
impl Drop for DirLock {
fn drop(&mut self) {
self.clean_up_sync().expect("failed to clean up dir lock");
}
}
async fn clean_up_path(path: &Path) -> Result<(), Error> {
if let Err(e) = fs::remove_file(path.join("pid")).await {
if e.kind() != io::ErrorKind::NotFound {
return Err(e.at(path.join("pid")));
}
}
if let Err(e) = fs::remove_dir(path).await {
if e.kind() != io::ErrorKind::NotFound {
return Err(e.at(path))
}
}
Ok(())
}
fn clean_up_path_sync(path: &Path) -> Result<(), Error> {
if let Err(e) = std::fs::remove_file(path.join("pid")) {
if e.kind() != io::ErrorKind::NotFound {
return Err(e.at(path.join("pid")))
}
}
if let Err(e) = std::fs::remove_dir(path) {
if e.kind() != io::ErrorKind::NotFound {
return Err(e.at(path))
}
}
Ok(())
}
fn pid_exists(pid: Pid) -> bool {
let mut system = sysinfo::System::default();
system.refresh_processes_specifics(ProcessesToUpdate::Some(&[pid]), true, ProcessRefreshKind::default()) > 0
}