mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-22 20:45:25 +00:00
We lock git checkout directories and the virtualenv to avoid two puffin instances running in parallel changing files at the same time and leading to a broken state. When one instance is blocking another, we need to inform the user (why is the program hanging?) and also add some information for them to debug the situation. The new messages will print ``` Waiting to acquire lock for /home/konsti/projects/puffin/.venv (lockfile: /home/konsti/projects/puffin/.venv/.lock) ``` or ``` Waiting to acquire lock for git+https://github.com/pydantic/pydantic-extra-types@0ce9f207a1e09a862287ab77512f0060c1625223 (lockfile: /home/konsti/projects/puffin/cache-all-kinds/git-v0/locks/f157fd329a506a34) ``` The messages aren't perfect but clear enough to see what the contention is and in the worst case to delete the lockfile. Fixes #714
131 lines
3.8 KiB
Rust
131 lines
3.8 KiB
Rust
use std::fmt::Display;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use fs2::FileExt;
|
|
use fs_err as fs;
|
|
use tempfile::NamedTempFile;
|
|
use tracing::{error, warn};
|
|
|
|
use puffin_warnings::warn_user;
|
|
|
|
/// Write `data` to `path` atomically using a temporary file and atomic rename.
|
|
pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
|
|
let temp_file = NamedTempFile::new_in(
|
|
path.as_ref()
|
|
.parent()
|
|
.expect("Write path must have a parent"),
|
|
)?;
|
|
fs_err::tokio::write(&temp_file, &data).await?;
|
|
temp_file.persist(&path).map_err(|err| {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
format!(
|
|
"Failed to persist temporary file to {}: {}",
|
|
path.as_ref().display(),
|
|
err.error
|
|
),
|
|
)
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Write `data` to `path` atomically using a temporary file and atomic rename.
|
|
pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
|
|
let temp_file = NamedTempFile::new_in(
|
|
path.as_ref()
|
|
.parent()
|
|
.expect("Write path must have a parent"),
|
|
)?;
|
|
fs_err::write(&temp_file, &data)?;
|
|
temp_file.persist(&path).map_err(|err| {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
format!(
|
|
"Failed to persist temporary file to {}: {}",
|
|
path.as_ref().display(),
|
|
err.error
|
|
),
|
|
)
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove the file or directory at `path`, if it exists.
|
|
///
|
|
/// Returns `true` if the file or directory was removed, and `false` if the path did not exist.
|
|
pub fn force_remove_all(path: impl AsRef<Path>) -> Result<bool, std::io::Error> {
|
|
let path = path.as_ref();
|
|
|
|
let metadata = match fs::metadata(path) {
|
|
Ok(metadata) => metadata,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
|
Err(err) => return Err(err),
|
|
};
|
|
|
|
if metadata.is_dir() {
|
|
fs::remove_dir_all(path)?;
|
|
} else {
|
|
fs::remove_file(path)?;
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Iterate over the subdirectories of a directory.
|
|
///
|
|
/// If the directory does not exist, returns an empty iterator.
|
|
pub fn directories(path: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
|
|
path.as_ref()
|
|
.read_dir()
|
|
.ok()
|
|
.into_iter()
|
|
.flatten()
|
|
.filter_map(|entry| match entry {
|
|
Ok(entry) => Some(entry),
|
|
Err(err) => {
|
|
warn!("Failed to read entry: {}", err);
|
|
None
|
|
}
|
|
})
|
|
.filter(|entry| {
|
|
entry
|
|
.file_type()
|
|
.map_or(false, |file_type| file_type.is_dir())
|
|
})
|
|
.map(|entry| entry.path())
|
|
}
|
|
|
|
/// A file lock that is automatically released when dropped.
|
|
#[derive(Debug)]
|
|
pub struct LockedFile(fs_err::File);
|
|
|
|
impl LockedFile {
|
|
pub fn acquire(path: impl AsRef<Path>, resource: impl Display) -> Result<Self, std::io::Error> {
|
|
let file = fs_err::File::create(path.as_ref())?;
|
|
match file.file().try_lock_exclusive() {
|
|
Ok(()) => Ok(Self(file)),
|
|
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
|
|
warn_user!(
|
|
"Waiting to acquire lock for {} (lockfile: {})",
|
|
resource,
|
|
path.as_ref().display()
|
|
);
|
|
file.file().lock_exclusive()?;
|
|
Ok(Self(file))
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for LockedFile {
|
|
fn drop(&mut self) {
|
|
if let Err(err) = self.0.file().unlock() {
|
|
error!(
|
|
"Failed to unlock {}; program may be stuck: {}",
|
|
self.0.path().display(),
|
|
err
|
|
);
|
|
}
|
|
}
|
|
}
|