Skip to main content

async_fs_io/
lock.rs

1//! Async advisory file locking.
2
3use std::path::Path;
4
5use crate::FsError;
6
7/// An acquired non-blocking exclusive advisory lock.
8///
9/// The operating-system lock is released when this value is dropped. The
10/// descriptor is intentionally private so callers cannot reintroduce direct
11/// synchronous filesystem access around the lock boundary.
12#[derive(Debug)]
13pub struct ExclusiveLock {
14    _file: std::fs::File,
15}
16
17/// Acquire a non-blocking exclusive advisory lock on `path`.
18///
19/// The open and lock syscalls run on Tokio's blocking pool because the
20/// portable advisory-lock APIs are synchronous OS primitives. The returned
21/// guard keeps the descriptor alive until it is dropped.
22pub async fn acquire_exclusive_lock(path: impl AsRef<Path>) -> Result<ExclusiveLock, FsError> {
23    let path = path.as_ref().to_owned();
24    let task_path = path.clone();
25    tokio::task::spawn_blocking(move || acquire_blocking(&task_path))
26        .await
27        .map_err(|error| FsError::Lock {
28            path: path.display().to_string(),
29            detail: format!("lock task failed: {error}"),
30        })?
31}
32
33fn acquire_blocking(path: &Path) -> Result<ExclusiveLock, FsError> {
34    let file = open_lock_file(path).map_err(|error| FsError::Lock {
35        path: path.display().to_string(),
36        detail: format!("failed to open lock file: {error}"),
37    })?;
38
39    ensure_lock_file_permissions(path);
40    fs2::FileExt::try_lock_exclusive(&file).map_err(|error| FsError::Lock {
41        path: path.display().to_string(),
42        detail: format!("already held by another process: {error}"),
43    })?;
44    Ok(ExclusiveLock { _file: file })
45}
46
47fn open_lock_file(path: &Path) -> Result<std::fs::File, std::io::Error> {
48    let mut write_options = std::fs::OpenOptions::new();
49    write_options
50        .create(true)
51        .truncate(false)
52        .read(true)
53        .write(true);
54    #[cfg(unix)]
55    std::os::unix::fs::OpenOptionsExt::mode(&mut write_options, 0o664);
56
57    match write_options.open(path) {
58        Ok(file) => Ok(file),
59        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
60            let mut read_options = std::fs::OpenOptions::new();
61            read_options.read(true).open(path)
62        }
63        Err(error) => Err(error),
64    }
65}
66
67#[cfg(unix)]
68fn ensure_lock_file_permissions(path: &Path) {
69    use std::os::unix::fs::PermissionsExt;
70
71    let permissions = std::fs::Permissions::from_mode(0o664);
72    let _ = std::fs::set_permissions(path, permissions);
73}
74
75#[cfg(not(unix))]
76fn ensure_lock_file_permissions(_path: &Path) {}