4186d117af
## Issue Addressed Closes #3049 This PR updates widely but this replace is safe as `File::options()` is equivelent to `OpenOptions::new()`. ref: https://doc.rust-lang.org/stable/src/std/fs.rs.html#378-380
141 lines
3.9 KiB
Rust
141 lines
3.9 KiB
Rust
use fs2::FileExt;
|
|
use std::fs::{self, File};
|
|
use std::io::{self, ErrorKind};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Cross-platform file lock that auto-deletes on drop.
|
|
///
|
|
/// This lockfile uses OS locking primitives (`flock` on Unix, `LockFile` on Windows), and will
|
|
/// only fail if locked by another process. I.e. if the file being locked already exists but isn't
|
|
/// locked, then it can still be locked. This is relevant if an ungraceful shutdown (SIGKILL, power
|
|
/// outage) caused the lockfile not to be deleted.
|
|
#[derive(Debug)]
|
|
pub struct Lockfile {
|
|
_file: File,
|
|
path: PathBuf,
|
|
file_existed: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum LockfileError {
|
|
FileLocked(PathBuf, io::Error),
|
|
IoError(PathBuf, io::Error),
|
|
UnableToOpenFile(PathBuf, io::Error),
|
|
}
|
|
|
|
impl Lockfile {
|
|
/// Obtain an exclusive lock on the file at `path`, creating it if it doesn't exist.
|
|
pub fn new(path: PathBuf) -> Result<Self, LockfileError> {
|
|
let file_existed = path.exists();
|
|
let file = if file_existed {
|
|
File::open(&path)
|
|
} else {
|
|
File::options()
|
|
.read(true)
|
|
.write(true)
|
|
.create_new(true)
|
|
.open(&path)
|
|
}
|
|
.map_err(|e| LockfileError::UnableToOpenFile(path.clone(), e))?;
|
|
|
|
file.try_lock_exclusive().map_err(|e| match e.kind() {
|
|
ErrorKind::WouldBlock => LockfileError::FileLocked(path.clone(), e),
|
|
_ => LockfileError::IoError(path.clone(), e),
|
|
})?;
|
|
Ok(Self {
|
|
_file: file,
|
|
path,
|
|
file_existed,
|
|
})
|
|
}
|
|
|
|
/// Return `true` if the lockfile existed when the lock was created.
|
|
///
|
|
/// This could indicate another process that isn't aware of the OS lock using the file,
|
|
/// or an ungraceful shutdown that caused the file not to be deleted.
|
|
pub fn file_existed(&self) -> bool {
|
|
self.file_existed
|
|
}
|
|
|
|
/// The path of the lockfile.
|
|
pub fn path(&self) -> &Path {
|
|
&self.path
|
|
}
|
|
}
|
|
|
|
impl Drop for Lockfile {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_file(&self.path);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use tempfile::tempdir;
|
|
|
|
#[cfg(unix)]
|
|
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
|
|
|
|
#[test]
|
|
fn new_lock() {
|
|
let temp = tempdir().unwrap();
|
|
let path = temp.path().join("lockfile");
|
|
let _lock = Lockfile::new(path.clone()).unwrap();
|
|
if cfg!(windows) {
|
|
assert!(matches!(
|
|
Lockfile::new(path).unwrap_err(),
|
|
// windows returns an IoError because the lockfile is already open :/
|
|
LockfileError::IoError(..),
|
|
));
|
|
} else {
|
|
assert!(matches!(
|
|
Lockfile::new(path).unwrap_err(),
|
|
LockfileError::FileLocked(..)
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn relock_after_drop() {
|
|
let temp = tempdir().unwrap();
|
|
let path = temp.path().join("lockfile");
|
|
|
|
let lock1 = Lockfile::new(path.clone()).unwrap();
|
|
drop(lock1);
|
|
let lock2 = Lockfile::new(path.clone()).unwrap();
|
|
assert!(!lock2.file_existed());
|
|
drop(lock2);
|
|
|
|
assert!(!path.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn lockfile_exists() {
|
|
let temp = tempdir().unwrap();
|
|
let path = temp.path().join("lockfile");
|
|
|
|
let _lockfile = File::create(&path).unwrap();
|
|
|
|
let lock = Lockfile::new(path).unwrap();
|
|
assert!(lock.file_existed());
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn permission_denied_create() {
|
|
let temp = tempdir().unwrap();
|
|
let path = temp.path().join("lockfile");
|
|
|
|
let lockfile = File::create(&path).unwrap();
|
|
lockfile
|
|
.set_permissions(Permissions::from_mode(0o000))
|
|
.unwrap();
|
|
|
|
assert!(matches!(
|
|
Lockfile::new(path).unwrap_err(),
|
|
LockfileError::UnableToOpenFile(..)
|
|
));
|
|
}
|
|
}
|