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/// Locks an immutable descriptor without locking its readable byte range on Windows.
27/// Closing the file releases the marker; callers must not call `unlock` for this lock.
28pub fn lock_descriptor(file: &File, exclusive: bool, nonblocking: bool) -> io::Result<bool> {
29    #[cfg(unix)]
30    {
31        if exclusive {
32            lock_exclusive_inner(file, nonblocking)
33        } else {
34            lock_shared(file).map(|()| true)
35        }
36    }
37    #[cfg(windows)]
38    {
39        // Windows byte locks affect I/O through other handles, unlike flock. Reserve a
40        // marker beyond any descriptor payload so cooperating readers can still read JSON.
41        let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
42        overlapped.Anonymous.Anonymous.Offset = u32::MAX - 1;
43        overlapped.Anonymous.Anonymous.OffsetHigh = u32::MAX;
44        let flags = if exclusive {
45            LOCKFILE_EXCLUSIVE_LOCK
46        } else {
47            0
48        } | if nonblocking {
49            LOCKFILE_FAIL_IMMEDIATELY
50        } else {
51            0
52        };
53        let result = unsafe {
54            LockFileEx(
55                file.as_raw_handle() as HANDLE,
56                flags,
57                0,
58                1,
59                0,
60                &mut overlapped,
61            )
62        };
63        if result != 0 {
64            return Ok(true);
65        }
66        let error = io::Error::last_os_error();
67        if nonblocking
68            && matches!(error.raw_os_error(), Some(code) if code as u32 == ERROR_LOCK_VIOLATION || code as u32 == ERROR_IO_PENDING)
69        {
70            return Ok(false);
71        }
72        Err(error)
73    }
74}
75
76/// Opens or creates an owner-only lock file without truncating it.
77pub fn open_lock_file(path: &Path) -> io::Result<File> {
78    open_lock_file_with(path, true, false)
79}
80
81/// Opens an existing lock file without creating a missing path.
82pub fn open_existing_lock_file(path: &Path) -> io::Result<File> {
83    open_lock_file_with(path, false, false)
84}
85
86/// Creates a new lock file and fails if the path already exists.
87pub fn create_new_lock_file(path: &Path) -> io::Result<File> {
88    open_lock_file_with(path, false, true)
89}
90
91/// Acquires an exclusive process-held lock, blocking until it becomes available.
92pub fn lock_exclusive(file: &File) -> io::Result<()> {
93    lock_exclusive_inner(file, false).map(|_| ())
94}
95
96/// Pins immutable data against cooperative exclusive eviction until the file closes.
97pub fn lock_shared(file: &File) -> io::Result<()> {
98    lock_shared_inner(file, false).map(|_| ())
99}
100
101/// Attempts to acquire a shared process-held lock without blocking.
102///
103/// Returns `Ok(false)` only when another process currently owns an exclusive lock.
104pub fn try_lock_shared(file: &File) -> io::Result<bool> {
105    lock_shared_inner(file, true)
106}
107
108#[cfg(unix)]
109fn lock_shared_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
110    let operation = if nonblocking {
111        libc::LOCK_SH | libc::LOCK_NB
112    } else {
113        libc::LOCK_SH
114    };
115    loop {
116        if unsafe { libc::flock(file.as_raw_fd(), operation) } == 0 {
117            return Ok(true);
118        }
119        let error = io::Error::last_os_error();
120        if error.kind() == io::ErrorKind::Interrupted {
121            continue;
122        }
123        if nonblocking
124            && matches!(
125                error.raw_os_error(),
126                Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN
127            )
128        {
129            return Ok(false);
130        }
131        return Err(error);
132    }
133}
134
135#[cfg(windows)]
136fn lock_shared_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
137    let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
138    let flags = if nonblocking {
139        LOCKFILE_FAIL_IMMEDIATELY
140    } else {
141        0
142    };
143    let result = unsafe {
144        LockFileEx(
145            file.as_raw_handle() as HANDLE,
146            flags,
147            0,
148            u32::MAX,
149            u32::MAX,
150            &mut overlapped,
151        )
152    };
153    if result != 0 {
154        return Ok(true);
155    }
156
157    let error = io::Error::last_os_error();
158    if nonblocking
159        && matches!(
160            error.raw_os_error(),
161            Some(code) if code as u32 == ERROR_LOCK_VIOLATION || code as u32 == ERROR_IO_PENDING
162        )
163    {
164        return Ok(false);
165    }
166    Err(error)
167}
168
169/// Attempts to acquire an exclusive process-held lock without blocking.
170///
171/// Returns `Ok(false)` only when another process currently owns the lock.
172pub fn try_lock_exclusive(file: &File) -> io::Result<bool> {
173    lock_exclusive_inner(file, true)
174}
175
176/// Releases an exclusive process-held lock.
177pub fn unlock(file: &File) -> io::Result<()> {
178    #[cfg(unix)]
179    {
180        let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
181        if result != 0 {
182            return Err(io::Error::last_os_error());
183        }
184    }
185
186    #[cfg(windows)]
187    {
188        let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
189        let result = unsafe {
190            UnlockFileEx(
191                file.as_raw_handle() as HANDLE,
192                0,
193                u32::MAX,
194                u32::MAX,
195                &mut overlapped,
196            )
197        };
198        if result == 0 {
199            return Err(io::Error::last_os_error());
200        }
201    }
202
203    Ok(())
204}
205
206#[cfg(unix)]
207fn lock_exclusive_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
208    let operation = if nonblocking {
209        libc::LOCK_EX | libc::LOCK_NB
210    } else {
211        libc::LOCK_EX
212    };
213    let result = unsafe { libc::flock(file.as_raw_fd(), operation) };
214    if result == 0 {
215        return Ok(true);
216    }
217
218    let error = io::Error::last_os_error();
219    if nonblocking
220        && matches!(
221            error.raw_os_error(),
222            Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN
223        )
224    {
225        return Ok(false);
226    }
227    Err(error)
228}
229
230fn open_lock_file_with(path: &Path, create: bool, create_new: bool) -> io::Result<File> {
231    let mut options = OpenOptions::new();
232    options
233        .create(create)
234        .create_new(create_new)
235        .truncate(false)
236        .read(true)
237        .write(true);
238    #[cfg(unix)]
239    options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
240    options.open(path)
241}
242
243#[cfg(windows)]
244fn lock_exclusive_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
245    let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
246    let flags = LOCKFILE_EXCLUSIVE_LOCK
247        | if nonblocking {
248            LOCKFILE_FAIL_IMMEDIATELY
249        } else {
250            0
251        };
252    let result = unsafe {
253        LockFileEx(
254            file.as_raw_handle() as HANDLE,
255            flags,
256            0,
257            u32::MAX,
258            u32::MAX,
259            &mut overlapped,
260        )
261    };
262    if result != 0 {
263        return Ok(true);
264    }
265
266    let error = io::Error::last_os_error();
267    if nonblocking
268        && matches!(
269            error.raw_os_error(),
270            Some(code) if code as u32 == ERROR_LOCK_VIOLATION || code as u32 == ERROR_IO_PENDING
271        )
272    {
273        return Ok(false);
274    }
275    Err(error)
276}
277
278//--------------------------------------------------------------------------------------------------
279// Tests
280//--------------------------------------------------------------------------------------------------
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn process_lock_is_exclusive_and_reusable() {
288        let dir = tempfile::tempdir().unwrap();
289        let path = dir.path().join("lease.lock");
290        let first = open_lock_file(&path).unwrap();
291        let second = open_lock_file(&path).unwrap();
292
293        assert!(try_lock_exclusive(&first).unwrap());
294        assert!(!try_lock_exclusive(&second).unwrap());
295        unlock(&first).unwrap();
296        assert!(try_lock_exclusive(&second).unwrap());
297    }
298
299    #[test]
300    fn shared_locks_are_compatible_and_exclude_writers() {
301        let dir = tempfile::tempdir().unwrap();
302        let path = dir.path().join("lease.lock");
303        let first_reader = open_lock_file(&path).unwrap();
304        let second_reader = open_lock_file(&path).unwrap();
305        let writer = open_lock_file(&path).unwrap();
306
307        assert!(try_lock_shared(&first_reader).unwrap());
308        assert!(try_lock_shared(&second_reader).unwrap());
309        assert!(!try_lock_exclusive(&writer).unwrap());
310        unlock(&first_reader).unwrap();
311        assert!(!try_lock_exclusive(&writer).unwrap());
312        unlock(&second_reader).unwrap();
313        assert!(try_lock_exclusive(&writer).unwrap());
314    }
315}