From 27069084104676c3f668f6402f60c6585b1ee8e4 Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:53:18 +0800 Subject: [PATCH 01/10] refactor(self-update)!: extract Windows registry cleanup from `do_remove_from_path()` BREAKING CHANGE: Windows registry cleanup is no longer performed by `do_remove_from_path()`. Co-authored-by: baka-gourd <36119339+baka-gourd@users.noreply.github.com> --- src/cli/self_update.rs | 3 +++ src/cli/self_update/windows.rs | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index facfbde9e8..d353f8cb7c 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -1159,6 +1159,9 @@ fn clean_cargo_home(no_modify_path: bool, process: &Process) -> Result<()> { utils::remove_file("rustup_bin", &rustup_path)?; + #[cfg(windows)] + do_remove_from_programs()?; + let cargo_bin_display = cargo_bin.display(); info!("removing empty cargo bin directory `{cargo_bin_display}`"); diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 1dd7e13671..ec4fe089f1 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -567,8 +567,7 @@ where pub(crate) fn do_remove_from_path(process: &Process) -> Result<()> { let new_path = _with_path_cargo_home_bin(_remove_from_path, process)?; - _apply_new_path(new_path)?; - do_remove_from_programs() + _apply_new_path(new_path) } const RUSTUP_UNINSTALL_ENTRY: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup"; From 6a57f02faa063f79d7e52038e5ad5bfe2f1d5036 Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:57:09 +0800 Subject: [PATCH 02/10] refactor(self-update)!: extract add registry from `do_add_to_path()` BREAKING CHANGE: `do_add_to_path()` will not add Windows registry --- src/cli/self_update.rs | 5 ++++- src/cli/self_update/windows.rs | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index d353f8cb7c..1f4d780957 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -92,7 +92,7 @@ pub use windows::complete_windows_uninstall; #[cfg(all(windows, feature = "test"))] pub use windows::{RegistryGuard, RegistryValueId, USER_PATH, get_path}; #[cfg(windows)] -use windows::{do_add_to_path, do_remove_from_path}; +use windows::{do_add_to_path, do_add_to_programs, do_remove_from_path, do_remove_from_programs}; #[cfg(windows)] pub(crate) use windows::{run_update, self_replace}; @@ -1010,6 +1010,9 @@ async fn maybe_install_rust(opts: InstallOpts<'_>, cfg: &mut Cfg<'_>) -> Result< do_add_to_path(cfg.process)?; } + #[cfg(windows)] + do_add_to_programs(cfg.process)?; + // If RUSTUP_HOME is not set, make sure it exists if cfg.process.var_os("RUSTUP_HOME").is_none() { let home = cfg diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index ec4fe089f1..8b1eb6726c 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -449,8 +449,7 @@ pub(crate) fn wait_for_parent() -> Result<()> { pub(crate) fn do_add_to_path(process: &Process) -> Result<()> { let new_path = _with_path_cargo_home_bin(_add_to_path, process)?; - _apply_new_path(new_path)?; - do_add_to_programs(process) + _apply_new_path(new_path) } fn _apply_new_path(new_path: Option) -> Result<()> { From 02516b33c45235a263442ed874b48cbf9a1ea4a5 Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:49:30 +0800 Subject: [PATCH 03/10] test(windows-registry): isolate registry fixtures with UUID roots --- src/cli/self_update.rs | 5 +- src/cli/self_update/windows.rs | 232 +++++++++++++++++++++++----- src/env_var.rs | 4 +- src/test/clitools.rs | 10 ++ tests/suite/cli_inst_interactive.rs | 4 +- tests/suite/cli_paths.rs | 16 +- tests/suite/cli_self_upd.rs | 11 +- 7 files changed, 228 insertions(+), 54 deletions(-) diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 1f4d780957..a48544bd24 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -90,7 +90,10 @@ mod windows; #[cfg(windows)] pub use windows::complete_windows_uninstall; #[cfg(all(windows, feature = "test"))] -pub use windows::{RegistryGuard, RegistryValueId, USER_PATH, get_path}; +pub use windows::{ + RUSTUP_TEST_REGISTRY_UUID, RegistryGuard, RegistryValueId, USER_PATH, get_path, + test_registry_uuid, +}; #[cfg(windows)] use windows::{do_add_to_path, do_add_to_programs, do_remove_from_path, do_remove_from_programs}; #[cfg(windows)] diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 8b1eb6726c..2d56d3af89 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -1,12 +1,18 @@ +#[cfg(any(test, feature = "test"))] +use std::borrow::Cow; use std::env::{consts::EXE_SUFFIX, split_paths}; use std::ffi::{OsStr, OsString}; use std::fmt; +#[cfg(any(test, feature = "test"))] +use std::io as std_io; use std::io::Write; use std::os::windows::ffi::OsStrExt; use std::path::Path; use std::process::Command; #[cfg(any(test, feature = "test"))] -use std::sync::{LockResult, Mutex, MutexGuard}; +use std::ptr; +#[cfg(any(test, feature = "test"))] +use std::sync::{LazyLock, LockResult, Mutex, MutexGuard}; use anyhow::{Context, Result, anyhow}; use tracing::{info, warn}; @@ -14,7 +20,15 @@ use tracing::{info, warn}; use windows_registry::Value; use windows_registry::{CURRENT_USER, HSTRING, Key}; use windows_result::HRESULT; +#[cfg(any(test, feature = "test"))] +use windows_sys::Win32::Foundation::{ + CloseHandle, HANDLE, WAIT_ABANDONED, WAIT_FAILED, WAIT_OBJECT_0, +}; use windows_sys::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_INVALID_DATA}; +#[cfg(any(test, feature = "test"))] +use windows_sys::Win32::System::Threading::{ + CreateMutexW, INFINITE, ReleaseMutex, WaitForSingleObject, +}; use super::super::errors::CliError; use super::common; @@ -464,7 +478,7 @@ fn _apply_new_path(new_path: Option) -> Result<()> { None => return Ok(()), // No need to set the path }; - let environment = CURRENT_USER.create("Environment")?; + let environment = environment_key()?; if new_path.is_empty() { environment.remove_value("PATH")?; @@ -493,9 +507,7 @@ fn _apply_new_path(new_path: Option) -> Result<()> { // this returns None then the PATH variable is not a string and we // should not mess with it. fn get_windows_path_var() -> Result> { - let environment = CURRENT_USER - .create("Environment") - .context("Failed opening Environment key")?; + let environment = environment_key().context("Failed opening Environment key")?; let reg_value = environment.get_hstring("PATH"); match reg_value { @@ -572,9 +584,7 @@ pub(crate) fn do_remove_from_path(process: &Process) -> Result<()> { const RUSTUP_UNINSTALL_ENTRY: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup"; fn rustup_uninstall_reg_key() -> Result { - CURRENT_USER - .create(RUSTUP_UNINSTALL_ENTRY) - .context("Failed creating uninstall key") + current_user_create(RUSTUP_UNINSTALL_ENTRY).context("Failed creating uninstall key") } pub(crate) fn do_update_programs_display_version(version: &str) -> Result<()> { @@ -614,7 +624,7 @@ pub(crate) fn do_add_to_programs(process: &Process) -> Result<()> { } pub(crate) fn do_remove_from_programs() -> Result<()> { - match CURRENT_USER.remove_tree(RUSTUP_UNINSTALL_ENTRY) { + match current_user_remove_tree(RUSTUP_UNINSTALL_ENTRY) { Ok(()) => Ok(()), Err(e) if e.code() == HRESULT::from_win32(ERROR_FILE_NOT_FOUND) => Ok(()), Err(e) => Err(anyhow!(e)), @@ -753,38 +763,183 @@ pub(crate) fn spawn_uninstall_gc(no_modify_path: bool, process: &Process) -> Res // so we use env var here, notifying it if we need to remove $CARGO_HOME/bin from $PATH const GC_MODIFY_PATH: &str = "RUSTUP_GC_MODIFY_PATH"; +#[cfg(any(test, feature = "test"))] +pub const RUSTUP_TEST_RANDOM_REGISTRY_PATH: &str = "RUSTUP_TEST_RANDOM_REGISTRY_PATH"; + +#[cfg(any(test, feature = "test"))] +pub const RUSTUP_TEST_REGISTRY_UUID: &str = "RUSTUP_TEST_REGISTRY_UUID"; + +#[cfg(any(test, feature = "test"))] +static TEST_REGISTRY_UUID: LazyLock> = LazyLock::new(|| { + if let Ok(uuid) = std::env::var(RUSTUP_TEST_REGISTRY_UUID) { + return Some(uuid); + } + + std::env::var_os(RUSTUP_TEST_RANDOM_REGISTRY_PATH) + .is_some() + .then(random_uuid) +}); + +#[cfg(any(test, feature = "test"))] +fn random_uuid() -> String { + let value: u128 = rand::random(); + format!( + "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}", + (value >> 96) as u32, + (value >> 80) as u16, + (value >> 64) as u16, + (value >> 48) as u16, + value & 0x0000_ffff_ffff_ffff + ) +} + +#[cfg(any(test, feature = "test"))] +pub fn test_registry_uuid() -> Option<&'static str> { + TEST_REGISTRY_UUID.as_deref() +} + +#[cfg(any(test, feature = "test"))] +fn test_registry_root(uuid: &str) -> String { + format!(r"Software\Microsoft\Windows\CurrentVersion\Uninstall\RustupTest-{uuid}") +} + +#[cfg(any(test, feature = "test"))] +fn registry_sub_key_path(sub_key: &'static str) -> Cow<'static, str> { + if let Some(uuid) = test_registry_uuid() { + if sub_key == "Environment" { + return Cow::Owned(format!(r"{}\Environment", test_registry_root(uuid))); + } + if sub_key == RUSTUP_UNINSTALL_ENTRY { + return Cow::Owned(format!(r"{}\Programs", test_registry_root(uuid))); + } + } + + Cow::Borrowed(sub_key) +} + +#[cfg(not(any(test, feature = "test")))] +fn registry_sub_key_path(sub_key: &'static str) -> &'static str { + sub_key +} + +fn current_user_create(sub_key: &'static str) -> windows_registry::Result { + CURRENT_USER.create(registry_sub_key_path(sub_key)) +} + +fn current_user_remove_tree(sub_key: &'static str) -> windows_registry::Result<()> { + CURRENT_USER.remove_tree(registry_sub_key_path(sub_key)) +} + +fn environment_key() -> windows_registry::Result { + current_user_create("Environment") +} + #[cfg(any(test, feature = "test"))] pub fn get_path() -> Result> { USER_PATH.get() } #[cfg(any(test, feature = "test"))] -pub struct RegistryGuard<'a> { - _locked: LockResult>, - id: &'static RegistryValueId, - prev: Option, +pub struct RegistryGuard { + _locked: RegistryGuardLock, + values: Vec<(&'static RegistryValueId, Option)>, + cleanup_uuid: Option<&'static str>, } #[cfg(any(test, feature = "test"))] -impl RegistryGuard<'_> { - pub fn new(id: &'static RegistryValueId) -> Result { +impl RegistryGuard { + pub fn new(ids: impl IntoIterator) -> Result { + let locked = RegistryGuardLock::new()?; + let cleanup_uuid = test_registry_uuid(); + let mut ids = ids.into_iter().collect::>(); + ids.sort_unstable_by_key(|id| (id.sub_key, id.value_name)); + ids.dedup_by(|left, right| { + left.sub_key == right.sub_key && left.value_name == right.value_name + }); + + let values = ids + .iter() + .map(|&id| Ok((id, id.get()?))) + .collect::>()?; + Ok(Self { - _locked: REGISTRY_LOCK.lock(), - id, - prev: id.get()?, + _locked: locked, + values, + cleanup_uuid, }) } } #[cfg(any(test, feature = "test"))] -impl Drop for RegistryGuard<'_> { +impl Drop for RegistryGuard { fn drop(&mut self) { - self.id.set(self.prev.as_ref()).unwrap(); + for (id, prev) in self.values.iter().rev() { + id.set(prev.as_ref()).unwrap(); + } + + if let Some(uuid) = self.cleanup_uuid { + match CURRENT_USER.remove_tree(test_registry_root(uuid)) { + Ok(()) => {} + Err(e) if e.code() == HRESULT::from_win32(ERROR_FILE_NOT_FOUND) => {} + Err(e) => panic!("failed to remove test registry root `{uuid}`: {e}"), + } + } } } #[cfg(any(test, feature = "test"))] -static REGISTRY_LOCK: Mutex<()> = Mutex::new(()); +struct RegistryGuardLock { + _thread_lock: LockResult>, + mutex: HANDLE, +} + +#[cfg(any(test, feature = "test"))] +impl RegistryGuardLock { + fn new() -> Result { + let thread_lock = REGISTRY_GUARD_LOCK.lock(); + let name = OsStr::new("RustupTestRegistryGuard") + .encode_wide() + .chain(Some(0)) + .collect::>(); + let mutex = unsafe { CreateMutexW(ptr::null(), 0, name.as_ptr()) }; + if mutex.is_null() { + return Err(std_io::Error::last_os_error()) + .context("failed to create registry test mutex"); + } + + match unsafe { WaitForSingleObject(mutex, INFINITE) } { + WAIT_OBJECT_0 | WAIT_ABANDONED => Ok(Self { + _thread_lock: thread_lock, + mutex, + }), + WAIT_FAILED => { + let error = std_io::Error::last_os_error(); + unsafe { + CloseHandle(mutex); + } + Err(error).context("failed waiting for registry test mutex") + } + code => { + unsafe { + CloseHandle(mutex); + } + Err(anyhow!( + "unexpected registry test mutex wait result: {code}" + )) + } + } + } +} + +#[cfg(any(test, feature = "test"))] +impl Drop for RegistryGuardLock { + fn drop(&mut self) { + unsafe { + ReleaseMutex(self.mutex); + CloseHandle(self.mutex); + } + } +} #[cfg(any(test, feature = "test"))] pub const USER_PATH: RegistryValueId = RegistryValueId { @@ -792,6 +947,9 @@ pub const USER_PATH: RegistryValueId = RegistryValueId { value_name: "PATH", }; +#[cfg(any(test, feature = "test"))] +static REGISTRY_GUARD_LOCK: Mutex<()> = Mutex::new(()); + #[cfg(any(test, feature = "test"))] pub struct RegistryValueId { pub sub_key: &'static str, @@ -801,7 +959,7 @@ pub struct RegistryValueId { #[cfg(any(test, feature = "test"))] impl RegistryValueId { pub fn get(&self) -> Result> { - let sub_key = CURRENT_USER.create(self.sub_key)?; + let sub_key = current_user_create(self.sub_key)?; match sub_key.get_value(self.value_name) { Ok(val) => Ok(Some(val)), Err(e) if e.code() == HRESULT::from_win32(ERROR_FILE_NOT_FOUND) => Ok(None), @@ -810,10 +968,14 @@ impl RegistryValueId { } pub fn set(&self, new: Option<&Value>) -> Result<()> { - let sub_key = CURRENT_USER.create(self.sub_key)?; + let sub_key = current_user_create(self.sub_key)?; match new { Some(new) => Ok(sub_key.set_value(self.value_name, new)?), - None => Ok(sub_key.remove_value(self.value_name)?), + None => match sub_key.remove_value(self.value_name) { + Ok(()) => Ok(()), + Err(e) if e.code() == HRESULT::from_win32(ERROR_FILE_NOT_FOUND) => Ok(()), + Err(e) => Err(e.into()), + }, } } } @@ -863,16 +1025,16 @@ mod tests { #[test] fn windows_path_regkey_type() { // per issue #261, setting PATH should use REG_EXPAND_SZ. - let _guard = RegistryGuard::new(&USER_PATH); - let environment = CURRENT_USER.create("Environment").unwrap(); - environment.remove_value("PATH").unwrap(); + let _guard = RegistryGuard::new([&USER_PATH]); + let environment = environment_key().unwrap(); + let _ = environment.remove_value("PATH"); { // Can't compare the Results as Eq isn't derived; thanks error-chain. #![allow(clippy::unit_cmp)] assert_eq!((), _apply_new_path(Some(HSTRING::from("foo"))).unwrap()); } - let environment = CURRENT_USER.create("Environment").unwrap(); + let environment = environment_key().unwrap(); let path = environment.get_value("PATH").unwrap(); let path_hstring = environment.get_hstring("PATH").unwrap(); assert_eq!(path.ty(), Type::ExpandString); @@ -883,8 +1045,8 @@ mod tests { fn windows_path_delete_key_when_empty() { // during uninstall the PATH key may end up empty; if so we should // delete it. - let _guard = RegistryGuard::new(&USER_PATH); - let environment = CURRENT_USER.create("Environment").unwrap(); + let _guard = RegistryGuard::new([&USER_PATH]); + let environment = environment_key().unwrap(); environment .set_expand_hstring("PATH", &HSTRING::from("foo")) .unwrap(); @@ -912,8 +1074,8 @@ mod tests { .collect(), ); - let _guard = RegistryGuard::new(&USER_PATH); - let environment = CURRENT_USER.create("Environment").unwrap(); + let _guard = RegistryGuard::new([&USER_PATH]); + let environment = environment_key().unwrap(); environment .set_bytes("PATH", Type::Bytes, &[0x12, 0x34]) .unwrap(); @@ -933,9 +1095,9 @@ mod tests { #[test] fn windows_treat_missing_path_as_empty() { // during install the PATH key may be missing; treat it as empty - let _guard = RegistryGuard::new(&USER_PATH); - let environment = CURRENT_USER.create("Environment").unwrap(); - environment.remove_value("PATH").unwrap(); + let _guard = RegistryGuard::new([&USER_PATH]); + let environment = environment_key().unwrap(); + let _ = environment.remove_value("PATH"); assert_eq!(Some(HSTRING::new()), get_windows_path_var().unwrap()); } diff --git a/src/env_var.rs b/src/env_var.rs index 45b19c6389..dcf06669ac 100644 --- a/src/env_var.rs +++ b/src/env_var.rs @@ -68,7 +68,7 @@ mod tests { ); let tp = TestProcess::with_vars(vars); #[cfg(windows)] - let _path_guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); let mut path_entries = vec![]; let mut cmd = Command::new("test"); @@ -116,7 +116,7 @@ mod tests { ); let tp = TestProcess::with_vars(vars); #[cfg(windows)] - let _path_guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); #[track_caller] fn check(tp: &TestProcess, path_entries: Vec, append: &str, expected: &[&str]) { diff --git a/src/test/clitools.rs b/src/test/clitools.rs index 1311322696..cf642bf0d7 100644 --- a/src/test/clitools.rs +++ b/src/test/clitools.rs @@ -62,6 +62,9 @@ pub struct Config { pub workdir: RefCell, /// This is the test root for keeping stuff together pub test_root_dir: PathBuf, + /// Per-test-process Windows registry UUID used when registry isolation is enabled. + #[cfg(windows)] + pub test_registry_uuid: Option, } /// Helper type to simplify assertions of a command's output. @@ -328,6 +331,11 @@ impl Config { if let Some(root) = self.rustup_update_root.as_ref() { cmd.env("RUSTUP_UPDATE_ROOT", root); } + + #[cfg(windows)] + if let Some(uuid) = self.test_registry_uuid.as_ref() { + cmd.env(crate::cli::self_update::RUSTUP_TEST_REGISTRY_UUID, uuid); + } } /// Returns an [`Assert`] object to check the output of running the command @@ -832,6 +840,8 @@ async fn setup_test_state(test_dist_dir: TempDir) -> (TempDir, Config) { rustup_update_root: None, workdir: RefCell::new(workdir), test_root_dir: test_dir.path().to_path_buf(), + #[cfg(windows)] + test_registry_uuid: crate::cli::self_update::test_registry_uuid().map(str::to_owned), }; let build_path = built_exe_dir.join(format!("rustup-init{EXE_SUFFIX}")); diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index 9b75e951c4..cb4bad4f8d 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -45,7 +45,7 @@ fn run_input_with_env(config: &Config, args: &[&str], input: &str, env: &[(&str, async fn update() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); run_input(&cx.config, &["rustup-init"], "\n\n"); run_input(&cx.config, &["rustup-init"], "\n\n").is_ok(); @@ -101,7 +101,7 @@ Rust is installed now. Great! async fn smoke_case_install_with_path_install() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); run_input(&cx.config, &["rustup-init"], "\n\n") .is_ok() diff --git a/tests/suite/cli_paths.rs b/tests/suite/cli_paths.rs index 65b6170aab..a8a3e67ea1 100644 --- a/tests/suite/cli_paths.rs +++ b/tests/suite/cli_paths.rs @@ -479,7 +479,7 @@ mod windows { /// Smoke test for end-to-end code connectivity of the installer path mgmt on windows. async fn install_uninstall_affect_path() { let cx = CliTestContext::new(Scenario::Empty).await; - let _guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _guard = RegistryGuard::new([&USER_PATH]).unwrap(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); let get_path_ = || { HSTRING::try_from(get_path().unwrap().unwrap()) @@ -505,7 +505,7 @@ mod windows { #[tokio::test] async fn uninstall_keeps_path_when_cargo_bin_is_non_empty() { let cx = CliTestContext::new(Scenario::Empty).await; - let _guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _guard = RegistryGuard::new([&USER_PATH]).unwrap(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); let get_path_ = || { HSTRING::try_from(get_path().unwrap().unwrap()) @@ -530,7 +530,7 @@ mod windows { #[tokio::test] async fn uninstall_doesnt_affect_path_with_no_modify_path() { let cx = CliTestContext::new(Scenario::Empty).await; - let _guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _guard = RegistryGuard::new([&USER_PATH]).unwrap(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); let get_path_ = || { HSTRING::try_from(get_path().unwrap().unwrap()) @@ -556,10 +556,10 @@ mod windows { async fn install_uninstall_affect_path_with_non_unicode() { use std::os::windows::ffi::OsStrExt; - use windows_registry::{CURRENT_USER, Type}; + use windows_registry::Type; let cx = CliTestContext::new(Scenario::Empty).await; - let _guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _guard = RegistryGuard::new([&USER_PATH]).unwrap(); // Set up a non unicode PATH let mut reg_value = Value::from([ 0x00, 0xD8, // leading surrogate @@ -567,11 +567,7 @@ mod windows { 0x00, 0x00, // null ]); reg_value.set_ty(Type::ExpandString); - CURRENT_USER - .create("Environment") - .unwrap() - .set_value("PATH", ®_value) - .unwrap(); + USER_PATH.set(Some(®_value)).unwrap(); // compute expected path after installation let mut expected = Value::from( diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index 4f95a9466b..c1969596aa 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -59,7 +59,7 @@ async fn setup_installed() -> CliTestContext { async fn install_bins_to_cargo_home() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); cx.config .expect(["rustup-init", "-y"]) @@ -103,7 +103,7 @@ info: default toolchain set to stable-[HOST_TUPLE] async fn proxies_are_relative_symlinks() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); cx.config .expect(["rustup-init", "-y"]) @@ -143,7 +143,7 @@ info: default toolchain set to stable-[HOST_TUPLE] async fn install_twice() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new(&USER_PATH).unwrap(); + let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); cx.config.expect(["rustup-init", "-y"]).await.is_ok(); cx.config.expect(["rustup-init", "-y"]).await.is_ok(); @@ -473,7 +473,7 @@ async fn update_overwrites_programs_display_version() { let version = env!("CARGO_PKG_VERSION"); let cx = SelfUpdateTestContext::new(TEST_VERSION).await; - let _guard = RegistryGuard::new(&USER_RUSTUP_VERSION).unwrap(); + let _guard = RegistryGuard::new([&USER_RUSTUP_VERSION]).unwrap(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -495,6 +495,9 @@ const USER_RUSTUP_VERSION: RegistryValueId = RegistryValueId { value_name: "DisplayVersion", }; +#[cfg(windows)] +static USER_RUSTUP_VERSION_LOCK: Mutex<()> = Mutex::new(()); + #[tokio::test] async fn update_but_not_installed() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; From 46177e1b3d0e5b310a860a3f60b471925c13c9fb Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:05:45 +0800 Subject: [PATCH 04/10] test(self-update): assert Programs registry values --- tests/suite/cli_self_upd.rs | 50 +++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index c1969596aa..520a20545d 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -52,6 +52,37 @@ async fn setup_installed() -> CliTestContext { cx } +#[cfg(windows)] +fn clear_programs_registry_values() { + for value in [ + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ] { + value.set(None).unwrap(); + } +} + +#[cfg(windows)] +fn assert_programs_registry_values(cx: &CliTestContext) { + let mut rustup = cx.config.cargodir.clone(); + rustup.push(format!("bin\\rustup{EXE_SUFFIX}")); + let expected_uninstall = format!("\"{}\" self uninstall", rustup.display()); + + assert_eq!( + USER_RUSTUP_UNINSTALL_STRING.get().unwrap().unwrap(), + Value::from(expected_uninstall.as_str()) + ); + assert_eq!( + USER_RUSTUP_DISPLAY_NAME.get().unwrap().unwrap(), + Value::from("Rustup: the Rust toolchain installer") + ); + assert_eq!( + USER_RUSTUP_VERSION.get().unwrap().unwrap(), + Value::from(env!("CARGO_PKG_VERSION")) + ); +} + /// This is the primary smoke test testing the full end to end behavior of the /// installation code path: everything that is output, the proxy installation, /// status of the proxies. @@ -473,7 +504,13 @@ async fn update_overwrites_programs_display_version() { let version = env!("CARGO_PKG_VERSION"); let cx = SelfUpdateTestContext::new(TEST_VERSION).await; - let _guard = RegistryGuard::new([&USER_RUSTUP_VERSION]).unwrap(); + let _guard = RegistryGuard::new([ + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ]) + .unwrap(); + clear_programs_registry_values(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -496,7 +533,16 @@ const USER_RUSTUP_VERSION: RegistryValueId = RegistryValueId { }; #[cfg(windows)] -static USER_RUSTUP_VERSION_LOCK: Mutex<()> = Mutex::new(()); +const USER_RUSTUP_UNINSTALL_STRING: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "UninstallString", +}; + +#[cfg(windows)] +const USER_RUSTUP_DISPLAY_NAME: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "DisplayName", +}; #[tokio::test] async fn update_but_not_installed() { From 797bea1ec1cf2f978671084dd790c7b5b844ca57 Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:00:36 +0800 Subject: [PATCH 05/10] test(self-update): cover PATH updates in isolated registry roots --- src/cli/self_update/windows.rs | 28 ++++++++++++++++++++ tests/suite/cli_self_upd.rs | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 2d56d3af89..928903fc77 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -989,6 +989,11 @@ mod tests { use super::*; use crate::process::TestProcess; + const USER_RUSTUP_UNINSTALL_STRING: RegistryValueId = RegistryValueId { + sub_key: RUSTUP_UNINSTALL_ENTRY, + value_name: "UninstallString", + }; + #[test] fn windows_install_does_not_add_path_twice() { assert_eq!( @@ -1000,6 +1005,29 @@ mod tests { ); } + #[test] + fn windows_add_to_path_does_not_add_to_programs() { + let tp = TestProcess::with_vars( + [( + "CARGO_HOME".to_string(), + r"c:\users\example\.cargo".to_string(), + )] + .iter() + .cloned() + .collect(), + ); + let _guard = RegistryGuard::new([&USER_PATH, &USER_RUSTUP_UNINSTALL_STRING]); + let environment = CURRENT_USER.create("Environment").unwrap(); + environment + .set_expand_hstring("PATH", &HSTRING::new()) + .unwrap(); + USER_RUSTUP_UNINSTALL_STRING.set(None).unwrap(); + + do_add_to_path(&tp.process).unwrap(); + + assert!(USER_RUSTUP_UNINSTALL_STRING.get().unwrap().is_none()); + } + #[test] fn windows_handle_non_unicode_path() { let initial_path = vec![ diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index 520a20545d..b228fef148 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -182,6 +182,53 @@ async fn install_twice() { assert!(rustup.exists()); } +#[cfg(windows)] +#[tokio::test] +async fn install_writes_programs_even_with_no_modify_path() { + let cx = CliTestContext::new(Scenario::Empty).await; + let _guard = RegistryGuard::new([ + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ]) + .unwrap(); + clear_programs_registry_values(); + + cx.config + .expect([ + "rustup-init", + "-y", + "--no-modify-path", + "--default-toolchain", + "none", + ]) + .await + .is_ok(); + + assert_programs_registry_values(&cx); +} + +#[cfg(windows)] +#[tokio::test] +async fn install_writes_programs_with_path() { + let cx = CliTestContext::new(Scenario::Empty).await; + let _guard = RegistryGuard::new([ + &USER_PATH, + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ]) + .unwrap(); + clear_programs_registry_values(); + + cx.config + .expect(["rustup-init", "-y", "--default-toolchain", "none"]) + .await + .is_ok(); + + assert_programs_registry_values(&cx); +} + /// Smoke test for the entire install process when dirs need to be made : /// depending just on unit tests here could miss subtle dependencies being added /// earlier in the code, so a black-box test is needed. From def055c23bd58f3a8286f9d4332a6f95890b0497 Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:51:28 +0800 Subject: [PATCH 06/10] test(self-update): cover Programs registry removal --- tests/suite/cli_self_upd.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index b228fef148..70c8a4c49e 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -390,6 +390,38 @@ warn: keeping non-empty cargo bin directory `[..]` assert!(mock_file.exists()); } +#[cfg(windows)] +#[tokio::test] +async fn uninstall_removes_programs_with_no_modify_path() { + let _guard = RegistryGuard::new([ + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ]) + .unwrap(); + clear_programs_registry_values(); + + let cx = setup_empty_installed().await; + + assert_programs_registry_values(&cx); + + cx.config + .expect(["rustup", "self", "uninstall", "-y", "--no-modify-path"]) + .await + .is_ok(); + + retry(Fibonacci::from_millis(1).map(jitter).take(23), || { + if USER_RUSTUP_UNINSTALL_STRING.get().unwrap().is_some() + || USER_RUSTUP_DISPLAY_NAME.get().unwrap().is_some() + || USER_RUSTUP_VERSION.get().unwrap().is_some() + { + return Err("Programs registry values still exist"); + } + Ok(()) + }) + .unwrap() +} + #[tokio::test] async fn uninstall_fails_if_not_installed() { let cx = setup_empty_installed().await; From 9a113c2c6f6f7077354870b83f2ff55c727eb0e1 Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:48:19 +0800 Subject: [PATCH 07/10] test(self-update): guard Windows uninstall registry state --- src/cli/self_update/windows.rs | 2 +- tests/suite/cli_self_upd.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 928903fc77..13b23c48aa 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -1017,7 +1017,7 @@ mod tests { .collect(), ); let _guard = RegistryGuard::new([&USER_PATH, &USER_RUSTUP_UNINSTALL_STRING]); - let environment = CURRENT_USER.create("Environment").unwrap(); + let environment = environment_key().unwrap(); environment .set_expand_hstring("PATH", &HSTRING::new()) .unwrap(); diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index 70c8a4c49e..c91bbc092a 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -83,6 +83,17 @@ fn assert_programs_registry_values(cx: &CliTestContext) { ); } +#[cfg(windows)] +fn self_update_registry_guard() -> RegistryGuard { + RegistryGuard::new([ + &USER_PATH, + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ]) + .unwrap() +} + /// This is the primary smoke test testing the full end to end behavior of the /// installation code path: everything that is output, the proxy installation, /// status of the proxies. @@ -255,6 +266,8 @@ async fn install_creates_cargo_home() { #[tokio::test] async fn uninstall_deletes_bins() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); // no-modify-path isn't needed here, as the test-dir-path isn't present // in the registry, so the no-change code path will be triggered. cx.config @@ -286,6 +299,8 @@ async fn uninstall_deletes_bins() { #[tokio::test] async fn uninstall_works_if_some_bins_dont_exist() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); let rustup = cx.config.cargodir.join(format!("bin/rustup{EXE_SUFFIX}")); let rustc = cx.config.cargodir.join(format!("bin/rustc{EXE_SUFFIX}")); let rustdoc = cx.config.cargodir.join(format!("bin/rustdoc{EXE_SUFFIX}")); @@ -320,6 +335,8 @@ async fn uninstall_works_if_some_bins_dont_exist() { #[tokio::test] async fn uninstall_deletes_rustup_home() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup", "self", "uninstall", "-y"]) .await @@ -330,6 +347,8 @@ async fn uninstall_deletes_rustup_home() { #[tokio::test] async fn uninstall_works_if_rustup_home_doesnt_exist() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config.rustupdir.remove().unwrap(); cx.config .expect(["rustup", "self", "uninstall", "-y"]) @@ -340,6 +359,8 @@ async fn uninstall_works_if_rustup_home_doesnt_exist() { #[tokio::test] async fn uninstall_deletes_cargo_home() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup", "self", "uninstall", "-y"]) .await @@ -350,6 +371,8 @@ async fn uninstall_deletes_cargo_home() { #[tokio::test] async fn complete_uninstall_removes_empty_cargo_bin() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); let cargo_bin = cx.config.cargodir.join("bin"); cx.config .expect(["rustup", "self", "uninstall", "-y"]) @@ -372,6 +395,8 @@ async fn complete_uninstall_removes_empty_cargo_bin() { #[tokio::test] async fn complete_uninstall_keeps_non_empty_cargo_bin() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); let cargo_bin = cx.config.cargodir.join("bin"); let mock_file = cargo_bin.join("custom-tool"); @@ -444,6 +469,8 @@ error: rustup is not installed at '[..]' #[cfg_attr(target_os = "macos", ignore)] // FIXME #1515 async fn uninstall_self_delete_works() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); let rustup = cx.config.cargodir.join(format!("bin/rustup{EXE_SUFFIX}")); let mut cmd = Command::new(rustup.clone()); cmd.args(["self", "uninstall", "-y"]); @@ -482,6 +509,7 @@ async fn uninstall_self_delete_works() { #[cfg(windows)] async fn uninstall_doesnt_leave_gc_file() { let cx = setup_empty_installed().await; + let _guard = self_update_registry_guard(); cx.config .expect(["rustup", "self", "uninstall", "-y"]) .await From 8f6ef85365ddb32532959b0e58a1bade72d4527f Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:50:40 +0800 Subject: [PATCH 08/10] test(self-update): guard Windows install and update registry state --- tests/suite/cli_self_upd.rs | 84 ++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/tests/suite/cli_self_upd.rs b/tests/suite/cli_self_upd.rs index c91bbc092a..a1f1c85d8b 100644 --- a/tests/suite/cli_self_upd.rs +++ b/tests/suite/cli_self_upd.rs @@ -101,7 +101,7 @@ fn self_update_registry_guard() -> RegistryGuard { async fn install_bins_to_cargo_home() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y"]) @@ -145,7 +145,7 @@ info: default toolchain set to stable-[HOST_TUPLE] async fn proxies_are_relative_symlinks() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y"]) @@ -185,7 +185,7 @@ info: default toolchain set to stable-[HOST_TUPLE] async fn install_twice() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = self_update_registry_guard(); cx.config.expect(["rustup-init", "-y"]).await.is_ok(); cx.config.expect(["rustup-init", "-y"]).await.is_ok(); @@ -246,6 +246,8 @@ async fn install_writes_programs_with_path() { #[tokio::test] async fn install_creates_cargo_home() { let cx = CliTestContext::new(Scenario::Empty).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); remove_dir_all(&cx.config.cargodir).unwrap(); cx.config.rustupdir.remove().unwrap(); cx.config @@ -418,12 +420,7 @@ warn: keeping non-empty cargo bin directory `[..]` #[cfg(windows)] #[tokio::test] async fn uninstall_removes_programs_with_no_modify_path() { - let _guard = RegistryGuard::new([ - &USER_RUSTUP_UNINSTALL_STRING, - &USER_RUSTUP_DISPLAY_NAME, - &USER_RUSTUP_VERSION, - ]) - .unwrap(); + let _guard = self_update_registry_guard(); clear_programs_registry_values(); let cx = setup_empty_installed().await; @@ -450,6 +447,8 @@ async fn uninstall_removes_programs_with_no_modify_path() { #[tokio::test] async fn uninstall_fails_if_not_installed() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); let rustup = cx.config.cargodir.join(format!("bin/rustup{EXE_SUFFIX}")); fs::remove_file(rustup).unwrap(); cx.config @@ -556,6 +555,8 @@ struct GcErr(Vec); #[tokio::test] async fn update_exact() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -580,6 +581,8 @@ info: downloading self-update (new version: [TEST_VERSION]) #[tokio::test] async fn update_precise() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -611,12 +614,7 @@ async fn update_overwrites_programs_display_version() { let version = env!("CARGO_PKG_VERSION"); let cx = SelfUpdateTestContext::new(TEST_VERSION).await; - let _guard = RegistryGuard::new([ - &USER_RUSTUP_UNINSTALL_STRING, - &USER_RUSTUP_DISPLAY_NAME, - &USER_RUSTUP_VERSION, - ]) - .unwrap(); + let _guard = self_update_registry_guard(); clear_programs_registry_values(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) @@ -669,6 +667,8 @@ error: rustup is not installed at '[CARGO_DIR]' #[tokio::test] async fn update_but_delete_existing_updater_first() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); // The updater is stored in a known location let setup = cx .config @@ -695,6 +695,8 @@ async fn update_but_delete_existing_updater_first() { #[tokio::test] async fn update_download_404() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -720,6 +722,8 @@ error: could not download file from '[..]' to '[..]': file not found #[tokio::test] async fn update_bogus_version() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -741,6 +745,8 @@ error: invalid value '1.0.0-alpha' for '[TOOLCHAIN]...': invalid toolchain name: #[tokio::test] async fn update_updates_rustup_bin() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(&["rustup-init", "-y", "--no-modify-path"]) .await @@ -769,6 +775,8 @@ async fn update_updates_rustup_bin() { #[tokio::test] async fn update_bad_schema() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -791,6 +799,8 @@ unknown variant [..] async fn update_no_change() { let version = env!("CARGO_PKG_VERSION"); let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -814,6 +824,8 @@ info: checking for self-update (current version: [CURRENT_VERSION]) #[tokio::test] async fn rustup_self_updates_trivial() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup", "set", "auto-self-update", "enable"]) .await @@ -836,6 +848,8 @@ async fn rustup_self_updates_trivial() { #[tokio::test] async fn rustup_self_updates_with_specified_toolchain() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup", "set", "auto-self-update", "enable"]) .await @@ -861,6 +875,8 @@ async fn rustup_self_updates_with_specified_toolchain() { #[tokio::test] async fn rustup_no_self_update_with_specified_toolchain() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -882,6 +898,8 @@ async fn rustup_no_self_update_with_specified_toolchain() { #[tokio::test] async fn rustup_self_update_exact() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup", "set", "auto-self-update", "enable"]) .await @@ -916,6 +934,8 @@ info: cleaning up downloads & tmp directories #[tokio::test] async fn updater_leaves_itself_for_later_deletion() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -936,6 +956,8 @@ async fn updater_leaves_itself_for_later_deletion() { #[tokio::test] async fn updater_is_deleted_after_running_rustup() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -961,6 +983,8 @@ async fn updater_is_deleted_after_running_rustup() { #[tokio::test] async fn updater_is_deleted_after_running_rustc() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -983,6 +1007,8 @@ async fn updater_is_deleted_after_running_rustc() { #[tokio::test] async fn rustup_still_works_after_update() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -1019,6 +1045,8 @@ async fn rustup_still_works_after_update() { #[tokio::test] async fn as_rustup_setup() { let cx = CliTestContext::new(Scenario::Empty).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); let init = cx.config.exedir.join(format!("rustup-init{EXE_SUFFIX}")); let setup = cx.config.exedir.join(format!("rustup-setup{EXE_SUFFIX}")); fs::copy(init, setup).unwrap(); @@ -1037,6 +1065,8 @@ async fn as_rustup_setup() { #[tokio::test] async fn reinstall_exact() { let cx = setup_empty_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect([ "rustup-init", @@ -1056,6 +1086,8 @@ info: updating existing rustup installation - leaving toolchains alone #[tokio::test] async fn reinstall_specifying_toolchain() { let cx = setup_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect([ "rustup-init", @@ -1075,6 +1107,8 @@ async fn reinstall_specifying_toolchain() { #[tokio::test] async fn reinstall_specifying_component() { let cx = setup_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup", "component", "add", "rls"]) .await @@ -1098,6 +1132,8 @@ async fn reinstall_specifying_component() { #[tokio::test] async fn reinstall_specifying_different_toolchain() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect([ "rustup-init", @@ -1117,6 +1153,8 @@ info: default toolchain set to nightly-[HOST_TUPLE] #[tokio::test] async fn install_sets_up_stable_unless_a_different_default_is_requested() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect([ "rustup-init", @@ -1140,6 +1178,8 @@ async fn install_sets_up_stable_unless_a_different_default_is_requested() { #[tokio::test] async fn install_sets_up_stable_unless_there_is_already_a_default() { let cx = setup_installed().await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup", "default", "nightly"]) .await @@ -1189,6 +1229,8 @@ error: unable to read from stdin for confirmation[..] async fn rustup_init_works_with_weird_names() { // Browsers often rename bins to e.g. rustup-init(2).exe. let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); let old = cx.config.exedir.join(format!("rustup-init{EXE_SUFFIX}")); let new = cx.config.exedir.join(format!("rustup-init(2){EXE_SUFFIX}")); fs::rename(old, new).unwrap(); @@ -1203,6 +1245,8 @@ async fn rustup_init_works_with_weird_names() { #[tokio::test] async fn rls_proxy_set_up_after_install() { let mut cx = CliTestContext::new(Scenario::None).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); { let cx = cx.with_dist_dir(Scenario::SimpleV2); @@ -1231,6 +1275,8 @@ help: run `rustup component add rls` to install it #[tokio::test] async fn rls_proxy_set_up_after_update() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); let rls_path = cx.config.cargodir.join(format!("bin/rls{EXE_SUFFIX}")); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) @@ -1244,6 +1290,8 @@ async fn rls_proxy_set_up_after_update() { #[tokio::test] async fn update_does_not_overwrite_rustfmt() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -1292,6 +1340,8 @@ warn: tool `rustfmt` is already installed, remove it from `[..]`, then run `rust #[tokio::test] async fn update_installs_clippy_cargo_and() { let cx = SelfUpdateTestContext::new(TEST_VERSION).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -1309,6 +1359,8 @@ async fn update_installs_clippy_cargo_and() { #[tokio::test] async fn install_with_components_and_targets() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect([ "rustup-init", @@ -1346,6 +1398,8 @@ rls-[HOST_TUPLE] (installed) #[tokio::test] async fn install_minimal_profile() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = self_update_registry_guard(); cx.config .expect([ "rustup-init", From e056c5aa418e7bb84e5b4a661b98ac694162f567 Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:53:58 +0800 Subject: [PATCH 09/10] test(windows-registry): guard installer registry writes across suite --- tests/suite/cli_exact.rs | 35 +++++++++++++++ tests/suite/cli_inst_interactive.rs | 67 +++++++++++++++++++++++++++-- tests/suite/cli_paths.rs | 35 ++++++++++++--- 3 files changed, 129 insertions(+), 8 deletions(-) diff --git a/tests/suite/cli_exact.rs b/tests/suite/cli_exact.rs index e161760b44..1e83c90d05 100644 --- a/tests/suite/cli_exact.rs +++ b/tests/suite/cli_exact.rs @@ -4,8 +4,39 @@ use rustup::test::{ CROSS_ARCH1, CROSS_ARCH2, CliTestContext, MULTI_ARCH1, Scenario, this_host_tuple, }; +#[cfg(windows)] +use rustup::test::{RegistryGuard, RegistryValueId, USER_PATH}; use rustup::utils::raw; +#[cfg(windows)] +const USER_RUSTUP_VERSION: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "DisplayVersion", +}; + +#[cfg(windows)] +const USER_RUSTUP_UNINSTALL_STRING: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "UninstallString", +}; + +#[cfg(windows)] +const USER_RUSTUP_DISPLAY_NAME: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "DisplayName", +}; + +#[cfg(windows)] +fn install_registry_guard() -> RegistryGuard { + RegistryGuard::new([ + &USER_PATH, + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ]) + .unwrap() +} + #[tokio::test] async fn update_once() { let cx = CliTestContext::new(Scenario::SimpleV2).await; @@ -43,6 +74,8 @@ rustc-[HOST_TUPLE] async fn update_once_and_check_self_update() { let test_version = "2.0.0"; let mut cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); let _dist_guard = cx.with_update_server(test_version); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) @@ -88,6 +121,8 @@ rustc-[HOST_TUPLE] async fn update_once_and_self_update() { let test_version = "2.0.0"; let mut cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); let _dist_guard = cx.with_update_server(test_version); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) diff --git a/tests/suite/cli_inst_interactive.rs b/tests/suite/cli_inst_interactive.rs index cb4bad4f8d..3b5eb656cd 100644 --- a/tests/suite/cli_inst_interactive.rs +++ b/tests/suite/cli_inst_interactive.rs @@ -6,9 +6,38 @@ use std::process::Stdio; use rustup::test::{Assert, CliTestContext, Config, SanitizedOutput, Scenario, this_host_tuple}; #[cfg(windows)] -use rustup::test::{RegistryGuard, USER_PATH}; +use rustup::test::{RegistryGuard, RegistryValueId, USER_PATH}; use rustup::utils::raw; +#[cfg(windows)] +const USER_RUSTUP_VERSION: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "DisplayVersion", +}; + +#[cfg(windows)] +const USER_RUSTUP_UNINSTALL_STRING: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "UninstallString", +}; + +#[cfg(windows)] +const USER_RUSTUP_DISPLAY_NAME: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "DisplayName", +}; + +#[cfg(windows)] +fn install_registry_guard() -> RegistryGuard { + RegistryGuard::new([ + &USER_PATH, + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ]) + .unwrap() +} + fn run_input(config: &Config, args: &[&str], input: &str) -> Assert { run_input_with_env(config, args, input, &[]) } @@ -45,7 +74,7 @@ fn run_input_with_env(config: &Config, args: &[&str], input: &str, env: &[(&str, async fn update() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = install_registry_guard(); run_input(&cx.config, &["rustup-init"], "\n\n"); run_input(&cx.config, &["rustup-init"], "\n\n").is_ok(); @@ -57,6 +86,8 @@ async fn update() { #[tokio::test] async fn smoke_case_install_no_modify_path() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); // During an interactive session, after "Press the Enter // key..." the UI emits a blank line, then there is a blank // line that comes from the user pressing enter, then log @@ -101,7 +132,7 @@ Rust is installed now. Great! async fn smoke_case_install_with_path_install() { let cx = CliTestContext::new(Scenario::SimpleV2).await; #[cfg(windows)] - let _path_guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = install_registry_guard(); run_input(&cx.config, &["rustup-init"], "\n\n") .is_ok() @@ -111,6 +142,8 @@ async fn smoke_case_install_with_path_install() { #[tokio::test] async fn blank_lines_around_stderr_log_output_update() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -240,6 +273,8 @@ async fn user_says_nope() { #[tokio::test] async fn with_no_toolchain() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); run_input( &cx.config, &[ @@ -265,6 +300,8 @@ no active toolchain #[tokio::test] async fn with_no_toolchain_doesnt_hang() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); run_input( &cx.config, &[ @@ -282,6 +319,8 @@ async fn with_no_toolchain_doesnt_hang() { #[tokio::test] async fn with_no_toolchain_doesnt_hang_with_concurrent_downloads_override() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); run_input( &cx.config, &[ @@ -302,6 +341,8 @@ async fn with_no_toolchain_doesnt_hang_with_concurrent_downloads_override() { #[tokio::test] async fn with_non_default_toolchain_still_prompts() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); run_input( &cx.config, &[ @@ -329,6 +370,8 @@ nightly-[HOST_TUPLE] (active, default) #[tokio::test] async fn with_non_release_channel_non_default_toolchain() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); run_input( &cx.config, &[ @@ -356,6 +399,8 @@ nightly-2015-01-02-[HOST_TUPLE] (active, default) #[tokio::test] async fn set_nightly_toolchain() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); run_input( &cx.config, &["rustup-init", "--no-modify-path"], @@ -379,6 +424,8 @@ nightly-[HOST_TUPLE] (active, default) #[tokio::test] async fn set_no_modify_path() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); run_input( &cx.config, &["rustup-init", "--no-modify-path"], @@ -394,6 +441,8 @@ async fn set_no_modify_path() { #[tokio::test] async fn set_nightly_toolchain_and_unset() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); run_input( &cx.config, &["rustup-init", "--no-modify-path"], @@ -433,6 +482,8 @@ async fn install_with_components() { args.extend_from_slice(comp_args); let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); cx.config.expect(&args).await.is_ok(); cx.config .expect(["rustup", "component", "list"]) @@ -461,6 +512,8 @@ rust-analysis-[HOST_TUPLE] (installed) #[tokio::test] async fn install_forces_and_skips_rls() { let cx = CliTestContext::new(Scenario::UnavailableRls).await; + #[cfg(windows)] + let _guard = install_registry_guard(); cx.config.set_current_dist_date("2015-01-01"); run_input( @@ -486,6 +539,8 @@ warn: skipping unavailable component rls #[tokio::test] async fn test_warn_if_complete_profile_is_used() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); cx.config .expect([ "rustup-init", @@ -506,6 +561,8 @@ warn: downloading with complete profile isn't recommended unless you are a devel #[tokio::test] async fn installing_when_already_installed_updates_toolchain() { let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); cx.config .expect(["rustup-init", "-y", "--no-modify-path"]) .await @@ -589,6 +646,8 @@ async fn with_no_prompt_install_succeeds_if_rustc_exists() { let temp_dir_path = temp_dir.path().to_str().unwrap(); let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); cx.config .expect_with_env( ["rustup-init", "-y", "--no-modify-path"], @@ -643,6 +702,8 @@ version = "12""#, let temp_dir_path = temp_dir.path().to_str().unwrap(); let cx = CliTestContext::new(Scenario::SimpleV2).await; + #[cfg(windows)] + let _guard = install_registry_guard(); cx.config .expect_with_env( ["rustup-init", "-y", "--no-modify-path"], diff --git a/tests/suite/cli_paths.rs b/tests/suite/cli_paths.rs index a8a3e67ea1..1d0d0bdbc8 100644 --- a/tests/suite/cli_paths.rs +++ b/tests/suite/cli_paths.rs @@ -471,15 +471,40 @@ error: could not amend shell profile[..] mod windows { use super::INIT_NONE; use rustup::test::{CliTestContext, Scenario}; - use rustup::test::{RegistryGuard, USER_PATH, get_path}; + use rustup::test::{RegistryGuard, RegistryValueId, USER_PATH, get_path}; use windows_registry::{HSTRING, Value}; + const USER_RUSTUP_VERSION: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "DisplayVersion", + }; + + const USER_RUSTUP_UNINSTALL_STRING: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "UninstallString", + }; + + const USER_RUSTUP_DISPLAY_NAME: RegistryValueId = RegistryValueId { + sub_key: r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rustup", + value_name: "DisplayName", + }; + + fn install_registry_guard() -> RegistryGuard { + RegistryGuard::new([ + &USER_PATH, + &USER_RUSTUP_UNINSTALL_STRING, + &USER_RUSTUP_DISPLAY_NAME, + &USER_RUSTUP_VERSION, + ]) + .unwrap() + } + #[tokio::test] /// Smoke test for end-to-end code connectivity of the installer path mgmt on windows. async fn install_uninstall_affect_path() { let cx = CliTestContext::new(Scenario::Empty).await; - let _guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = install_registry_guard(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); let get_path_ = || { HSTRING::try_from(get_path().unwrap().unwrap()) @@ -505,7 +530,7 @@ mod windows { #[tokio::test] async fn uninstall_keeps_path_when_cargo_bin_is_non_empty() { let cx = CliTestContext::new(Scenario::Empty).await; - let _guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = install_registry_guard(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); let get_path_ = || { HSTRING::try_from(get_path().unwrap().unwrap()) @@ -530,7 +555,7 @@ mod windows { #[tokio::test] async fn uninstall_doesnt_affect_path_with_no_modify_path() { let cx = CliTestContext::new(Scenario::Empty).await; - let _guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = install_registry_guard(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); let get_path_ = || { HSTRING::try_from(get_path().unwrap().unwrap()) @@ -559,7 +584,7 @@ mod windows { use windows_registry::Type; let cx = CliTestContext::new(Scenario::Empty).await; - let _guard = RegistryGuard::new([&USER_PATH]).unwrap(); + let _guard = install_registry_guard(); // Set up a non unicode PATH let mut reg_value = Value::from([ 0x00, 0xD8, // leading surrogate From 59c661a277ba7dacdad119c69ab03072bfeb3dba Mon Sep 17 00:00:00 2001 From: baka-gourd <36119339+baka-gourd@users.noreply.github.com> Date: Sun, 21 Jun 2026 13:58:05 +0800 Subject: [PATCH 10/10] fix(windows-registry): handle concurrent registry guard opens On Windows, fall back to `OpenMutexW` when `CreateMutexW` hits `ERROR_ACCESS_DENIED` for an existing registry guard mutex. This lets concurrent test processes wait on the shared mutex instead of failing during guard construction. Also update PATH registry tests for UUID-isolated roots, where the initial PATH may be absent and uninstall may correctly restore it to `None`. On Windows, fall back to `OpenMutexW` when `CreateMutexW` hits `ERROR_ACCESS_DENIED` for an existing registry guard mutex. This lets concurrent test processes wait on the shared mutex instead of failing during guard construction. Also update PATH registry tests for UUID-isolated roots, where the initial PATH may be absent and uninstall may correctly restore it to `None`. --- src/cli/self_update/windows.rs | 19 ++++++++++++--- tests/suite/cli_paths.rs | 42 +++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/cli/self_update/windows.rs b/src/cli/self_update/windows.rs index 13b23c48aa..605c9c9460 100644 --- a/src/cli/self_update/windows.rs +++ b/src/cli/self_update/windows.rs @@ -22,12 +22,12 @@ use windows_registry::{CURRENT_USER, HSTRING, Key}; use windows_result::HRESULT; #[cfg(any(test, feature = "test"))] use windows_sys::Win32::Foundation::{ - CloseHandle, HANDLE, WAIT_ABANDONED, WAIT_FAILED, WAIT_OBJECT_0, + CloseHandle, ERROR_ACCESS_DENIED, HANDLE, WAIT_ABANDONED, WAIT_FAILED, WAIT_OBJECT_0, }; use windows_sys::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_INVALID_DATA}; #[cfg(any(test, feature = "test"))] use windows_sys::Win32::System::Threading::{ - CreateMutexW, INFINITE, ReleaseMutex, WaitForSingleObject, + CreateMutexW, INFINITE, MUTEX_MODIFY_STATE, OpenMutexW, ReleaseMutex, WaitForSingleObject, }; use super::super::errors::CliError; @@ -902,9 +902,19 @@ impl RegistryGuardLock { .chain(Some(0)) .collect::>(); let mutex = unsafe { CreateMutexW(ptr::null(), 0, name.as_ptr()) }; + let mutex = if mutex.is_null() { + let error = std_io::Error::last_os_error(); + if error.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) { + unsafe { OpenMutexW(SYNCHRONIZE_ACCESS | MUTEX_MODIFY_STATE, 0, name.as_ptr()) } + } else { + return Err(error).context("failed to create registry test mutex"); + } + } else { + mutex + }; if mutex.is_null() { return Err(std_io::Error::last_os_error()) - .context("failed to create registry test mutex"); + .context("failed to open registry test mutex"); } match unsafe { WaitForSingleObject(mutex, INFINITE) } { @@ -950,6 +960,9 @@ pub const USER_PATH: RegistryValueId = RegistryValueId { #[cfg(any(test, feature = "test"))] static REGISTRY_GUARD_LOCK: Mutex<()> = Mutex::new(()); +#[cfg(any(test, feature = "test"))] +const SYNCHRONIZE_ACCESS: u32 = 0x0010_0000; + #[cfg(any(test, feature = "test"))] pub struct RegistryValueId { pub sub_key: &'static str, diff --git a/tests/suite/cli_paths.rs b/tests/suite/cli_paths.rs index 1d0d0bdbc8..aff38aab15 100644 --- a/tests/suite/cli_paths.rs +++ b/tests/suite/cli_paths.rs @@ -506,25 +506,31 @@ mod windows { let cx = CliTestContext::new(Scenario::Empty).await; let _guard = install_registry_guard(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); - let get_path_ = || { - HSTRING::try_from(get_path().unwrap().unwrap()) + let get_path_ = || -> Option { + get_path() .unwrap() - .to_string() + .map(|value| HSTRING::try_from(value).unwrap().to_string()) }; cx.config.expect(&INIT_NONE).await.is_ok(); assert!( - get_path_().contains(cfg_path.trim_matches('"')), + get_path_() + .as_deref() + .is_some_and(|path| path.contains(cfg_path.trim_matches('"'))), "`{}` not in `{}`", cfg_path, - get_path_() + get_path_().unwrap_or_default() ); cx.config .expect(&["rustup", "self", "uninstall", "-y"]) .await .is_ok(); - assert!(!get_path_().contains(&cfg_path)); + assert!( + get_path_() + .as_deref() + .is_none_or(|path| !path.contains(&cfg_path)) + ); } #[tokio::test] @@ -532,10 +538,10 @@ mod windows { let cx = CliTestContext::new(Scenario::Empty).await; let _guard = install_registry_guard(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); - let get_path_ = || { - HSTRING::try_from(get_path().unwrap().unwrap()) + let get_path_ = || -> Option { + get_path() .unwrap() - .to_string() + .map(|value| HSTRING::try_from(value).unwrap().to_string()) }; cx.config.expect(&INIT_NONE).await.is_ok(); @@ -545,10 +551,12 @@ mod windows { .await .is_ok(); assert!( - get_path_().contains(cfg_path.trim_matches('"')), + get_path_() + .as_deref() + .is_some_and(|path| path.contains(cfg_path.trim_matches('"'))), "`{}` not in `{}`", cfg_path, - get_path_() + get_path_().unwrap_or_default() ); } @@ -557,10 +565,10 @@ mod windows { let cx = CliTestContext::new(Scenario::Empty).await; let _guard = install_registry_guard(); let cfg_path = cx.config.cargodir.join("bin").display().to_string(); - let get_path_ = || { - HSTRING::try_from(get_path().unwrap().unwrap()) + let get_path_ = || -> Option { + get_path() .unwrap() - .to_string() + .map(|value| HSTRING::try_from(value).unwrap().to_string()) }; cx.config.expect(&INIT_NONE).await.is_ok(); @@ -569,10 +577,12 @@ mod windows { .await .is_ok(); assert!( - get_path_().contains(cfg_path.trim_matches('"')), + get_path_() + .as_deref() + .is_some_and(|path| path.contains(cfg_path.trim_matches('"'))), "`{}` not in `{}`", cfg_path, - get_path_() + get_path_().unwrap_or_default() ); }