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}