use crate::{hash_path, user_forc_directory};
use std::{
fs::{create_dir_all, remove_file, File},
io::{self, Read, Write},
path::{Path, PathBuf},
};
pub struct PidFileLocking(PathBuf);
impl PidFileLocking {
pub fn new<X: AsRef<Path>, Y: AsRef<Path>>(
filename: X,
dir: Y,
extension: &str,
) -> PidFileLocking {
let file_name = hash_path(filename);
Self(
user_forc_directory()
.join(dir)
.join(file_name)
.with_extension(extension),
)
}
pub fn lsp<X: AsRef<Path>>(filename: X) -> PidFileLocking {
Self::new(filename, ".lsp-locks", "lock")
}
#[cfg(not(target = "windows"))]
fn is_pid_active(pid: usize) -> bool {
use std::process::Command;
let output = Command::new("ps")
.arg("-p")
.arg(pid.to_string())
.output()
.expect("Failed to execute ps command");
let output_str = String::from_utf8_lossy(&output.stdout);
output_str.contains(&format!("{} ", pid))
}
#[cfg(target = "windows")]
fn is_pid_active(pid: usize) -> bool {
use std::process::Command;
let output = Command::new("tasklist")
.arg("/FI")
.arg(format!("PID eq {}", pid))
.output()
.expect("Failed to execute tasklist command");
let output_str = String::from_utf8_lossy(&output.stdout);
output_str.contains(&format!("{}", pid))
}
pub fn release(&self) -> io::Result<()> {
if self.is_locked() {
Err(io::Error::new(
std::io::ErrorKind::Other,
format!(
"Cannot remove a dirty lock file, it is locked by another process (PID: {:#?})",
self.get_locker_pid()
),
))
} else {
self.remove_file()?;
Ok(())
}
}
fn remove_file(&self) -> io::Result<()> {
match remove_file(&self.0) {
Err(e) => {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(e);
}
Ok(())
}
_ => Ok(()),
}
}
pub fn get_locker_pid(&self) -> Option<usize> {
let fs = File::open(&self.0);
if let Ok(mut file) = fs {
let mut contents = String::new();
file.read_to_string(&mut contents).ok();
drop(file);
if let Ok(pid) = contents.trim().parse::<usize>() {
return if Self::is_pid_active(pid) {
Some(pid)
} else {
let _ = self.remove_file();
None
};
}
}
None
}
pub fn is_locked(&self) -> bool {
self.get_locker_pid()
.map(|pid| pid != (std::process::id() as usize))
.unwrap_or_default()
}
pub fn lock(&self) -> io::Result<()> {
self.release()?;
if let Some(dir) = self.0.parent() {
create_dir_all(dir)?;
}
let mut fs = File::create(&self.0)?;
fs.write_all(std::process::id().to_string().as_bytes())?;
fs.sync_all()?;
fs.flush()?;
Ok(())
}
}
#[cfg(test)]
mod test {
use super::PidFileLocking;
use std::{
fs::{metadata, File},
io::{ErrorKind, Write},
os::unix::fs::MetadataExt,
};
#[test]
fn test_fs_locking_same_process() {
let x = PidFileLocking::lsp("test");
assert!(!x.is_locked()); assert!(x.lock().is_ok());
let x = PidFileLocking::lsp("test");
assert!(!x.is_locked());
}
#[test]
fn test_legacy() {
let x = PidFileLocking::lsp("legacy");
assert!(x.lock().is_ok());
assert!(metadata(&x.0).is_ok());
let _ = File::create(&x.0).unwrap();
assert_eq!(metadata(&x.0).unwrap().size(), 0);
let x = PidFileLocking::lsp("legacy");
assert!(!x.is_locked());
}
#[test]
fn test_remove() {
let x = PidFileLocking::lsp("lock");
assert!(x.lock().is_ok());
assert!(x.release().is_ok());
assert!(x.release().is_ok());
}
#[test]
fn test_fs_locking_stale() {
let x = PidFileLocking::lsp("stale");
assert!(x.lock().is_ok());
assert!(metadata(&x.0).is_ok());
let mut x = File::create(&x.0).unwrap();
x.write_all(b"191919191919").unwrap();
x.flush().unwrap();
drop(x);
let x = PidFileLocking::lsp("stale");
assert!(!x.is_locked());
let e = metadata(&x.0).unwrap_err().kind();
assert_eq!(e, ErrorKind::NotFound);
}
}