Skip to content

Commit 19076a6

Browse files
committed
fix: lock to ensure atomicity of remove + rename operations
1 parent 4bb4def commit 19076a6

1 file changed

Lines changed: 33 additions & 5 deletions

File tree

crates/vite_install/src/package_manager.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ use std::{
55
io::BufReader,
66
path::Path,
77
process::{ExitStatus, Stdio},
8+
sync::{Arc, Mutex, OnceLock},
89
};
910

1011
use semver::{Version, VersionReq};
1112
use serde::{Deserialize, Serialize};
12-
use tokio::{fs::remove_dir_all, process::Command};
13+
use tokio::{fs::remove_dir_all, process::Command, sync::Mutex as TokioMutex};
1314
use vite_error::Error;
1415
use vite_path::{AbsolutePath, AbsolutePathBuf};
1516
use vite_str::Str;
@@ -320,6 +321,22 @@ async fn get_latest_version(package_manager_type: PackageManagerType) -> Result<
320321
Ok(package_json.version)
321322
}
322323

324+
/// Get a per-directory lock for atomic remove + rename operations.
325+
/// This ensures that multiple concurrent threads won't conflict when
326+
/// installing the same package manager version.
327+
fn get_directory_lock(path: impl AsRef<Path>) -> Arc<TokioMutex<()>> {
328+
static LOCKS: OnceLock<Mutex<HashMap<String, Arc<TokioMutex<()>>>>> = OnceLock::new();
329+
330+
let locks_map = LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
331+
let path_str = path.as_ref().to_string_lossy().to_string();
332+
333+
// Acquire the mutex for the locks map
334+
// This is safe because we're only doing a quick HashMap lookup
335+
let mut map = locks_map.lock().unwrap();
336+
337+
map.entry(path_str).or_insert_with(|| Arc::new(TokioMutex::new(()))).clone()
338+
}
339+
323340
/// Download the package manager and extract it to the cache directory.
324341
/// Return the install directory, e.g. $`CACHE_DIR/vite/package_manager/pnpm/10.0.0/pnpm`
325342
async fn download_package_manager(
@@ -374,9 +391,19 @@ async fn download_package_manager(
374391
tracing::debug!("Rename package dir to {}", bin_name);
375392
tokio::fs::rename(&target_dir_tmp.join("package"), &target_dir_tmp.join(&bin_name)).await?;
376393

377-
// check bin_file again, for the concurrent download cases
378-
if is_exists_file(&bin_file)? {
379-
tracing::debug!("bin_file already exists, skip rename");
394+
// Use a per-directory lock to ensure atomicity of remove + rename operations
395+
// This prevents DirectoryNotEmpty exceptions when multiple threads try to install
396+
// the same package manager version concurrently.
397+
let dir_lock = get_directory_lock(&target_dir);
398+
let _guard = dir_lock.lock().await;
399+
400+
// Check again after acquiring the lock, in case another thread completed
401+
// the installation while we were downloading
402+
if is_exists_file(&bin_file)?
403+
&& is_exists_file(bin_file.with_extension("cmd"))?
404+
&& is_exists_file(bin_file.with_extension("ps1"))?
405+
{
406+
tracing::debug!("bin_file already exists after lock acquisition, skip rename");
380407
return Ok(install_dir);
381408
}
382409

@@ -395,12 +422,13 @@ async fn download_package_manager(
395422
/// Remove the directory and all its contents.
396423
/// Ignore the error if the directory is not found.
397424
async fn remove_dir_all_force(path: impl AsRef<Path>) -> Result<(), std::io::Error> {
398-
match remove_dir_all(path).await {
425+
match remove_dir_all(path.as_ref()).await {
399426
Ok(()) => Ok(()),
400427
Err(e) => {
401428
if e.kind() == std::io::ErrorKind::NotFound {
402429
Ok(())
403430
} else {
431+
tracing::error!("remove_dir_all_force path: {:?} error: {e:?}", path.as_ref());
404432
Err(e)
405433
}
406434
}

0 commit comments

Comments
 (0)