nickel_lang_flock/
lib.rs

1//! File-locking support.
2//!
3//! This was extracted from
4//! [Cargo](https://github.com/rust-lang/cargo/blob/511e3ab9458ea6a6fa6a3eadc0b46c05829b0243/src/cargo/util/flock.rs),
5//! so presumably the platform-dependent parts have been tested.
6
7use std::fs::{File, OpenOptions};
8use std::io;
9use std::path::{Path, PathBuf};
10
11use sys::*;
12
13/// A locked file.
14///
15/// Locks are either shared (multiple processes can access the file) or
16/// exclusive (only one process can access the file).
17///
18/// When this value is dropped, the lock will be released.
19#[derive(Debug)]
20pub struct FileLock {
21    f: Option<File>,
22    path: PathBuf,
23}
24
25impl Drop for FileLock {
26    fn drop(&mut self) {
27        if let Some(f) = self.f.take() {
28            if let Err(e) = unlock(&f) {
29                eprintln!("failed to release lock {}: {e:?}", self.path.display());
30            }
31        }
32    }
33}
34
35fn open(path: &Path, opts: &OpenOptions, create: bool) -> io::Result<File> {
36    let f = opts.open(path).or_else(|e| {
37        // If we were requested to create this file, and there was a
38        // NotFound error, then that was likely due to missing
39        // intermediate directories. Try creating them and try again.
40        if e.kind() == io::ErrorKind::NotFound && create {
41            std::fs::create_dir_all(path.parent().unwrap())?;
42            Ok(opts.open(path)?)
43        } else {
44            Err(e)
45        }
46    })?;
47    Ok(f)
48}
49
50pub fn open_rw_exclusive_create<P>(path: P, msg: &str) -> io::Result<FileLock>
51where
52    P: AsRef<Path>,
53{
54    let mut opts = OpenOptions::new();
55    let path = path.as_ref();
56    opts.read(true).write(true).create(true);
57    let f = open(path, &opts, true)?;
58    acquire(msg, path, &|| try_lock_exclusive(&f), &|| {
59        lock_exclusive(&f)
60    })?;
61    Ok(FileLock {
62        f: Some(f),
63        path: path.to_owned(),
64    })
65}
66
67pub fn open_ro_shared_create<P: AsRef<Path>>(path: P, msg: &str) -> std::io::Result<FileLock> {
68    let mut opts = OpenOptions::new();
69    let path = path.as_ref();
70    opts.read(true).write(true).create(true);
71    let f = open(path, &opts, true)?;
72    acquire(msg, path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
73    Ok(FileLock {
74        f: Some(f),
75        path: path.to_owned(),
76    })
77}
78
79fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> std::io::Result<bool> {
80    // File locking on Unix is currently implemented via `flock`, which is known
81    // to be broken on NFS. We could in theory just ignore errors that happen on
82    // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
83    // forever**, even if the "non-blocking" flag is passed!
84    //
85    // As a result, we just skip all file locks entirely on NFS mounts. That
86    // should avoid calling any `flock` functions at all, and it wouldn't work
87    // there anyway.
88    //
89    // [1]: https://github.com/rust-lang/cargo/issues/2615
90    if is_on_nfs_mount(path) {
91        eprintln!("{path:?} appears to be an NFS mount, not trying to lock");
92        return Ok(true);
93    }
94
95    match lock_try() {
96        Ok(()) => return Ok(true),
97
98        // In addition to ignoring NFS which is commonly not working we also
99        // just ignore locking on filesystems that look like they don't
100        // implement file locking.
101        Err(e) if error_unsupported(&e) => return Ok(true),
102
103        Err(e) => {
104            if !error_contended(&e) {
105                //let cx = format!("failed to lock file: {}", path.display());
106                return Err(e);
107            }
108        }
109    }
110    Ok(false)
111}
112
113/// Acquires a lock on a file in a "nice" manner.
114///
115/// Almost all long-running blocking actions in Cargo have a status message
116/// associated with them as we're not sure how long they'll take. Whenever a
117/// conflicted file lock happens, this is the case (we're not sure when the lock
118/// will be released).
119///
120/// This function will acquire the lock on a `path`, printing out a nice message
121/// to the console if we have to wait for it. It will first attempt to use `try`
122/// to acquire a lock on the crate, and in the case of contention it will emit a
123/// status message based on `msg` to [`GlobalContext`]'s shell, and then use `block` to
124/// block waiting to acquire a lock.
125///
126/// Returns an error if the lock could not be acquired or if any error other
127/// than a contention error happens.
128fn acquire(
129    msg: &str,
130    path: &Path,
131    lock_try: &dyn Fn() -> io::Result<()>,
132    lock_block: &dyn Fn() -> io::Result<()>,
133) -> io::Result<()> {
134    if try_acquire(path, lock_try)? {
135        return Ok(());
136    }
137    eprintln!("Waiting for file lock on {}", msg);
138
139    lock_block()?;
140    Ok(())
141}
142
143#[cfg(all(target_os = "linux", not(target_env = "musl")))]
144fn is_on_nfs_mount(path: &Path) -> bool {
145    use std::ffi::CString;
146    use std::mem;
147    use std::os::unix::prelude::*;
148
149    let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
150        return false;
151    };
152
153    unsafe {
154        let mut buf: libc::statfs = mem::zeroed();
155        let r = libc::statfs(path.as_ptr(), &mut buf);
156
157        r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
158    }
159}
160
161#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
162fn is_on_nfs_mount(_path: &Path) -> bool {
163    false
164}
165
166#[cfg(unix)]
167mod sys {
168    use std::fs::File;
169    use std::io::{Error, Result};
170    use std::os::unix::io::AsRawFd;
171
172    #[cfg(not(target_os = "solaris"))]
173    const LOCK_SH: i32 = libc::LOCK_SH;
174    #[cfg(target_os = "solaris")]
175    const LOCK_SH: i32 = 1;
176    #[cfg(not(target_os = "solaris"))]
177    const LOCK_EX: i32 = libc::LOCK_EX;
178    #[cfg(target_os = "solaris")]
179    const LOCK_EX: i32 = 2;
180    #[cfg(not(target_os = "solaris"))]
181    const LOCK_NB: i32 = libc::LOCK_NB;
182    #[cfg(target_os = "solaris")]
183    const LOCK_NB: i32 = 4;
184    #[cfg(not(target_os = "solaris"))]
185    const LOCK_UN: i32 = libc::LOCK_UN;
186    #[cfg(target_os = "solaris")]
187    const LOCK_UN: i32 = 8;
188
189    pub(super) fn lock_shared(file: &File) -> Result<()> {
190        flock(file, LOCK_SH)
191    }
192
193    pub(super) fn lock_exclusive(file: &File) -> Result<()> {
194        flock(file, LOCK_EX)
195    }
196
197    pub(super) fn try_lock_shared(file: &File) -> Result<()> {
198        flock(file, LOCK_SH | LOCK_NB)
199    }
200
201    pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
202        flock(file, LOCK_EX | LOCK_NB)
203    }
204
205    pub(super) fn unlock(file: &File) -> Result<()> {
206        flock(file, LOCK_UN)
207    }
208
209    pub(super) fn error_contended(err: &Error) -> bool {
210        err.raw_os_error() == Some(libc::EWOULDBLOCK)
211    }
212
213    pub(super) fn error_unsupported(err: &Error) -> bool {
214        match err.raw_os_error() {
215            // Unfortunately, depending on the target, these may or may not be the same.
216            // For targets in which they are the same, the duplicate pattern causes a warning.
217            #[allow(unreachable_patterns)]
218            Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
219            Some(libc::ENOSYS) => true,
220            _ => false,
221        }
222    }
223
224    #[cfg(not(target_os = "solaris"))]
225    fn flock(file: &File, flag: libc::c_int) -> Result<()> {
226        let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
227        if ret < 0 {
228            Err(Error::last_os_error())
229        } else {
230            Ok(())
231        }
232    }
233
234    #[cfg(target_os = "solaris")]
235    fn flock(file: &File, flag: libc::c_int) -> Result<()> {
236        // Solaris lacks flock(), so try to emulate using fcntl()
237        let mut flock = libc::flock {
238            l_type: 0,
239            l_whence: 0,
240            l_start: 0,
241            l_len: 0,
242            l_sysid: 0,
243            l_pid: 0,
244            l_pad: [0, 0, 0, 0],
245        };
246        flock.l_type = if flag & LOCK_UN != 0 {
247            libc::F_UNLCK
248        } else if flag & LOCK_EX != 0 {
249            libc::F_WRLCK
250        } else if flag & LOCK_SH != 0 {
251            libc::F_RDLCK
252        } else {
253            panic!("unexpected flock() operation")
254        };
255
256        let mut cmd = libc::F_SETLKW;
257        if (flag & LOCK_NB) != 0 {
258            cmd = libc::F_SETLK;
259        }
260
261        let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
262
263        if ret < 0 {
264            Err(Error::last_os_error())
265        } else {
266            Ok(())
267        }
268    }
269}
270
271#[cfg(windows)]
272mod sys {
273    use std::fs::File;
274    use std::io::{Error, Result};
275    use std::mem;
276    use std::os::windows::io::AsRawHandle;
277
278    use windows_sys::Win32::Foundation::HANDLE;
279    use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
280    use windows_sys::Win32::Storage::FileSystem::{
281        LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
282    };
283
284    pub(super) fn lock_shared(file: &File) -> Result<()> {
285        lock_file(file, 0)
286    }
287
288    pub(super) fn lock_exclusive(file: &File) -> Result<()> {
289        lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
290    }
291
292    pub(super) fn try_lock_shared(file: &File) -> Result<()> {
293        lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
294    }
295
296    pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
297        lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
298    }
299
300    pub(super) fn error_contended(err: &Error) -> bool {
301        err.raw_os_error()
302            .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
303    }
304
305    pub(super) fn error_unsupported(err: &Error) -> bool {
306        err.raw_os_error()
307            .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
308    }
309
310    pub(super) fn unlock(file: &File) -> Result<()> {
311        unsafe {
312            let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
313            if ret == 0 {
314                Err(Error::last_os_error())
315            } else {
316                Ok(())
317            }
318        }
319    }
320
321    fn lock_file(file: &File, flags: u32) -> Result<()> {
322        unsafe {
323            let mut overlapped = mem::zeroed();
324            let ret = LockFileEx(
325                file.as_raw_handle() as HANDLE,
326                flags,
327                0,
328                !0,
329                !0,
330                &mut overlapped,
331            );
332            if ret == 0 {
333                Err(Error::last_os_error())
334            } else {
335                Ok(())
336            }
337        }
338    }
339}