diff --git a/Cargo.lock b/Cargo.lock index d4afd7c5..fec59991 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1303,6 +1303,7 @@ dependencies = [ "predicates", "regex", "reqwest", + "retry", "semver", "serde", "serde_json", @@ -1868,6 +1869,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "retry" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e211f878258887b3e65dd3c8ff9f530fe109f441a117ee0cdc27f341355032" +dependencies = [ + "rand", +] + [[package]] name = "ring" version = "0.17.14" diff --git a/Cargo.toml b/Cargo.toml index 968fa6db..37d425a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ path-absolutize = "3" numeric-sort = "0.1" regex = "1" toml = "0.9" +retry = "2" [target.'cfg(windows)'.dependencies] windows = { version = "0.62", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects", "Win32_System_Console", "Win32_System_Threading", "Services_Store", "Foundation", "Foundation_Collections", "Web_Http", "Web_Http_Headers", "Storage_Streams", "Management_Deployment"] } diff --git a/src/operations.rs b/src/operations.rs index dfec2ab4..0a17061c 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -14,6 +14,7 @@ use crate::utils::get_bin_dir; use crate::utils::get_julianightlies_base_url; use crate::utils::get_juliaserver_base_url; use crate::utils::is_valid_julia_path; +use crate::utils::retry_rename; use crate::utils::{print_juliaup_style, JuliaupMessageType}; use anyhow::{anyhow, bail, Context, Error, Result}; use bstr::ByteSlice; @@ -676,7 +677,7 @@ pub fn install_from_url( if target_path.exists() { std::fs::remove_dir_all(&target_path)?; } - std::fs::rename(temp_dir.keep(), &target_path)?; + retry_rename(&temp_dir.keep(), &target_path)?; Ok(JuliaupConfigChannel::DirectDownloadChannel { path: path.to_string_lossy().into_owned(), @@ -1585,7 +1586,7 @@ pub fn update_version_db(channel: &Option, paths: &GlobalPaths) -> Resul new_config_file.data.last_version_db_update = Some(chrono::Utc::now()); if let Some(temp_versiondb_download_path) = temp_versiondb_download_path { - std::fs::rename(&temp_versiondb_download_path, &paths.versiondb)?; + retry_rename(&temp_versiondb_download_path, &paths.versiondb)?; } else if delete_old_version_db { let _ = std::fs::remove_file(&paths.versiondb); } diff --git a/src/utils.rs b/src/utils.rs index d5b1c999..f52e7868 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,11 @@ use anyhow::{anyhow, bail, Context, Result}; use console::style; +use retry::{ + delay::{jitter, Fibonacci}, + retry, OperationResult, +}; use semver::{BuildMetadata, Version}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use url::Url; pub fn get_juliaserver_base_url() -> Result { @@ -223,3 +227,29 @@ pub fn print_juliaup_style(action: &str, message: &str, message_type: JuliaupMes eprintln!("{} {}", styled_action, message); } + +/// Retry a rename with Fibonacci backoff to handle transient permission errors +/// from e.g. antivirus scanners. Similar approach to rustup. +pub fn retry_rename(src: &Path, dest: &Path) -> Result<()> { + // 20 fib steps from 1 millisecond sums to ~18 seconds + retry( + Fibonacci::from_millis(1).map(jitter).take(20), + || match std::fs::rename(src, dest) { + Ok(()) => OperationResult::Ok(()), + Err(e) => match e.kind() { + std::io::ErrorKind::PermissionDenied => { + log::debug!("Retrying rename {} to {}.", src.display(), dest.display()); + OperationResult::Retry(e) + } + _ => OperationResult::Err(e), + }, + }, + ) + .with_context(|| { + format!( + "Failed to rename '{}' to '{}'.", + src.display(), + dest.display() + ) + }) +}