cargo/util/
flock.rs

1use std::fs::{File, OpenOptions};
2use std::io;
3use std::io::{Read, Seek, SeekFrom, Write};
4use std::path::{Display, Path, PathBuf};
5
6use fs2::{lock_contended_error, FileExt};
7use termcolor::Color::Cyan;
8#[cfg(windows)]
9use winapi::shared::winerror::ERROR_INVALID_FUNCTION;
10
11use crate::util::errors::{CargoResult, CargoResultExt};
12use crate::util::paths;
13use crate::util::Config;
14
15#[derive(Debug)]
16pub struct FileLock {
17    f: Option<File>,
18    path: PathBuf,
19    state: State,
20}
21
22#[derive(PartialEq, Debug)]
23enum State {
24    Unlocked,
25    Shared,
26    Exclusive,
27}
28
29impl FileLock {
30    /// Returns the underlying file handle of this lock.
31    pub fn file(&self) -> &File {
32        self.f.as_ref().unwrap()
33    }
34
35    /// Returns the underlying path that this lock points to.
36    ///
37    /// Note that special care must be taken to ensure that the path is not
38    /// referenced outside the lifetime of this lock.
39    pub fn path(&self) -> &Path {
40        assert_ne!(self.state, State::Unlocked);
41        &self.path
42    }
43
44    /// Returns the parent path containing this file
45    pub fn parent(&self) -> &Path {
46        assert_ne!(self.state, State::Unlocked);
47        self.path.parent().unwrap()
48    }
49
50    /// Removes all sibling files to this locked file.
51    ///
52    /// This can be useful if a directory is locked with a sentinel file but it
53    /// needs to be cleared out as it may be corrupt.
54    pub fn remove_siblings(&self) -> CargoResult<()> {
55        let path = self.path();
56        for entry in path.parent().unwrap().read_dir()? {
57            let entry = entry?;
58            if Some(&entry.file_name()[..]) == path.file_name() {
59                continue;
60            }
61            let kind = entry.file_type()?;
62            if kind.is_dir() {
63                paths::remove_dir_all(entry.path())?;
64            } else {
65                paths::remove_file(entry.path())?;
66            }
67        }
68        Ok(())
69    }
70}
71
72impl Read for FileLock {
73    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
74        self.file().read(buf)
75    }
76}
77
78impl Seek for FileLock {
79    fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
80        self.file().seek(to)
81    }
82}
83
84impl Write for FileLock {
85    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
86        self.file().write(buf)
87    }
88
89    fn flush(&mut self) -> io::Result<()> {
90        self.file().flush()
91    }
92}
93
94impl Drop for FileLock {
95    fn drop(&mut self) {
96        if self.state != State::Unlocked {
97            if let Some(f) = self.f.take() {
98                let _ = f.unlock();
99            }
100        }
101    }
102}
103
104/// A "filesystem" is intended to be a globally shared, hence locked, resource
105/// in Cargo.
106///
107/// The `Path` of a filesystem cannot be learned unless it's done in a locked
108/// fashion, and otherwise functions on this structure are prepared to handle
109/// concurrent invocations across multiple instances of Cargo.
110#[derive(Clone, Debug)]
111pub struct Filesystem {
112    root: PathBuf,
113}
114
115impl Filesystem {
116    /// Creates a new filesystem to be rooted at the given path.
117    pub fn new(path: PathBuf) -> Filesystem {
118        Filesystem { root: path }
119    }
120
121    /// Like `Path::join`, creates a new filesystem rooted at this filesystem
122    /// joined with the given path.
123    pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
124        Filesystem::new(self.root.join(other))
125    }
126
127    /// Like `Path::push`, pushes a new path component onto this filesystem.
128    pub fn push<T: AsRef<Path>>(&mut self, other: T) {
129        self.root.push(other);
130    }
131
132    /// Consumes this filesystem and returns the underlying `PathBuf`.
133    ///
134    /// Note that this is a relatively dangerous operation and should be used
135    /// with great caution!.
136    pub fn into_path_unlocked(self) -> PathBuf {
137        self.root
138    }
139
140    /// Returns the underlying `Path`.
141    ///
142    /// Note that this is a relatively dangerous operation and should be used
143    /// with great caution!.
144    pub fn as_path_unlocked(&self) -> &Path {
145        &self.root
146    }
147
148    /// Creates the directory pointed to by this filesystem.
149    ///
150    /// Handles errors where other Cargo processes are also attempting to
151    /// concurrently create this directory.
152    pub fn create_dir(&self) -> CargoResult<()> {
153        paths::create_dir_all(&self.root)?;
154        Ok(())
155    }
156
157    /// Returns an adaptor that can be used to print the path of this
158    /// filesystem.
159    pub fn display(&self) -> Display<'_> {
160        self.root.display()
161    }
162
163    /// Opens exclusive access to a file, returning the locked version of a
164    /// file.
165    ///
166    /// This function will create a file at `path` if it doesn't already exist
167    /// (including intermediate directories), and then it will acquire an
168    /// exclusive lock on `path`. If the process must block waiting for the
169    /// lock, the `msg` is printed to `config`.
170    ///
171    /// The returned file can be accessed to look at the path and also has
172    /// read/write access to the underlying file.
173    pub fn open_rw<P>(&self, path: P, config: &Config, msg: &str) -> CargoResult<FileLock>
174    where
175        P: AsRef<Path>,
176    {
177        self.open(
178            path.as_ref(),
179            OpenOptions::new().read(true).write(true).create(true),
180            State::Exclusive,
181            config,
182            msg,
183        )
184    }
185
186    /// Opens shared access to a file, returning the locked version of a file.
187    ///
188    /// This function will fail if `path` doesn't already exist, but if it does
189    /// then it will acquire a shared lock on `path`. If the process must block
190    /// waiting for the lock, the `msg` is printed to `config`.
191    ///
192    /// The returned file can be accessed to look at the path and also has read
193    /// access to the underlying file. Any writes to the file will return an
194    /// error.
195    pub fn open_ro<P>(&self, path: P, config: &Config, msg: &str) -> CargoResult<FileLock>
196    where
197        P: AsRef<Path>,
198    {
199        self.open(
200            path.as_ref(),
201            OpenOptions::new().read(true),
202            State::Shared,
203            config,
204            msg,
205        )
206    }
207
208    fn open(
209        &self,
210        path: &Path,
211        opts: &OpenOptions,
212        state: State,
213        config: &Config,
214        msg: &str,
215    ) -> CargoResult<FileLock> {
216        let path = self.root.join(path);
217
218        // If we want an exclusive lock then if we fail because of NotFound it's
219        // likely because an intermediate directory didn't exist, so try to
220        // create the directory and then continue.
221        let f = opts
222            .open(&path)
223            .or_else(|e| {
224                if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
225                    paths::create_dir_all(path.parent().unwrap())?;
226                    Ok(opts.open(&path)?)
227                } else {
228                    Err(anyhow::Error::from(e))
229                }
230            })
231            .chain_err(|| format!("failed to open: {}", path.display()))?;
232        match state {
233            State::Exclusive => {
234                acquire(config, msg, &path, &|| f.try_lock_exclusive(), &|| {
235                    f.lock_exclusive()
236                })?;
237            }
238            State::Shared => {
239                acquire(config, msg, &path, &|| f.try_lock_shared(), &|| {
240                    f.lock_shared()
241                })?;
242            }
243            State::Unlocked => {}
244        }
245        Ok(FileLock {
246            f: Some(f),
247            path,
248            state,
249        })
250    }
251}
252
253impl PartialEq<Path> for Filesystem {
254    fn eq(&self, other: &Path) -> bool {
255        self.root == other
256    }
257}
258
259impl PartialEq<Filesystem> for Path {
260    fn eq(&self, other: &Filesystem) -> bool {
261        self == other.root
262    }
263}
264
265/// Acquires a lock on a file in a "nice" manner.
266///
267/// Almost all long-running blocking actions in Cargo have a status message
268/// associated with them as we're not sure how long they'll take. Whenever a
269/// conflicted file lock happens, this is the case (we're not sure when the lock
270/// will be released).
271///
272/// This function will acquire the lock on a `path`, printing out a nice message
273/// to the console if we have to wait for it. It will first attempt to use `try`
274/// to acquire a lock on the crate, and in the case of contention it will emit a
275/// status message based on `msg` to `config`'s shell, and then use `block` to
276/// block waiting to acquire a lock.
277///
278/// Returns an error if the lock could not be acquired or if any error other
279/// than a contention error happens.
280fn acquire(
281    config: &Config,
282    msg: &str,
283    path: &Path,
284    r#try: &dyn Fn() -> io::Result<()>,
285    block: &dyn Fn() -> io::Result<()>,
286) -> CargoResult<()> {
287    // File locking on Unix is currently implemented via `flock`, which is known
288    // to be broken on NFS. We could in theory just ignore errors that happen on
289    // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
290    // forever**, even if the "non-blocking" flag is passed!
291    //
292    // As a result, we just skip all file locks entirely on NFS mounts. That
293    // should avoid calling any `flock` functions at all, and it wouldn't work
294    // there anyway.
295    //
296    // [1]: https://github.com/rust-lang/cargo/issues/2615
297    if is_on_nfs_mount(path) {
298        return Ok(());
299    }
300
301    match r#try() {
302        Ok(()) => return Ok(()),
303
304        // In addition to ignoring NFS which is commonly not working we also
305        // just ignore locking on filesystems that look like they don't
306        // implement file locking. We detect that here via the return value of
307        // locking (e.g., inspecting errno).
308        #[cfg(unix)]
309        Err(ref e) if e.raw_os_error() == Some(libc::ENOTSUP) => return Ok(()),
310
311        #[cfg(target_os = "linux")]
312        Err(ref e) if e.raw_os_error() == Some(libc::ENOSYS) => return Ok(()),
313
314        #[cfg(windows)]
315        Err(ref e) if e.raw_os_error() == Some(ERROR_INVALID_FUNCTION as i32) => return Ok(()),
316
317        Err(e) => {
318            if e.raw_os_error() != lock_contended_error().raw_os_error() {
319                let e = anyhow::Error::from(e);
320                let cx = format!("failed to lock file: {}", path.display());
321                return Err(e.context(cx).into());
322            }
323        }
324    }
325    let msg = format!("waiting for file lock on {}", msg);
326    config.shell().status_with_color("Blocking", &msg, Cyan)?;
327
328    block().chain_err(|| format!("failed to lock file: {}", path.display()))?;
329    return Ok(());
330
331    #[cfg(all(target_os = "linux", not(target_env = "musl")))]
332    fn is_on_nfs_mount(path: &Path) -> bool {
333        use std::ffi::CString;
334        use std::mem;
335        use std::os::unix::prelude::*;
336
337        let path = match CString::new(path.as_os_str().as_bytes()) {
338            Ok(path) => path,
339            Err(_) => return false,
340        };
341
342        unsafe {
343            let mut buf: libc::statfs = mem::zeroed();
344            let r = libc::statfs(path.as_ptr(), &mut buf);
345
346            r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
347        }
348    }
349
350    #[cfg(any(not(target_os = "linux"), target_env = "musl"))]
351    fn is_on_nfs_mount(_path: &Path) -> bool {
352        false
353    }
354}