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/// Pins immutable data against cooperative exclusive eviction until the file closes.
47pub fn lock_shared(file: &File) -> io::Result<()> {
48    #[cfg(unix)]
49    loop {
50        if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_SH) } == 0 {
51            return Ok(());
52        }
53        let error = io::Error::last_os_error();
54        if error.kind() != io::ErrorKind::Interrupted {
55            return Err(error);
56        }
57    }
58    #[cfg(windows)]
59    {
60        let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
61        let result = unsafe {
62            LockFileEx(
63                file.as_raw_handle() as HANDLE,
64                0,
65                0,
66                u32::MAX,
67                u32::MAX,
68                &mut overlapped,
69            )
70        };
71        if result == 0 {
72            return Err(io::Error::last_os_error());
73        }
74        Ok(())
75    }
76}
77
78/// Attempts to acquire an exclusive process-held lock without blocking.
79///
80/// Returns `Ok(false)` only when another process currently owns the lock.
81pub fn try_lock_exclusive(file: &File) -> io::Result<bool> {
82    lock_exclusive_inner(file, true)
83}
84
85/// Releases an exclusive process-held lock.
86pub fn unlock(file: &File) -> io::Result<()> {
87    #[cfg(unix)]
88    {
89        let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
90        if result != 0 {
91            return Err(io::Error::last_os_error());
92        }
93    }
94
95    #[cfg(windows)]
96    {
97        let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
98        let result = unsafe {
99            UnlockFileEx(
100                file.as_raw_handle() as HANDLE,
101                0,
102                u32::MAX,
103                u32::MAX,
104                &mut overlapped,
105            )
106        };
107        if result == 0 {
108            return Err(io::Error::last_os_error());
109        }
110    }
111
112    Ok(())
113}
114
115#[cfg(unix)]
116fn lock_exclusive_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
117    let operation = if nonblocking {
118        libc::LOCK_EX | libc::LOCK_NB
119    } else {
120        libc::LOCK_EX
121    };
122    let result = unsafe { libc::flock(file.as_raw_fd(), operation) };
123    if result == 0 {
124        return Ok(true);
125    }
126
127    let error = io::Error::last_os_error();
128    if nonblocking
129        && matches!(
130            error.raw_os_error(),
131            Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN
132        )
133    {
134        return Ok(false);
135    }
136    Err(error)
137}
138
139fn open_lock_file_with(path: &Path, create: bool, create_new: bool) -> io::Result<File> {
140    let mut options = OpenOptions::new();
141    options
142        .create(create)
143        .create_new(create_new)
144        .truncate(false)
145        .read(true)
146        .write(true);
147    #[cfg(unix)]
148    options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
149    options.open(path)
150}
151
152#[cfg(windows)]
153fn lock_exclusive_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
154    let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
155    let flags = LOCKFILE_EXCLUSIVE_LOCK
156        | if nonblocking {
157            LOCKFILE_FAIL_IMMEDIATELY
158        } else {
159            0
160        };
161    let result = unsafe {
162        LockFileEx(
163            file.as_raw_handle() as HANDLE,
164            flags,
165            0,
166            u32::MAX,
167            u32::MAX,
168            &mut overlapped,
169        )
170    };
171    if result != 0 {
172        return Ok(true);
173    }
174
175    let error = io::Error::last_os_error();
176    if nonblocking
177        && matches!(
178            error.raw_os_error(),
179            Some(code) if code as u32 == ERROR_LOCK_VIOLATION || code as u32 == ERROR_IO_PENDING
180        )
181    {
182        return Ok(false);
183    }
184    Err(error)
185}
186
187//--------------------------------------------------------------------------------------------------
188// Tests
189//--------------------------------------------------------------------------------------------------
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn process_lock_is_exclusive_and_reusable() {
197        let dir = tempfile::tempdir().unwrap();
198        let path = dir.path().join("lease.lock");
199        let first = open_lock_file(&path).unwrap();
200        let second = open_lock_file(&path).unwrap();
201
202        assert!(try_lock_exclusive(&first).unwrap());
203        assert!(!try_lock_exclusive(&second).unwrap());
204        unlock(&first).unwrap();
205        assert!(try_lock_exclusive(&second).unwrap());
206    }
207}