Skip to content

Commit d64cba8

Browse files
committed
fix(env): use rename-before-copy for all Windows shims on refresh
On Windows, .exe files may be locked by antivirus, search indexer, or still-running processes. The remove_file call fails with "Access denied" (os error 5), preventing npm/npx/vpx shims from being refreshed. Apply the same rename-to-.old pattern used for vp.exe to all tool shims during --refresh. Cleanup of .old files runs after all shims are created.
1 parent fad87e9 commit d64cba8

1 file changed

Lines changed: 35 additions & 11 deletions

File tree

  • crates/vite_global_cli/src/commands/env

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
7979
}
8080
}
8181

82+
// Best-effort cleanup of .old files from rename-before-copy on Windows
83+
#[cfg(windows)]
84+
if refresh {
85+
cleanup_old_files(&bin_dir).await;
86+
}
87+
8288
// Print results
8389
if !created.is_empty() {
8490
println!("{}", help::render_heading("Created Shims"));
@@ -142,15 +148,7 @@ async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> R
142148
// that launched us). Windows prevents overwriting a running exe, so we
143149
// rename it to a timestamped .old file first, then copy the new one.
144150
if tokio::fs::try_exists(&bin_vp_exe).await.unwrap_or(false) {
145-
let timestamp = std::time::SystemTime::now()
146-
.duration_since(std::time::UNIX_EPOCH)
147-
.unwrap_or_default()
148-
.as_secs();
149-
let old_name = format!("vp.exe.{timestamp}.old");
150-
let old_path = bin_dir.join(&old_name);
151-
if let Err(e) = tokio::fs::rename(&bin_vp_exe, &old_path).await {
152-
tracing::warn!("Failed to rename running vp.exe to {}: {}", old_name, e);
153-
}
151+
rename_to_old(&bin_vp_exe).await;
154152
}
155153

156154
tokio::fs::copy(trampoline_src.as_path(), &bin_vp_exe).await?;
@@ -194,8 +192,15 @@ async fn create_shim(
194192
if !refresh {
195193
return Ok(false);
196194
}
197-
// Remove existing shim for refresh
198-
tokio::fs::remove_file(&shim_path).await?;
195+
// Remove existing shim for refresh.
196+
// On Windows, .exe files may be locked (by antivirus, indexer, or
197+
// still-running processes), so rename to .old first instead of deleting.
198+
#[cfg(windows)]
199+
rename_to_old(&shim_path).await;
200+
#[cfg(not(windows))]
201+
{
202+
tokio::fs::remove_file(&shim_path).await?;
203+
}
199204
}
200205

201206
#[cfg(unix)]
@@ -304,6 +309,25 @@ pub(crate) fn get_trampoline_path() -> Result<vite_path::AbsolutePathBuf, Error>
304309
.ok_or_else(|| Error::ConfigError("Invalid trampoline path".into()))
305310
}
306311

312+
/// Rename an existing `.exe` to a timestamped `.old` file instead of deleting.
313+
///
314+
/// On Windows, running `.exe` files can't be deleted or overwritten, but they can
315+
/// be renamed. The `.old` files are cleaned up by `cleanup_old_files()`.
316+
#[cfg(windows)]
317+
async fn rename_to_old(path: &vite_path::AbsolutePath) {
318+
let timestamp = std::time::SystemTime::now()
319+
.duration_since(std::time::UNIX_EPOCH)
320+
.unwrap_or_default()
321+
.as_secs();
322+
if let Some(name) = path.as_path().file_name().and_then(|n| n.to_str()) {
323+
let old_name = format!("{name}.{timestamp}.old");
324+
let old_path = path.as_path().with_file_name(&old_name);
325+
if let Err(e) = tokio::fs::rename(path, &old_path).await {
326+
tracing::warn!("Failed to rename {} to {}: {}", name, old_name, e);
327+
}
328+
}
329+
}
330+
307331
/// Best-effort cleanup of accumulated `.old` files from previous rename-before-copy operations.
308332
///
309333
/// When refreshing `bin/vp.exe` on Windows, the running trampoline is renamed to a

0 commit comments

Comments
 (0)