Skip to main content

microsandbox_utils/
process_lock.rs

1//! Process-held cross-platform file locks.
2
3use std::fs::{File, OpenOptions};
4use std::io;
5#[cfg(unix)]
6use std::os::fd::AsRawFd;
7#[cfg(unix)]
8use std::os::unix::fs::OpenOptionsExt;
9#[cfg(windows)]
10use std::os::windows::io::AsRawHandle;
11use std::path::Path;
12
13#[cfg(windows)]
14use windows_sys::Win32::Foundation::{ERROR_IO_PENDING, ERROR_LOCK_VIOLATION, HANDLE};
15#[cfg(windows)]
16use windows_sys::Win32::Storage::FileSystem::{
17    LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx, UnlockFileEx,
18};
19#[cfg(windows)]
20use windows_sys::Win32::System::IO::OVERLAPPED;
21
22//--------------------------------------------------------------------------------------------------
23// Functions
24//--------------------------------------------------------------------------------------------------
25
26/// Opens or creates an owner-only lock file without truncating it.
27pub fn open_lock_file(path: &Path) -> io::Result<File> {
28    open_lock_file_with(path, true, false)
29}
30
31/// Opens an existing lock file without creating a missing path.
32pub fn open_existing_lock_file(path: &Path) -> io::Result<File> {
33    open_lock_file_with(path, false, false)
34}
35
36/// Creates a new lock file and fails if the path already exists.
37pub fn create_new_lock_file(path: &Path) -> io::Result<File> {
38    open_lock_file_with(path, false, true)
39}
40
41/// Acquires an exclusive process-held lock, blocking until it becomes available.
42pub fn lock_exclusive(file: &File) -> io::Result<()> {
43    lock_exclusive_inner(file, false).map(|_| ())
44}
45
46/// Attempts to acquire an exclusive process-held lock without blocking.
47///
48/// Returns `Ok(false)` only when another process currently owns the lock.
49pub fn try_lock_exclusive(file: &File) -> io::Result<bool> {
50    lock_exclusive_inner(file, true)
51}
52
53/// Releases an exclusive process-held lock.
54pub fn unlock(file: &File) -> io::Result<()> {
55    #[cfg(unix)]
56    {
57        let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
58        if result != 0 {
59            return Err(io::Error::last_os_error());
60        }
61    }
62
63    #[cfg(windows)]
64    {
65        let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
66        let result = unsafe {
67            UnlockFileEx(
68                file.as_raw_handle() as HANDLE,
69                0,
70                u32::MAX,
71                u32::MAX,
72                &mut overlapped,
73            )
74        };
75        if result == 0 {
76            return Err(io::Error::last_os_error());
77        }
78    }
79
80    Ok(())
81}
82
83#[cfg(unix)]
84fn lock_exclusive_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
85    let operation = if nonblocking {
86        libc::LOCK_EX | libc::LOCK_NB
87    } else {
88        libc::LOCK_EX
89    };
90    let result = unsafe { libc::flock(file.as_raw_fd(), operation) };
91    if result == 0 {
92        return Ok(true);
93    }
94
95    let error = io::Error::last_os_error();
96    if nonblocking
97        && matches!(
98            error.raw_os_error(),
99            Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN
100        )
101    {
102        return Ok(false);
103    }
104    Err(error)
105}
106
107fn open_lock_file_with(path: &Path, create: bool, create_new: bool) -> io::Result<File> {
108    let mut options = OpenOptions::new();
109    options
110        .create(create)
111        .create_new(create_new)
112        .truncate(false)
113        .read(true)
114        .write(true);
115    #[cfg(unix)]
116    options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
117    options.open(path)
118}
119
120#[cfg(windows)]
121fn lock_exclusive_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
122    let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
123    let flags = LOCKFILE_EXCLUSIVE_LOCK
124        | if nonblocking {
125            LOCKFILE_FAIL_IMMEDIATELY
126        } else {
127            0
128        };
129    let result = unsafe {
130        LockFileEx(
131            file.as_raw_handle() as HANDLE,
132            flags,
133            0,
134            u32::MAX,
135            u32::MAX,
136            &mut overlapped,
137        )
138    };
139    if result != 0 {
140        return Ok(true);
141    }
142
143    let error = io::Error::last_os_error();
144    if nonblocking
145        && matches!(
146            error.raw_os_error(),
147            Some(code) if code as u32 == ERROR_LOCK_VIOLATION || code as u32 == ERROR_IO_PENDING
148        )
149    {
150        return Ok(false);
151    }
152    Err(error)
153}
154
155//--------------------------------------------------------------------------------------------------
156// Tests
157//--------------------------------------------------------------------------------------------------
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn process_lock_is_exclusive_and_reusable() {
165        let dir = tempfile::tempdir().unwrap();
166        let path = dir.path().join("lease.lock");
167        let first = open_lock_file(&path).unwrap();
168        let second = open_lock_file(&path).unwrap();
169
170        assert!(try_lock_exclusive(&first).unwrap());
171        assert!(!try_lock_exclusive(&second).unwrap());
172        unlock(&first).unwrap();
173        assert!(try_lock_exclusive(&second).unwrap());
174    }
175}