@@ -5,11 +5,12 @@ use std::{
55 io:: BufReader ,
66 path:: Path ,
77 process:: { ExitStatus , Stdio } ,
8+ sync:: { Arc , Mutex , OnceLock } ,
89} ;
910
1011use semver:: { Version , VersionReq } ;
1112use 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 } ;
1314use vite_error:: Error ;
1415use vite_path:: { AbsolutePath , AbsolutePathBuf } ;
1516use 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`
325342async 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.
397424async 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