1use std::fs::{File, OpenOptions};
11use std::io;
12use std::io::{Read, Seek, SeekFrom, Write};
13use std::path::{Display, Path, PathBuf};
14
15use crate::util::errors::CargoResult;
16use crate::util::style;
17use crate::util::GlobalContext;
18use anyhow::Context as _;
19use cargo_util::paths;
20use sys::*;
21
22#[derive(Debug)]
35pub struct FileLock {
36 f: Option<File>,
37 path: PathBuf,
38}
39
40impl FileLock {
41 pub fn file(&self) -> &File {
43 self.f.as_ref().unwrap()
44 }
45
46 pub fn path(&self) -> &Path {
51 &self.path
52 }
53
54 pub fn parent(&self) -> &Path {
56 self.path.parent().unwrap()
57 }
58
59 pub fn remove_siblings(&self) -> CargoResult<()> {
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 if let Err(e) = unlock(&f) {
107 tracing::warn!("failed to release lock: {e:?}");
108 }
109 }
110 }
111}
112
113#[derive(Clone, Debug)]
146pub struct Filesystem {
147 root: PathBuf,
148}
149
150impl Filesystem {
151 pub fn new(path: PathBuf) -> Filesystem {
153 Filesystem { root: path }
154 }
155
156 pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
159 Filesystem::new(self.root.join(other))
160 }
161
162 pub fn push<T: AsRef<Path>>(&mut self, other: T) {
164 self.root.push(other);
165 }
166
167 pub fn into_path_unlocked(self) -> PathBuf {
172 self.root
173 }
174
175 pub fn as_path_unlocked(&self) -> &Path {
180 &self.root
181 }
182
183 pub fn create_dir(&self) -> CargoResult<()> {
188 paths::create_dir_all(&self.root)
189 }
190
191 pub fn display(&self) -> Display<'_> {
194 self.root.display()
195 }
196
197 pub fn open_rw_exclusive_create<P>(
208 &self,
209 path: P,
210 gctx: &GlobalContext,
211 msg: &str,
212 ) -> CargoResult<FileLock>
213 where
214 P: AsRef<Path>,
215 {
216 let mut opts = OpenOptions::new();
217 opts.read(true).write(true).create(true);
218 let (path, f) = self.open(path.as_ref(), &opts, true)?;
219 acquire(gctx, msg, &path, &|| try_lock_exclusive(&f), &|| {
220 lock_exclusive(&f)
221 })?;
222 Ok(FileLock { f: Some(f), path })
223 }
224
225 pub fn try_open_rw_exclusive_create<P: AsRef<Path>>(
230 &self,
231 path: P,
232 ) -> CargoResult<Option<FileLock>> {
233 let mut opts = OpenOptions::new();
234 opts.read(true).write(true).create(true);
235 let (path, f) = self.open(path.as_ref(), &opts, true)?;
236 if try_acquire(&path, &|| try_lock_exclusive(&f))? {
237 Ok(Some(FileLock { f: Some(f), path }))
238 } else {
239 Ok(None)
240 }
241 }
242
243 pub fn open_ro_shared<P>(
253 &self,
254 path: P,
255 gctx: &GlobalContext,
256 msg: &str,
257 ) -> CargoResult<FileLock>
258 where
259 P: AsRef<Path>,
260 {
261 let (path, f) = self.open(path.as_ref(), &OpenOptions::new().read(true), false)?;
262 acquire(gctx, msg, &path, &|| try_lock_shared(&f), &|| {
263 lock_shared(&f)
264 })?;
265 Ok(FileLock { f: Some(f), path })
266 }
267
268 pub fn open_ro_shared_create<P: AsRef<Path>>(
274 &self,
275 path: P,
276 gctx: &GlobalContext,
277 msg: &str,
278 ) -> CargoResult<FileLock> {
279 let mut opts = OpenOptions::new();
280 opts.read(true).write(true).create(true);
281 let (path, f) = self.open(path.as_ref(), &opts, true)?;
282 acquire(gctx, msg, &path, &|| try_lock_shared(&f), &|| {
283 lock_shared(&f)
284 })?;
285 Ok(FileLock { f: Some(f), path })
286 }
287
288 pub fn try_open_ro_shared_create<P: AsRef<Path>>(
293 &self,
294 path: P,
295 ) -> CargoResult<Option<FileLock>> {
296 let mut opts = OpenOptions::new();
297 opts.read(true).write(true).create(true);
298 let (path, f) = self.open(path.as_ref(), &opts, true)?;
299 if try_acquire(&path, &|| try_lock_shared(&f))? {
300 Ok(Some(FileLock { f: Some(f), path }))
301 } else {
302 Ok(None)
303 }
304 }
305
306 fn open(&self, path: &Path, opts: &OpenOptions, create: bool) -> CargoResult<(PathBuf, File)> {
307 let path = self.root.join(path);
308 let f = opts
309 .open(&path)
310 .or_else(|e| {
311 if e.kind() == io::ErrorKind::NotFound && create {
315 paths::create_dir_all(path.parent().unwrap())?;
316 Ok(opts.open(&path)?)
317 } else {
318 Err(anyhow::Error::from(e))
319 }
320 })
321 .with_context(|| format!("failed to open: {}", path.display()))?;
322 Ok((path, f))
323 }
324}
325
326impl PartialEq<Path> for Filesystem {
327 fn eq(&self, other: &Path) -> bool {
328 self.root == other
329 }
330}
331
332impl PartialEq<Filesystem> for Path {
333 fn eq(&self, other: &Filesystem) -> bool {
334 self == other.root
335 }
336}
337
338fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> CargoResult<bool> {
339 if is_on_nfs_mount(path) {
350 tracing::debug!("{path:?} appears to be an NFS mount, not trying to lock");
351 return Ok(true);
352 }
353
354 match lock_try() {
355 Ok(()) => return Ok(true),
356
357 Err(e) if error_unsupported(&e) => return Ok(true),
361
362 Err(e) => {
363 if !error_contended(&e) {
364 let e = anyhow::Error::from(e);
365 let cx = format!("failed to lock file: {}", path.display());
366 return Err(e.context(cx));
367 }
368 }
369 }
370 Ok(false)
371}
372
373fn acquire(
389 gctx: &GlobalContext,
390 msg: &str,
391 path: &Path,
392 lock_try: &dyn Fn() -> io::Result<()>,
393 lock_block: &dyn Fn() -> io::Result<()>,
394) -> CargoResult<()> {
395 if cfg!(debug_assertions) {
396 gctx.shell().verbosity();
398 }
399 if try_acquire(path, lock_try)? {
400 return Ok(());
401 }
402 let msg = format!("waiting for file lock on {}", msg);
403 gctx.shell()
404 .status_with_color("Blocking", &msg, &style::NOTE)?;
405
406 lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
407 Ok(())
408}
409
410#[cfg(all(target_os = "linux", not(target_env = "musl")))]
411fn is_on_nfs_mount(path: &Path) -> bool {
412 use std::ffi::CString;
413 use std::mem;
414 use std::os::unix::prelude::*;
415
416 let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
417 return false;
418 };
419
420 unsafe {
421 let mut buf: libc::statfs = mem::zeroed();
422 let r = libc::statfs(path.as_ptr(), &mut buf);
423
424 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
425 }
426}
427
428#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
429fn is_on_nfs_mount(_path: &Path) -> bool {
430 false
431}
432
433#[cfg(unix)]
434mod sys {
435 use std::fs::File;
436 use std::io::{Error, Result};
437 use std::os::unix::io::AsRawFd;
438
439 #[cfg(not(target_os = "solaris"))]
440 const LOCK_SH: i32 = libc::LOCK_SH;
441 #[cfg(target_os = "solaris")]
442 const LOCK_SH: i32 = 1;
443 #[cfg(not(target_os = "solaris"))]
444 const LOCK_EX: i32 = libc::LOCK_EX;
445 #[cfg(target_os = "solaris")]
446 const LOCK_EX: i32 = 2;
447 #[cfg(not(target_os = "solaris"))]
448 const LOCK_NB: i32 = libc::LOCK_NB;
449 #[cfg(target_os = "solaris")]
450 const LOCK_NB: i32 = 4;
451 #[cfg(not(target_os = "solaris"))]
452 const LOCK_UN: i32 = libc::LOCK_UN;
453 #[cfg(target_os = "solaris")]
454 const LOCK_UN: i32 = 8;
455
456 pub(super) fn lock_shared(file: &File) -> Result<()> {
457 flock(file, LOCK_SH)
458 }
459
460 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
461 flock(file, LOCK_EX)
462 }
463
464 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
465 flock(file, LOCK_SH | LOCK_NB)
466 }
467
468 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
469 flock(file, LOCK_EX | LOCK_NB)
470 }
471
472 pub(super) fn unlock(file: &File) -> Result<()> {
473 flock(file, LOCK_UN)
474 }
475
476 pub(super) fn error_contended(err: &Error) -> bool {
477 err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
478 }
479
480 pub(super) fn error_unsupported(err: &Error) -> bool {
481 match err.raw_os_error() {
482 #[allow(unreachable_patterns)]
485 Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
486 Some(libc::ENOSYS) => true,
487 _ => false,
488 }
489 }
490
491 #[cfg(not(target_os = "solaris"))]
492 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
493 let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
494 if ret < 0 {
495 Err(Error::last_os_error())
496 } else {
497 Ok(())
498 }
499 }
500
501 #[cfg(target_os = "solaris")]
502 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
503 let mut flock = libc::flock {
505 l_type: 0,
506 l_whence: 0,
507 l_start: 0,
508 l_len: 0,
509 l_sysid: 0,
510 l_pid: 0,
511 l_pad: [0, 0, 0, 0],
512 };
513 flock.l_type = if flag & LOCK_UN != 0 {
514 libc::F_UNLCK
515 } else if flag & LOCK_EX != 0 {
516 libc::F_WRLCK
517 } else if flag & LOCK_SH != 0 {
518 libc::F_RDLCK
519 } else {
520 panic!("unexpected flock() operation")
521 };
522
523 let mut cmd = libc::F_SETLKW;
524 if (flag & LOCK_NB) != 0 {
525 cmd = libc::F_SETLK;
526 }
527
528 let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
529
530 if ret < 0 {
531 Err(Error::last_os_error())
532 } else {
533 Ok(())
534 }
535 }
536}
537
538#[cfg(windows)]
539mod sys {
540 use std::fs::File;
541 use std::io::{Error, Result};
542 use std::mem;
543 use std::os::windows::io::AsRawHandle;
544
545 use windows_sys::Win32::Foundation::HANDLE;
546 use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
547 use windows_sys::Win32::Storage::FileSystem::{
548 LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
549 };
550
551 pub(super) fn lock_shared(file: &File) -> Result<()> {
552 lock_file(file, 0)
553 }
554
555 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
556 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
557 }
558
559 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
560 lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
561 }
562
563 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
564 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
565 }
566
567 pub(super) fn error_contended(err: &Error) -> bool {
568 err.raw_os_error()
569 .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
570 }
571
572 pub(super) fn error_unsupported(err: &Error) -> bool {
573 err.raw_os_error()
574 .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
575 }
576
577 pub(super) fn unlock(file: &File) -> Result<()> {
578 unsafe {
579 let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
580 if ret == 0 {
581 Err(Error::last_os_error())
582 } else {
583 Ok(())
584 }
585 }
586 }
587
588 fn lock_file(file: &File, flags: u32) -> Result<()> {
589 unsafe {
590 let mut overlapped = mem::zeroed();
591 let ret = LockFileEx(
592 file.as_raw_handle() as HANDLE,
593 flags,
594 0,
595 !0,
596 !0,
597 &mut overlapped,
598 );
599 if ret == 0 {
600 Err(Error::last_os_error())
601 } else {
602 Ok(())
603 }
604 }
605 }
606}