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    #[cfg(unix)]
99    loop {
100        if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_SH) } == 0 {
101            return Ok(());
102        }
103        let error = io::Error::last_os_error();
104        if error.kind() != io::ErrorKind::Interrupted {
105            return Err(error);
106        }
107    }
108    #[cfg(windows)]
109    {
110        let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
111        let result = unsafe {
112            LockFileEx(
113                file.as_raw_handle() as HANDLE,
114                0,
115                0,
116                u32::MAX,
117                u32::MAX,
118                &mut overlapped,
119            )
120        };
121        if result == 0 {
122            return Err(io::Error::last_os_error());
123        }
124        Ok(())
125    }
126}
127
128/// Attempts to acquire an exclusive process-held lock without blocking.
129///
130/// Returns `Ok(false)` only when another process currently owns the lock.
131pub fn try_lock_exclusive(file: &File) -> io::Result<bool> {
132    lock_exclusive_inner(file, true)
133}
134
135/// Releases an exclusive process-held lock.
136pub fn unlock(file: &File) -> io::Result<()> {
137    #[cfg(unix)]
138    {
139        let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
140        if result != 0 {
141            return Err(io::Error::last_os_error());
142        }
143    }
144
145    #[cfg(windows)]
146    {
147        let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
148        let result = unsafe {
149            UnlockFileEx(
150                file.as_raw_handle() as HANDLE,
151                0,
152                u32::MAX,
153                u32::MAX,
154                &mut overlapped,
155            )
156        };
157        if result == 0 {
158            return Err(io::Error::last_os_error());
159        }
160    }
161
162    Ok(())
163}
164
165#[cfg(unix)]
166fn lock_exclusive_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
167    let operation = if nonblocking {
168        libc::LOCK_EX | libc::LOCK_NB
169    } else {
170        libc::LOCK_EX
171    };
172    let result = unsafe { libc::flock(file.as_raw_fd(), operation) };
173    if result == 0 {
174        return Ok(true);
175    }
176
177    let error = io::Error::last_os_error();
178    if nonblocking
179        && matches!(
180            error.raw_os_error(),
181            Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN
182        )
183    {
184        return Ok(false);
185    }
186    Err(error)
187}
188
189fn open_lock_file_with(path: &Path, create: bool, create_new: bool) -> io::Result<File> {
190    let mut options = OpenOptions::new();
191    options
192        .create(create)
193        .create_new(create_new)
194        .truncate(false)
195        .read(true)
196        .write(true);
197    #[cfg(unix)]
198    options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
199    options.open(path)
200}
201
202#[cfg(windows)]
203fn lock_exclusive_inner(file: &File, nonblocking: bool) -> io::Result<bool> {
204    let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
205    let flags = LOCKFILE_EXCLUSIVE_LOCK
206        | if nonblocking {
207            LOCKFILE_FAIL_IMMEDIATELY
208        } else {
209            0
210        };
211    let result = unsafe {
212        LockFileEx(
213            file.as_raw_handle() as HANDLE,
214            flags,
215            0,
216            u32::MAX,
217            u32::MAX,
218            &mut overlapped,
219        )
220    };
221    if result != 0 {
222        return Ok(true);
223    }
224
225    let error = io::Error::last_os_error();
226    if nonblocking
227        && matches!(
228            error.raw_os_error(),
229            Some(code) if code as u32 == ERROR_LOCK_VIOLATION || code as u32 == ERROR_IO_PENDING
230        )
231    {
232        return Ok(false);
233    }
234    Err(error)
235}
236
237//--------------------------------------------------------------------------------------------------
238// Tests
239//--------------------------------------------------------------------------------------------------
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn process_lock_is_exclusive_and_reusable() {
247        let dir = tempfile::tempdir().unwrap();
248        let path = dir.path().join("lease.lock");
249        let first = open_lock_file(&path).unwrap();
250        let second = open_lock_file(&path).unwrap();
251
252        assert!(try_lock_exclusive(&first).unwrap());
253        assert!(!try_lock_exclusive(&second).unwrap());
254        unlock(&first).unwrap();
255        assert!(try_lock_exclusive(&second).unwrap());
256    }
257}