1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
//! Create ".lock" files atomically on any filesystem.
//!
//! This crate contains support for creating lock files as are used on
//! various UNIX type systems. This is similar to the `lockfile` program
//! from [procmail](http://www.procmail.org) or the `dotlockfile`
//! program from [liblockfile](https://github.com/miquels/liblockfile).
//!
//! They are called ".lock" files, because they are traditionally named
//! the same as the file they are referencing with the extension of
//! `.lock`.
//!
//! The algorithm that is used to create a lock file in an atomic way is
//! as follows:
//!
//! 1. A unique file is created using
//! [`tempfile`](https://docs.rs/tempfile).
//!
//! 2. The destination lock file is created using the `link` system
//! call. This operation is atomic across all filesystems including
//! NFS. The result of this operation is ignored, as success is based on
//! subsequent results.
//!
//! 3. Delete the temporary file.
//!
//! 4. The metadata of the destination is retrieved. If this fails,
//! repeat the process.
//!
//! 5. The metadata of the temporary file and the destination lock file
//! are compared. If they are the same file, then we have successfully
//! locked the file. Return the opened file.
//!
//! 6. If the lock file is stale (older than a configured age), delete
//! the existing lock file and retry immediately.
//!
//! 7. Before retrying, sleep briefly (defaults to 5 seconds).
//!
//! # Examples
//!
//! ```no_run
//! use dotlock::DotlockOptions;
//! use std::time::Duration;
//!
//! let _lock = DotlockOptions::new()
//! .tries(10)
//! .pause(Duration::from_secs(1))
//! .create("database.lock").unwrap();
//! ```
extern crate tempfile;
use std::fs::{remove_file, File, Metadata, Permissions};
use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom, Write};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::{Duration, SystemTime};
use tempfile::Builder;
const DEFAULT_PAUSE: Duration = Duration::from_secs(5);
const DEFAULT_TRIES: usize = 10;
// Do the two Metadata reference the same file?
fn meta_eq(a: &Metadata, b: &Metadata) -> bool {
a.dev() == b.dev() && a.ino() == b.ino()
}
/// A created ".lock" file.
#[derive(Debug)]
pub struct Dotlock {
file: File,
path: Option<PathBuf>,
}
impl Dotlock {
fn create_in(path: &Path, options: DotlockOptions, tempdir: &Path) -> Result<File> {
let mut trynum = 0;
loop {
// Create a unique temporary file in the same directory
let temp = Builder::new().tempfile_in(tempdir)?;
let tempmeta = temp.as_file().metadata()?;
// link temporary file to destination, ignore the result
std::fs::hard_link(temp.path(), &path).ok();
// Drop the temporary file
let temp = temp.into_file();
// stat the destination lock file
let destmeta = match std::fs::metadata(&path) {
Ok(meta) => meta,
Err(_) => continue,
};
// Compare result of stat to temporary file
if meta_eq(&destmeta, &tempmeta) {
if let Some(perm) = options.permissions {
temp.set_permissions(perm)?;
}
break Ok(temp);
}
// Is the existing lock stale?
if let Some(stale_age) = options.stale_age {
let now = SystemTime::now();
if let Ok(modtime) = destmeta.modified() {
if let Ok(age) = now.duration_since(modtime) {
if age >= stale_age {
remove_file(&path).ok();
continue;
}
}
}
}
trynum += 1;
if trynum >= options.tries {
break Err(Error::new(ErrorKind::TimedOut, "Timed out"));
}
// Pause only before retrying
sleep(options.pause);
}
}
fn create_with(path: PathBuf, options: DotlockOptions) -> Result<Self> {
let file = Self::create_in(&path, options, &path.parent().unwrap_or(Path::new(".")))?;
Ok(Self {
file,
path: Some(path),
})
}
/// Attempts to create the named lock file using the default options.
pub fn create<T: Into<PathBuf>>(path: T) -> Result<Self> {
DotlockOptions::new().create(path.into())
}
/// Unlocks the lock by removing the file. The lock will be
/// automatically removed when this `Dotlock` is dropped.
pub fn unlock(&mut self) -> Result<()> {
self.path.take().map_or(Ok(()), |path| remove_file(path))
}
/// Attempts to sync all OS-internal metadata to disk. Calls
/// [`File::sync_all`](https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_all).
pub fn sync_all(&self) -> Result<()> {
self.file.sync_all()
}
/// Attempts to sync all OS-internal data to disk except
/// metadata. Calls
/// [`File::sync_data`](https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_data).
pub fn sync_data(&self) -> Result<()> {
self.file.sync_all()
}
/// Truncates or extends the underlying file, updating the size of
/// this file to become `size`. Calls
/// [`File::set_len`](https://doc.rust-lang.org/std/fs/struct.File.html#method.set_len).
pub fn set_len(&self, size: u64) -> Result<()> {
self.file.set_len(size)
}
/// Queries metadata about the underlying file. Calls
/// [`File::metadata`](https://doc.rust-lang.org/std/fs/struct.File.html#method.metadata).
pub fn metadata(&self) -> Result<Metadata> {
self.file.metadata()
}
/// Changes the permissions on the underlying file. Calls
/// [`File::set_permissions`](https://doc.rust-lang.org/std/fs/struct.File.html#method.set_permissions).
pub fn set_permissions(&self, perm: Permissions) -> Result<()> {
self.file.set_permissions(perm)
}
}
impl Drop for Dotlock {
fn drop(&mut self) {
self.unlock().ok();
}
}
impl Read for Dotlock {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.file.read(buf)
}
}
impl Seek for Dotlock {
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
self.file.seek(pos)
}
}
impl Write for Dotlock {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
self.file.write(buf)
}
fn flush(&mut self) -> Result<()> {
self.file.flush()
}
}
/// Options which can be used to configure how a lock file is created.
///
/// This builder exposes the ability to configure how a lock file is
/// created. The [`Dotlock::create`] method is an alias for the
/// [`create`] method here.
///
/// To use `DotlockOptions`, first call [`new`], then chain calls to
/// methods to set each option required, and finally call [`create`]
/// with the full path of the lock file to create. This will give you a
/// `io::Result` with a [`Dotlock`] inside.
///
/// [`new`]: struct.DotlockOptions.html#method.new
/// [`create`]: struct.DotlockOptions.html#method.create
/// [`Dotlock`]: struct.Dotlock.html
/// [`Dotlock::create`]: struct.Dotlock.html#method.create
///
/// # Examples
///
/// Create a lock file using the defaults:
///
/// ```no_run
/// use dotlock::DotlockOptions;
///
/// DotlockOptions::new().create("database.lock").unwrap();
/// ```
///
/// Create a lock file, but failing immediately if creating it fails,
/// and remove lock files older than 5 minutes.
///
/// ```no_run
/// use dotlock::DotlockOptions;
/// use std::time::Duration;
///
/// DotlockOptions::new()
/// .tries(1)
/// .stale_age(Duration::from_secs(300))
/// .create("database.lock").unwrap();
/// ```
#[derive(Debug)]
pub struct DotlockOptions {
pause: Duration,
tries: usize,
permissions: Option<Permissions>,
stale_age: Option<Duration>,
}
impl DotlockOptions {
/// Create a new set of options.
pub fn new() -> Self {
Self {
pause: DEFAULT_PAUSE,
tries: DEFAULT_TRIES,
permissions: None,
stale_age: None,
}
}
/// Set the time `Dotlock` will pause between attempts to create the
/// lock file. Defaults to 5 seconds.
pub fn pause<T: Into<Duration>>(mut self, pause: T) -> Self {
self.pause = pause.into();
self
}
/// Set the number of times `Dotlock` will try to create the lock
/// file. Defaults to 10 times.
pub fn tries(mut self, tries: usize) -> Self {
self.tries = tries.max(1);
self
}
/// Set the permissions on the newly created lock file. If not set,
/// the lock file permissions will be based on the current umask.
pub fn permissions(mut self, perm: Permissions) -> Self {
self.permissions = Some(perm);
self
}
/// Set the age at which a lock file is considered stale. If not
/// set, the existing file age will not be considered for staleness.
pub fn stale_age<T: Into<Duration>>(mut self, age: T) -> Self {
self.stale_age = Some(age.into());
self
}
/// Create the lock file at `path` with the options in `self`.
pub fn create<T: Into<PathBuf>>(self, path: T) -> Result<Dotlock> {
Dotlock::create_with(path.into(), self)
}
}