Skip to content

Commit 201a5aa

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

6 files changed

Lines changed: 36 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ jobs:
172172

173173
- name: Run CLI E2E tests
174174
run: |
175-
pnpm test
175+
RUST_BACKTRACE=1 pnpm test
176176
git diff --exit-code
177177
178178
install-e2e-test:

Cargo.lock

Lines changed: 14 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ criterion = { version = "0.7", features = ["html_reports"] }
4141
crossterm = { version = "0.29.0", features = ["event-stream"] }
4242
directories = "6.0.0"
4343
flate2 = "1.0.35"
44+
fs4 = "0.13"
4445
futures-util = "0.3.31"
4546
hex = "0.4.3"
4647
httpmock = "0.7"

crates/vite_install/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ rust-version.workspace = true
1111
backon = { workspace = true }
1212
directories = { workspace = true }
1313
flate2 = { workspace = true }
14+
fs4 = { workspace = true }
1415
futures-util = { workspace = true }
1516
hex = { workspace = true }
1617
indoc = { workspace = true }

crates/vite_install/src/package_manager.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
process::{ExitStatus, Stdio},
88
};
99

10+
use fs4::fs_std::FileExt;
1011
use semver::{Version, VersionReq};
1112
use serde::{Deserialize, Serialize};
1213
use tokio::{fs::remove_dir_all, process::Command};
@@ -374,9 +375,21 @@ async fn download_package_manager(
374375
tracing::debug!("Rename package dir to {}", bin_name);
375376
tokio::fs::rename(&target_dir_tmp.join("package"), &target_dir_tmp.join(&bin_name)).await?;
376377

377-
// check bin_file again, for the concurrent download cases
378+
// Use a file-based lock to ensure atomicity of remove + rename operations
379+
// This prevents DirectoryNotEmpty exceptions when multiple processes/threads
380+
// try to install the same package manager version concurrently.
381+
// Create lock file with a unique name based on the target directory
382+
let lock_path = parent_dir.join(format!("{version}.lock"));
383+
tracing::debug!("Acquiring lock file: {:?}", lock_path);
384+
let _lock_file = File::create(&lock_path)?;
385+
// Acquire exclusive lock (blocks until available)
386+
_lock_file.lock_exclusive()?;
387+
tracing::debug!("Lock acquired: {:?}", lock_path);
388+
389+
// Check again after acquiring the lock, in case another thread completed
390+
// the installation while we were downloading
378391
if is_exists_file(&bin_file)? {
379-
tracing::debug!("bin_file already exists, skip rename");
392+
tracing::debug!("bin_file already exists after lock acquisition, skip rename");
380393
return Ok(install_dir);
381394
}
382395

@@ -395,12 +408,13 @@ async fn download_package_manager(
395408
/// Remove the directory and all its contents.
396409
/// Ignore the error if the directory is not found.
397410
async fn remove_dir_all_force(path: impl AsRef<Path>) -> Result<(), std::io::Error> {
398-
match remove_dir_all(path).await {
411+
match remove_dir_all(path.as_ref()).await {
399412
Ok(()) => Ok(()),
400413
Err(e) => {
401414
if e.kind() == std::io::ErrorKind::NotFound {
402415
Ok(())
403416
} else {
417+
tracing::error!("remove_dir_all_force path: {:?} error: {e:?}", path.as_ref());
404418
Err(e)
405419
}
406420
}

packages/tools/src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ const DEFAULT_PASSTHROUGH_ENVS = [
126126
'*_TOKEN',
127127
// oxc specific
128128
'OXLINT_*',
129+
// Rust specific
130+
'RUST_*',
129131
].map(env => new Minimatch(env));
130132

131133
export function isPassThroughEnv(env: string) {

0 commit comments

Comments
 (0)