From 8529bdcdfbcc1c04bf7b8d416fb264fffa69d3fe Mon Sep 17 00:00:00 2001 From: Mikael Rinne <40919111+rorychatt@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:49:23 +0200 Subject: [PATCH 1/4] fix(install): avoid truncating lock file to prevent sharing violation on Windows --- crates/vite_install/src/package_manager.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index bd7c16e4f3..e7e973c7bd 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -469,7 +469,12 @@ pub async fn download_package_manager( // The lock is automatically skipped on NFS filesystems where locking is unreliable. let lock_path = parent_dir.join(format!("{version}.lock")); tracing::debug!("Acquire lock file: {:?}", lock_path); - let lock_file = File::create(lock_path.as_path())?; + let lock_file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(lock_path.as_path())?; // Acquire exclusive lock (blocks until available) lock_file.lock()?; tracing::debug!("Lock acquired: {:?}", lock_path); @@ -602,7 +607,12 @@ async fn download_bun_package_manager( // Acquire lock for atomic rename let lock_path = parent_dir.join(format!("{version}.lock")); tracing::debug!("Acquire lock file: {:?}", lock_path); - let lock_file = File::create(lock_path.as_path())?; + let lock_file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(lock_path.as_path())?; lock_file.lock()?; tracing::debug!("Lock acquired: {:?}", lock_path); From 31b8a9d59d760d956d614c5485d9e3798110c4ee Mon Sep 17 00:00:00 2001 From: Mikael Rinne <40919111+rorychatt@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:29:04 +0200 Subject: [PATCH 2/4] test: add concurrent file lock test and extract open_lock_file helper --- crates/vite_install/src/package_manager.rs | 58 +++++++++++++++++----- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index e7e973c7bd..142754a8bc 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -469,12 +469,7 @@ pub async fn download_package_manager( // The lock is automatically skipped on NFS filesystems where locking is unreliable. let lock_path = parent_dir.join(format!("{version}.lock")); tracing::debug!("Acquire lock file: {:?}", lock_path); - let lock_file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(lock_path.as_path())?; + let lock_file = open_lock_file(lock_path.as_path())?; // Acquire exclusive lock (blocks until available) lock_file.lock()?; tracing::debug!("Lock acquired: {:?}", lock_path); @@ -498,6 +493,18 @@ pub async fn download_package_manager( Ok((install_dir, package_name, version)) } +/// Open a lock file without truncating it. This is required on Windows +/// where `File::create` implies truncation, which is forbidden when another +/// process holds an exclusive lock on the file. +fn open_lock_file(lock_path: &Path) -> io::Result { + fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(lock_path) +} + /// Get the platform-specific npm package name for bun. /// Returns the `@oven/bun-{os}-{arch}` package name for the current platform. fn get_bun_platform_package_name() -> Result<&'static str, Error> { @@ -607,12 +614,7 @@ async fn download_bun_package_manager( // Acquire lock for atomic rename let lock_path = parent_dir.join(format!("{version}.lock")); tracing::debug!("Acquire lock file: {:?}", lock_path); - let lock_file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(lock_path.as_path())?; + let lock_file = open_lock_file(lock_path.as_path())?; lock_file.lock()?; tracing::debug!("Lock acquired: {:?}", lock_path); @@ -2519,4 +2521,36 @@ mod tests { "On musl targets, package name should end with -musl, got: {name}" ); } + /// Note: The true ERROR_SHARING_VIOLATION occurs when *multiple processes* + /// attempt to lock the file concurrently on Windows (e.g. during parallel MSBuild tasks). + /// Standard cargo tests run in a single process, which the Windows OS allows to bypass + /// the truncation violation. This test validates the safe `OpenOptions` syntax + /// and ensures `open_lock_file` successfully acquires and releases locks without panicking. + #[test] + fn test_concurrent_lock_file_creation_windows_compat() { + use std::fs; + + let temp_dir = tempfile::tempdir().unwrap(); + let lock_path = temp_dir.path().join("test_concurrent.lock"); + + // Process 1: Open and acquire exclusive lock using the new approach + let lock_file1 = super::open_lock_file(&lock_path).expect("Failed to open lock file 1"); + + // Acquire lock + lock_file1.lock().expect("Failed to lock file 1"); + + // Process 2: Attempt to open the same file while it is exclusively locked. + // In the buggy implementation (`File::create`), this would throw ERROR_SHARING_VIOLATION + // on Windows because `create` implies `truncate`, which Windows forbids for locked files. + let open_result = super::open_lock_file(&lock_path); + + assert!( + open_result.is_ok(), + "Expected second handle to open successfully even when locked, but got: {:?}", + open_result.err() + ); + + // Release lock + lock_file1.unlock().expect("Failed to unlock file 1"); + } } From 20557dd62073ec1e30a1d73a3eb8adaeaa4cb4e1 Mon Sep 17 00:00:00 2001 From: Mikael Rinne <40919111+rorychatt@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:50:17 +0200 Subject: [PATCH 3/4] fix: remove unused std::fs import in test --- crates/vite_install/src/package_manager.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index 142754a8bc..a8e7c58700 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -2528,8 +2528,6 @@ mod tests { /// and ensures `open_lock_file` successfully acquires and releases locks without panicking. #[test] fn test_concurrent_lock_file_creation_windows_compat() { - use std::fs; - let temp_dir = tempfile::tempdir().unwrap(); let lock_path = temp_dir.path().join("test_concurrent.lock"); From 08d1ff79724ed39802befa1a269ee0453360abd4 Mon Sep 17 00:00:00 2001 From: Mikael Rinne <40919111+rorychatt@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:03:44 +0200 Subject: [PATCH 4/4] style: fix rustfmt issue in open_lock_file --- crates/vite_install/src/package_manager.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index a8e7c58700..cefa127fc3 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -497,12 +497,7 @@ pub async fn download_package_manager( /// where `File::create` implies truncation, which is forbidden when another /// process holds an exclusive lock on the file. fn open_lock_file(lock_path: &Path) -> io::Result { - fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(lock_path) + fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(lock_path) } /// Get the platform-specific npm package name for bun.