tinymist_std/fs/flock.rs
1//! Upstream: <https://github.com/rust-lang/cargo/blob/rust-1.83.0/src/cargo/util/flock.rs>
2//! File-locking support.
3//!
4//! This module defines the [`Filesystem`] type which is an abstraction over a
5//! filesystem, ensuring that access to the filesystem is only done through
6//! coordinated locks.
7//!
8//! The [`FileLock`] type represents a locked file, and provides access to the
9//! file.
10
11use std::fs::{File, OpenOptions};
12use std::io;
13use std::io::{Read, Seek, SeekFrom, Write};
14use std::path::{Display, Path, PathBuf};
15
16use anyhow::Context as _;
17use anyhow::Result;
18
19use self::sys::*;
20use super::paths;
21
22/// A locked file.
23///
24/// This provides access to file while holding a lock on the file. This type
25/// implements the [`Read`], [`Write`], and [`Seek`] traits to provide access
26/// to the underlying file.
27///
28/// Locks are either shared (multiple processes can access the file) or
29/// exclusive (only one process can access the file).
30///
31/// This type is created via methods on the [`Filesystem`] type.
32///
33/// When this value is dropped, the lock will be released.
34#[derive(Debug)]
35pub struct FileLock {
36 f: Option<File>,
37 path: PathBuf,
38}
39
40impl FileLock {
41 /// Returns the underlying file handle of this lock.
42 pub fn file(&self) -> &File {
43 self.f.as_ref().unwrap()
44 }
45
46 /// Returns the underlying path that this lock points to.
47 ///
48 /// Note that special care must be taken to ensure that the path is not
49 /// referenced outside the lifetime of this lock.
50 pub fn path(&self) -> &Path {
51 &self.path
52 }
53
54 /// Returns the parent path containing this file
55 pub fn parent(&self) -> &Path {
56 self.path.parent().unwrap()
57 }
58
59 /// Removes all sibling files to this locked file.
60 ///
61 /// This can be useful if a directory is locked with a sentinel file but it
62 /// needs to be cleared out as it may be corrupt.
63 pub fn remove_siblings(&self) -> Result<()> {
64 let path = self.path();
65 for entry in path.parent().unwrap().read_dir()? {
66 let entry = entry?;
67 if Some(&entry.file_name()[..]) == path.file_name() {
68 continue;
69 }
70 let kind = entry.file_type()?;
71 if kind.is_dir() {
72 paths::remove_dir_all(entry.path())?;
73 } else {
74 paths::remove_file(entry.path())?;
75 }
76 }
77 Ok(())
78 }
79}
80
81impl Read for FileLock {
82 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
83 self.file().read(buf)
84 }
85}
86
87impl Seek for FileLock {
88 fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
89 self.file().seek(to)
90 }
91}
92
93impl Write for FileLock {
94 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
95 self.file().write(buf)
96 }
97
98 fn flush(&mut self) -> io::Result<()> {
99 self.file().flush()
100 }
101}
102
103impl Drop for FileLock {
104 fn drop(&mut self) {
105 if let Some(f) = self.f.take()
106 && let Err(e) = unlock(&f)
107 {
108 log::warn!("failed to release lock: {e:?}");
109 }
110 }
111}
112
113/// A "filesystem" is intended to be a globally shared, hence locked, resource
114/// in Cargo.
115///
116/// The `Path` of a filesystem cannot be learned unless it's done in a locked
117/// fashion, and otherwise functions on this structure are prepared to handle
118/// concurrent invocations across multiple instances of Cargo.
119///
120/// The methods on `Filesystem` that open files return a [`FileLock`] which
121/// holds the lock, and that type provides methods for accessing the
122/// underlying file.
123///
124/// If the blocking methods (like [`Filesystem::open_ro_shared`]) detect that
125/// they will block, then they will display a message to the user letting them
126/// know it is blocked. There are non-blocking variants starting with the
127/// `try_` prefix like [`Filesystem::try_open_ro_shared_create`].
128///
129/// The behavior of locks acquired by the `Filesystem` depend on the operating
130/// system. On unix-like system, they are advisory using [`flock`], and thus
131/// not enforced against processes which do not try to acquire the lock. On
132/// Windows, they are mandatory using [`LockFileEx`], enforced against all
133/// processes.
134///
135/// This **does not** guarantee that a lock is acquired. In some cases, for
136/// example on filesystems that don't support locking, it will return a
137/// [`FileLock`] even though the filesystem lock was not acquired. This is
138/// intended to provide a graceful fallback instead of refusing to work.
139/// Usually there aren't multiple processes accessing the same resource. In
140/// that case, it is the user's responsibility to not run concurrent
141/// processes.
142///
143/// [`flock`]: https://linux.die.net/man/2/flock
144/// [`LockFileEx`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex
145#[derive(Clone, Debug)]
146pub struct Filesystem {
147 root: PathBuf,
148}
149
150impl Filesystem {
151 /// Creates a new filesystem to be rooted at the given path.
152 pub fn new(path: PathBuf) -> Filesystem {
153 Filesystem { root: path }
154 }
155
156 /// Like `Path::join`, creates a new filesystem rooted at this filesystem
157 /// joined with the given path.
158 pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
159 Filesystem::new(self.root.join(other))
160 }
161
162 /// Like `Path::push`, pushes a new path component onto this filesystem.
163 pub fn push<T: AsRef<Path>>(&mut self, other: T) {
164 self.root.push(other);
165 }
166
167 /// Consumes this filesystem and returns the underlying `PathBuf`.
168 ///
169 /// Note that this is a relatively dangerous operation and should be used
170 /// with great caution!.
171 pub fn into_path_unlocked(self) -> PathBuf {
172 self.root
173 }
174
175 /// Returns the underlying `Path`.
176 ///
177 /// Note that this is a relatively dangerous operation and should be used
178 /// with great caution!.
179 pub fn as_path_unlocked(&self) -> &Path {
180 &self.root
181 }
182
183 /// Creates the directory pointed to by this filesystem.
184 ///
185 /// Handles errors where other Cargo processes are also attempting to
186 /// concurrently create this directory.
187 pub fn create_dir(&self) -> Result<()> {
188 paths::create_dir_all(&self.root)
189 }
190
191 /// Returns an adaptor that can be used to print the path of this
192 /// filesystem.
193 pub fn display(&self) -> Display<'_> {
194 self.root.display()
195 }
196
197 /// Opens read-write exclusive access to a file, returning the locked
198 /// version of a file.
199 ///
200 /// This function will create a file at `path` if it doesn't already exist
201 /// (including intermediate directories), and then it will acquire an
202 /// exclusive lock on `path`. If the process must block waiting for the
203 /// lock, the `msg` is printed to stderr.
204 ///
205 /// The returned file can be accessed to look at the path and also has
206 /// read/write access to the underlying file.
207 pub fn open_rw_exclusive_create<P>(&self, path: P, msg: &str) -> Result<FileLock>
208 where
209 P: AsRef<Path>,
210 {
211 let mut opts = OpenOptions::new();
212 opts.read(true).write(true).create(true);
213 let (path, f) = self.open(path.as_ref(), &opts, true)?;
214 acquire(msg, &path, &|| try_lock_exclusive(&f), &|| {
215 lock_exclusive(&f)
216 })?;
217 Ok(FileLock { f: Some(f), path })
218 }
219
220 /// A non-blocking version of [`Filesystem::open_rw_exclusive_create`].
221 ///
222 /// Returns `None` if the operation would block due to another process
223 /// holding the lock.
224 pub fn try_open_rw_exclusive_create<P: AsRef<Path>>(
225 &self,
226 path: P,
227 ) -> Result<Option<FileLock>> {
228 let mut opts = OpenOptions::new();
229 opts.read(true).write(true).create(true);
230 let (path, f) = self.open(path.as_ref(), &opts, true)?;
231 if try_acquire(&path, &|| try_lock_exclusive(&f))? {
232 Ok(Some(FileLock { f: Some(f), path }))
233 } else {
234 Ok(None)
235 }
236 }
237
238 /// Opens read-only shared access to a file, returning the locked version of
239 /// a file.
240 ///
241 /// This function will fail if `path` doesn't already exist, but if it does
242 /// then it will acquire a shared lock on `path`. If the process must block
243 /// waiting for the lock, the `msg` is printed to stderr.
244 ///
245 /// The returned file can be accessed to look at the path and also has read
246 /// access to the underlying file. Any writes to the file will return an
247 /// error.
248 pub fn open_ro_shared<P>(&self, path: P, msg: &str) -> Result<FileLock>
249 where
250 P: AsRef<Path>,
251 {
252 let (path, f) = self.open(path.as_ref(), OpenOptions::new().read(true), false)?;
253 acquire(msg, &path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
254 Ok(FileLock { f: Some(f), path })
255 }
256
257 /// Opens read-only shared access to a file, returning the locked version of
258 /// a file.
259 ///
260 /// Compared to [`Filesystem::open_ro_shared`], this will create the file
261 /// (and any directories in the parent) if the file does not already
262 /// exist.
263 pub fn open_ro_shared_create<P: AsRef<Path>>(&self, path: P, msg: &str) -> Result<FileLock> {
264 let mut opts = OpenOptions::new();
265 opts.read(true).write(true).create(true);
266 let (path, f) = self.open(path.as_ref(), &opts, true)?;
267 acquire(msg, &path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
268 Ok(FileLock { f: Some(f), path })
269 }
270
271 /// A non-blocking version of [`Filesystem::open_ro_shared_create`].
272 ///
273 /// Returns `None` if the operation would block due to another process
274 /// holding the lock.
275 pub fn try_open_ro_shared_create<P: AsRef<Path>>(&self, path: P) -> Result<Option<FileLock>> {
276 let mut opts = OpenOptions::new();
277 opts.read(true).write(true).create(true);
278 let (path, f) = self.open(path.as_ref(), &opts, true)?;
279 if try_acquire(&path, &|| try_lock_shared(&f))? {
280 Ok(Some(FileLock { f: Some(f), path }))
281 } else {
282 Ok(None)
283 }
284 }
285
286 fn open(&self, path: &Path, opts: &OpenOptions, create: bool) -> Result<(PathBuf, File)> {
287 let path = self.root.join(path);
288 let f = opts
289 .open(&path)
290 .or_else(|e| {
291 // If we were requested to create this file, and there was a
292 // NotFound error, then that was likely due to missing
293 // intermediate directories. Try creating them and try again.
294 if e.kind() == io::ErrorKind::NotFound && create {
295 paths::create_dir_all(path.parent().unwrap())?;
296 Ok(opts.open(&path)?)
297 } else {
298 Err(anyhow::Error::from(e))
299 }
300 })
301 .with_context(|| format!("failed to open: {}", path.display()))?;
302 Ok((path, f))
303 }
304}
305
306impl PartialEq<Path> for Filesystem {
307 fn eq(&self, other: &Path) -> bool {
308 self.root == other
309 }
310}
311
312impl PartialEq<Filesystem> for Path {
313 fn eq(&self, other: &Filesystem) -> bool {
314 self == other.root
315 }
316}
317
318fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> Result<bool> {
319 // File locking on Unix is currently implemented via `flock`, which is known
320 // to be broken on NFS. We could in theory just ignore errors that happen on
321 // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
322 // forever**, even if the "non-blocking" flag is passed!
323 //
324 // As a result, we just skip all file locks entirely on NFS mounts. That
325 // should avoid calling any `flock` functions at all, and it wouldn't work
326 // there anyway.
327 //
328 // [1]: https://github.com/rust-lang/cargo/issues/2615
329 if is_on_nfs_mount(path) {
330 log::info!("{path:?} appears to be an NFS mount, not trying to lock");
331 return Ok(true);
332 }
333
334 match lock_try() {
335 Ok(()) => return Ok(true),
336
337 // In addition to ignoring NFS which is commonly not working we also
338 // just ignore locking on filesystems that look like they don't
339 // implement file locking.
340 Err(e) if error_unsupported(&e) => return Ok(true),
341
342 Err(e) => {
343 if !error_contended(&e) {
344 let e = anyhow::Error::from(e);
345 let cx = format!("failed to lock file: {}", path.display());
346 return Err(e.context(cx));
347 }
348 }
349 }
350 Ok(false)
351}
352
353/// Acquires a lock on a file in a "nice" manner.
354///
355/// Almost all long-running blocking actions in Cargo have a status message
356/// associated with them as we're not sure how long they'll take. Whenever a
357/// conflicted file lock happens, this is the case (we're not sure when the lock
358/// will be released).
359///
360/// This function will acquire the lock on a `path`, printing out a nice message
361/// to the console if we have to wait for it. It will first attempt to use `try`
362/// to acquire a lock on the crate, and in the case of contention it will emit a
363/// status message based on `msg` to [`GlobalContext`]'s shell, and then use
364/// `block` to block waiting to acquire a lock.
365///
366/// Returns an error if the lock could not be acquired or if any error other
367/// than a contention error happens.
368fn acquire(
369 msg: &str,
370 path: &Path,
371 lock_try: &dyn Fn() -> io::Result<()>,
372 lock_block: &dyn Fn() -> io::Result<()>,
373) -> Result<()> {
374 if try_acquire(path, lock_try)? {
375 return Ok(());
376 }
377 log::info!("waiting for file lock on {msg}");
378
379 lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
380 Ok(())
381}
382
383#[cfg(all(target_os = "linux", not(target_env = "musl")))]
384fn is_on_nfs_mount(path: &Path) -> bool {
385 use std::ffi::CString;
386 use std::mem;
387 use std::os::unix::prelude::*;
388
389 let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
390 return false;
391 };
392
393 // SAFETY: this is implemented by the cargo
394 unsafe {
395 let mut buf: libc::statfs = mem::zeroed();
396 let r = libc::statfs(path.as_ptr(), &mut buf);
397
398 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
399 }
400}
401
402#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
403fn is_on_nfs_mount(_path: &Path) -> bool {
404 false
405}
406
407#[cfg(unix)]
408mod sys {
409 use std::fs::File;
410 use std::io::{Error, Result};
411 use std::os::unix::io::AsRawFd;
412
413 pub(super) fn lock_shared(file: &File) -> Result<()> {
414 flock(file, libc::LOCK_SH)
415 }
416
417 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
418 flock(file, libc::LOCK_EX)
419 }
420
421 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
422 flock(file, libc::LOCK_SH | libc::LOCK_NB)
423 }
424
425 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
426 flock(file, libc::LOCK_EX | libc::LOCK_NB)
427 }
428
429 pub(super) fn unlock(file: &File) -> Result<()> {
430 flock(file, libc::LOCK_UN)
431 }
432
433 pub(super) fn error_contended(err: &Error) -> bool {
434 err.raw_os_error() == Some(libc::EWOULDBLOCK)
435 }
436
437 pub(super) fn error_unsupported(err: &Error) -> bool {
438 match err.raw_os_error() {
439 // Unfortunately, depending on the target, these may or may not be the same.
440 // For targets in which they are the same, the duplicate pattern causes a warning.
441 #[allow(unreachable_patterns)]
442 Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
443 Some(libc::ENOSYS) => true,
444 _ => false,
445 }
446 }
447
448 #[cfg(not(target_os = "solaris"))]
449 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
450 // SAFETY: this is implemented by the cargo
451 let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
452 if ret < 0 {
453 Err(Error::last_os_error())
454 } else {
455 Ok(())
456 }
457 }
458
459 #[cfg(target_os = "solaris")]
460 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
461 // Solaris lacks flock(), so try to emulate using fcntl()
462 let mut flock = libc::flock {
463 l_type: 0,
464 l_whence: 0,
465 l_start: 0,
466 l_len: 0,
467 l_sysid: 0,
468 l_pid: 0,
469 l_pad: [0, 0, 0, 0],
470 };
471 flock.l_type = if flag & libc::LOCK_UN != 0 {
472 libc::F_UNLCK
473 } else if flag & libc::LOCK_EX != 0 {
474 libc::F_WRLCK
475 } else if flag & libc::LOCK_SH != 0 {
476 libc::F_RDLCK
477 } else {
478 panic!("unexpected flock() operation")
479 };
480
481 let mut cmd = libc::F_SETLKW;
482 if (flag & libc::LOCK_NB) != 0 {
483 cmd = libc::F_SETLK;
484 }
485
486 let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
487
488 if ret < 0 {
489 Err(Error::last_os_error())
490 } else {
491 Ok(())
492 }
493 }
494}
495
496#[cfg(windows)]
497mod sys {
498 use std::fs::File;
499 use std::io::{Error, Result};
500 use std::mem;
501 use std::os::windows::io::AsRawHandle;
502
503 use windows_sys::Win32::Foundation::HANDLE;
504 use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
505 use windows_sys::Win32::Storage::FileSystem::{
506 LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx, UnlockFile,
507 };
508
509 pub(super) fn lock_shared(file: &File) -> Result<()> {
510 lock_file(file, 0)
511 }
512
513 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
514 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
515 }
516
517 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
518 lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
519 }
520
521 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
522 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
523 }
524
525 pub(super) fn error_contended(err: &Error) -> bool {
526 err.raw_os_error() == Some(ERROR_LOCK_VIOLATION as i32)
527 }
528
529 pub(super) fn error_unsupported(err: &Error) -> bool {
530 err.raw_os_error() == Some(ERROR_INVALID_FUNCTION as i32)
531 }
532
533 pub(super) fn unlock(file: &File) -> Result<()> {
534 // SAFETY: this is implemented by the cargo
535 unsafe {
536 let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
537 if ret == 0 {
538 Err(Error::last_os_error())
539 } else {
540 Ok(())
541 }
542 }
543 }
544
545 fn lock_file(file: &File, flags: u32) -> Result<()> {
546 // SAFETY: this is implemented by the cargo
547 unsafe {
548 let mut overlapped = mem::zeroed();
549 let ret = LockFileEx(
550 file.as_raw_handle() as HANDLE,
551 flags,
552 0,
553 !0,
554 !0,
555 &mut overlapped,
556 );
557 if ret == 0 {
558 Err(Error::last_os_error())
559 } else {
560 Ok(())
561 }
562 }
563 }
564}