Auto merge of #6940 - alexcrichton:readonly-compat, r=ehuss

Re-enable compatibility with readonly CARGO_HOME

Previously Cargo would attempt to work as much as possible with a
previously filled out CARGO_HOME, even if it was mounted as read-only.
In #6880 this was regressed as a few global locks and files were always
attempted to be opened in writable mode.

This commit fixes these issues by correcting two locations:

* First the global package cache lock has error handling to allow
  acquiring the lock in read-only mode inaddition to read/write mode. If
  the read/write mode failed due to an error that looks like a readonly
  filesystem then we assume everything in the package cache is readonly
  and we switch to just acquiring any lock, this time a shared readonly
  one. We in theory aren't actually doing any synchronization at that
  point since it's all readonly anyway.

* Next when unpacking package we're careful to issue a `stat` call
  before opening a file in writable mode. This way our preexisting guard
  to return early if a package is unpacked will succeed before we open
  anything in writable mode.

Closes #6928
This commit is contained in:
bors 2019-05-14 15:53:49 +00:00
commit 414c1eb4d5
4 changed files with 117 additions and 12 deletions

View file

@ -449,15 +449,16 @@ impl<'cfg> RegistrySource<'cfg> {
let path = dst.join(PACKAGE_SOURCE_LOCK);
let path = self.config.assert_package_cache_locked(&path);
let unpack_dir = path.parent().unwrap();
if let Ok(meta) = path.metadata() {
if meta.len() > 0 {
return Ok(unpack_dir.to_path_buf());
}
}
let mut ok = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&path)?;
let meta = ok.metadata()?;
if meta.len() > 0 {
return Ok(unpack_dir.to_path_buf());
}
let gz = GzDecoder::new(tarball);
let mut tar = Archive::new(gz);

View file

@ -6,7 +6,7 @@ use std::env;
use std::fmt;
use std::fs::{self, File};
use std::io::prelude::*;
use std::io::SeekFrom;
use std::io::{self, SeekFrom};
use std::mem;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@ -860,21 +860,71 @@ impl Config {
return ret;
}
/// Acquires an exclusive lock on the global "package cache"
///
/// This lock is global per-process and can be acquired recursively. An RAII
/// structure is returned to release the lock, and if this process
/// abnormally terminates the lock is also released.
pub fn acquire_package_cache_lock<'a>(&'a self) -> CargoResult<PackageCacheLock<'a>> {
let mut slot = self.package_cache_lock.borrow_mut();
match *slot {
// We've already acquired the lock in this process, so simply bump
// the count and continue.
Some((_, ref mut cnt)) => {
*cnt += 1;
}
None => {
let lock = self
.home_path
.open_rw(".package-cache", self, "package cache lock")
.chain_err(|| "failed to acquire package cache lock")?;
*slot = Some((lock, 1));
let path = ".package-cache";
let desc = "package cache lock";
// First, attempt to open an exclusive lock which is in general
// the purpose of this lock!
//
// If that fails because of a readonly filesystem, though, then
// we don't want to fail because it's a readonly filesystem. In
// some situations Cargo is prepared to have a readonly
// filesystem yet still work since it's all been pre-downloaded
// and/or pre-unpacked. In these situations we want to keep
// Cargo running if possible, so if it's a readonly filesystem
// switch to a shared lock which should hopefully succeed so we
// can continue.
//
// Note that the package cache lock protects files in the same
// directory, so if it's a readonly filesystem we assume that
// the entire package cache is readonly, so we're just acquiring
// something to prove it works, we're not actually doing any
// synchronization at that point.
match self.home_path.open_rw(path, self, desc) {
Ok(lock) => *slot = Some((lock, 1)),
Err(e) => {
if maybe_readonly(&e) {
if let Ok(lock) = self.home_path.open_ro(path, self, desc) {
*slot = Some((lock, 1));
return Ok(PackageCacheLock(self));
}
}
Err(e).chain_err(|| "failed to acquire package cache lock")?;
}
}
}
}
Ok(PackageCacheLock(self))
return Ok(PackageCacheLock(self));
fn maybe_readonly(err: &failure::Error) -> bool {
err.iter_chain().any(|err| {
if let Some(io) = err.downcast_ref::<io::Error>() {
if io.kind() == io::ErrorKind::PermissionDenied {
return true;
}
#[cfg(unix)]
return io.raw_os_error() == Some(libc::EROFS);
}
false
})
}
}
pub fn release_package_cache_lock(&self) {}

View file

@ -1,5 +1,6 @@
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::Path;
use crate::support::cargo_process;
use crate::support::git;
@ -1979,3 +1980,48 @@ fn ignore_invalid_json_lines() {
p.cargo("build").run();
}
#[test]
fn readonly_registry_still_works() {
Package::new("foo", "0.1.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[project]
name = "a"
version = "0.5.0"
authors = []
[dependencies]
foo = '0.1.0'
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("generate-lockfile").run();
p.cargo("fetch --locked").run();
chmod_readonly(&paths::home());
p.cargo("build").run();
fn chmod_readonly(path: &Path) {
for entry in t!(path.read_dir()) {
let entry = t!(entry);
let path = entry.path();
if t!(entry.file_type()).is_dir() {
chmod_readonly(&path);
} else {
set_readonly(&path);
}
}
set_readonly(path);
}
fn set_readonly(path: &Path) {
let mut perms = t!(path.metadata()).permissions();
perms.set_readonly(true);
t!(fs::set_permissions(path, perms));
}
}

View file

@ -158,10 +158,18 @@ where
{
match f(path) {
Ok(()) => {}
Err(ref e) if cfg!(windows) && e.kind() == ErrorKind::PermissionDenied => {
Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
let mut p = t!(path.metadata()).permissions();
p.set_readonly(false);
t!(fs::set_permissions(path, p));
// Unix also requires the parent to not be readonly for example when
// removing files
let parent = path.parent().unwrap();
let mut p = t!(parent.metadata()).permissions();
p.set_readonly(false);
t!(fs::set_permissions(parent, p));
f(path).unwrap_or_else(|e| {
panic!("failed to {} {}: {}", desc, path.display(), e);
})