123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
- // SPDX-License-Identifier: Apache-2.0
- // SPDX-License-Identifier: MIT
- // taken from https://github.com/rust-lang/cargo/blob/b0c9586f4cbf426914df47c65de38ea323772c74/src/cargo/util/flock.rs
- #![allow(dead_code)]
- use std::fs::{create_dir_all, File, OpenOptions};
- use std::io;
- use std::io::{Read, Seek, SeekFrom, Write};
- use std::path::{Path, PathBuf};
- use crate::Result;
- use anyhow::Context as _;
- use sys::*;
- #[derive(Debug)]
- pub struct FileLock {
- f: Option<File>,
- path: PathBuf,
- state: State,
- }
- #[derive(PartialEq, Debug)]
- enum State {
- Unlocked,
- Shared,
- Exclusive,
- }
- impl FileLock {
- /// Returns the underlying file handle of this lock.
- pub fn file(&self) -> &File {
- self.f.as_ref().unwrap()
- }
- /// Returns the underlying path that this lock points to.
- ///
- /// Note that special care must be taken to ensure that the path is not
- /// referenced outside the lifetime of this lock.
- pub fn path(&self) -> &Path {
- assert_ne!(self.state, State::Unlocked);
- &self.path
- }
- /// Returns the parent path containing this file
- pub fn parent(&self) -> &Path {
- assert_ne!(self.state, State::Unlocked);
- self.path.parent().unwrap()
- }
- }
- impl Read for FileLock {
- fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
- self.file().read(buf)
- }
- }
- impl Seek for FileLock {
- fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
- self.file().seek(to)
- }
- }
- impl Write for FileLock {
- fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
- self.file().write(buf)
- }
- fn flush(&mut self) -> io::Result<()> {
- self.file().flush()
- }
- }
- impl Drop for FileLock {
- fn drop(&mut self) {
- if self.state != State::Unlocked {
- if let Some(f) = self.f.take() {
- let _ = unlock(&f);
- }
- }
- }
- }
- /// Opens exclusive access to a file, returning the locked version of a
- /// file.
- ///
- /// This function will create a file at `path` if it doesn't already exist
- /// (including intermediate directories), and then it will acquire an
- /// exclusive lock on `path`. If the process must block waiting for the
- /// lock, the `msg` is logged.
- ///
- /// The returned file can be accessed to look at the path and also has
- /// read/write access to the underlying file.
- pub fn open_rw<P>(path: P, msg: &str) -> Result<FileLock>
- where
- P: AsRef<Path>,
- {
- open(
- path.as_ref(),
- OpenOptions::new().read(true).write(true).create(true),
- State::Exclusive,
- msg,
- )
- }
- /// Opens shared access to a file, returning the locked version of a file.
- ///
- /// This function will fail if `path` doesn't already exist, but if it does
- /// then it will acquire a shared lock on `path`. If the process must block
- /// waiting for the lock, the `msg` is logged.
- ///
- /// The returned file can be accessed to look at the path and also has read
- /// access to the underlying file. Any writes to the file will return an
- /// error.
- pub fn open_ro<P>(path: P, msg: &str) -> Result<FileLock>
- where
- P: AsRef<Path>,
- {
- open(
- path.as_ref(),
- OpenOptions::new().read(true),
- State::Shared,
- msg,
- )
- }
- fn open(path: &Path, opts: &OpenOptions, state: State, msg: &str) -> Result<FileLock> {
- // If we want an exclusive lock then if we fail because of NotFound it's
- // likely because an intermediate directory didn't exist, so try to
- // create the directory and then continue.
- let f = opts
- .open(path)
- .or_else(|e| {
- if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
- create_dir_all(path.parent().unwrap())?;
- Ok(opts.open(path)?)
- } else {
- Err(anyhow::Error::from(e))
- }
- })
- .with_context(|| format!("failed to open: {}", path.display()))?;
- match state {
- State::Exclusive => {
- acquire(msg, path, &|| try_lock_exclusive(&f), &|| {
- lock_exclusive(&f)
- })?;
- }
- State::Shared => {
- acquire(msg, path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
- }
- State::Unlocked => {}
- }
- Ok(FileLock {
- f: Some(f),
- path: path.to_path_buf(),
- state,
- })
- }
- /// Acquires a lock on a file in a "nice" manner.
- ///
- /// Almost all long-running blocking actions in Cargo have a status message
- /// associated with them as we're not sure how long they'll take. Whenever a
- /// conflicted file lock happens, this is the case (we're not sure when the lock
- /// will be released).
- ///
- /// This function will acquire the lock on a `path`, printing out a nice message
- /// to the console if we have to wait for it. It will first attempt to use `try`
- /// to acquire a lock on the crate, and in the case of contention it will emit a
- /// status message based on `msg` to `config`'s shell, and then use `block` to
- /// block waiting to acquire a lock.
- ///
- /// Returns an error if the lock could not be acquired or if any error other
- /// than a contention error happens.
- fn acquire(
- msg: &str,
- path: &Path,
- lock_try: &dyn Fn() -> io::Result<()>,
- lock_block: &dyn Fn() -> io::Result<()>,
- ) -> Result<()> {
- // File locking on Unix is currently implemented via `flock`, which is known
- // to be broken on NFS. We could in theory just ignore errors that happen on
- // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
- // forever**, even if the "non-blocking" flag is passed!
- //
- // As a result, we just skip all file locks entirely on NFS mounts. That
- // should avoid calling any `flock` functions at all, and it wouldn't work
- // there anyway.
- //
- // [1]: https://github.com/rust-lang/cargo/issues/2615
- if is_on_nfs_mount(path) {
- return Ok(());
- }
- match lock_try() {
- Ok(()) => return Ok(()),
- // In addition to ignoring NFS which is commonly not working we also
- // just ignore locking on filesystems that look like they don't
- // implement file locking.
- Err(e) if error_unsupported(&e) => return Ok(()),
- Err(e) => {
- if !error_contended(&e) {
- let e = anyhow::Error::from(e);
- let cx = format!("failed to lock file: {}", path.display());
- return Err(e.context(cx));
- }
- }
- }
- let msg = format!("waiting for file lock on {msg}");
- log::info!(action = "Blocking"; "{}", &msg);
- lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
- return Ok(());
- #[cfg(all(target_os = "linux", not(target_env = "musl")))]
- fn is_on_nfs_mount(path: &Path) -> bool {
- use std::ffi::CString;
- use std::mem;
- use std::os::unix::prelude::*;
- let path = match CString::new(path.as_os_str().as_bytes()) {
- Ok(path) => path,
- Err(_) => return false,
- };
- unsafe {
- let mut buf: libc::statfs = mem::zeroed();
- let r = libc::statfs(path.as_ptr(), &mut buf);
- r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
- }
- }
- #[cfg(any(not(target_os = "linux"), target_env = "musl"))]
- fn is_on_nfs_mount(_path: &Path) -> bool {
- false
- }
- }
- #[cfg(unix)]
- mod sys {
- use std::fs::File;
- use std::io::{Error, Result};
- use std::os::unix::io::AsRawFd;
- pub(super) fn lock_shared(file: &File) -> Result<()> {
- flock(file, libc::LOCK_SH)
- }
- pub(super) fn lock_exclusive(file: &File) -> Result<()> {
- flock(file, libc::LOCK_EX)
- }
- pub(super) fn try_lock_shared(file: &File) -> Result<()> {
- flock(file, libc::LOCK_SH | libc::LOCK_NB)
- }
- pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
- flock(file, libc::LOCK_EX | libc::LOCK_NB)
- }
- pub(super) fn unlock(file: &File) -> Result<()> {
- flock(file, libc::LOCK_UN)
- }
- pub(super) fn error_contended(err: &Error) -> bool {
- err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
- }
- pub(super) fn error_unsupported(err: &Error) -> bool {
- match err.raw_os_error() {
- // Unfortunately, depending on the target, these may or may not be the same.
- // For targets in which they are the same, the duplicate pattern causes a warning.
- #[allow(unreachable_patterns)]
- Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
- Some(libc::ENOSYS) => true,
- _ => false,
- }
- }
- #[cfg(not(target_os = "solaris"))]
- fn flock(file: &File, flag: libc::c_int) -> Result<()> {
- let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
- if ret < 0 {
- Err(Error::last_os_error())
- } else {
- Ok(())
- }
- }
- #[cfg(target_os = "solaris")]
- fn flock(file: &File, flag: libc::c_int) -> Result<()> {
- // Solaris lacks flock(), so simply succeed with a no-op
- Ok(())
- }
- }
- #[cfg(windows)]
- mod sys {
- use std::fs::File;
- use std::io::{Error, Result};
- use std::mem;
- use std::os::windows::io::AsRawHandle;
- use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION, HANDLE};
- use windows_sys::Win32::Storage::FileSystem::{
- LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LOCK_FILE_FLAGS,
- };
- pub(super) fn lock_shared(file: &File) -> Result<()> {
- lock_file(file, 0)
- }
- pub(super) fn lock_exclusive(file: &File) -> Result<()> {
- lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
- }
- pub(super) fn try_lock_shared(file: &File) -> Result<()> {
- lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
- }
- pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
- lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
- }
- pub(super) fn error_contended(err: &Error) -> bool {
- err
- .raw_os_error()
- .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
- }
- pub(super) fn error_unsupported(err: &Error) -> bool {
- err
- .raw_os_error()
- .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
- }
- pub(super) fn unlock(file: &File) -> Result<()> {
- let ret = unsafe { UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0) };
- if ret == 0 {
- Err(Error::last_os_error())
- } else {
- Ok(())
- }
- }
- fn lock_file(file: &File, flags: LOCK_FILE_FLAGS) -> Result<()> {
- let ret = unsafe {
- let mut overlapped = mem::zeroed();
- LockFileEx(
- file.as_raw_handle() as HANDLE,
- flags,
- 0,
- !0,
- !0,
- &mut overlapped,
- )
- };
- if ret == 0 {
- Err(Error::last_os_error())
- } else {
- Ok(())
- }
- }
- }
|