fix(msi): keep only native ProductCode uninstall entry (#15891)

* fix(msi): keep only native ProductCode uninstall entry

Move installer state outside the Uninstall registry path,
clean up legacy duplicate entries, and use the MSI ProductCode
for updates and uninstalling.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): harden update and uninstall handling

- handle legacy EXE updates without an MSI ProductCode
- propagate MsiExec uninstall failures
- validate and XML-quote custom ARP values

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): validate registry state before update and uninstall

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): pass WindowsInstaller state to elevated sequence

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): block unsupported MSI-to-EXE upgrades

- resolve native MSI state and ProductCode safely
- suppress reboot while preserving MSI uninstall results
- publish the resolved ARP install location
- skip invalid unrelated MSI uninstall entries

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): fail uninstall when ProductCode is missing

Prevent known MSI installations from falling back to
EXE cleanup when the ProductCode cannot be resolved.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(msi): do not abort update on ARP version write failure

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-08-22 17:49:00 +08:00
committed by GitHub
parent d5a7f67999
commit a7deef02a2
6 changed files with 348 additions and 165 deletions
+36 -7
View File
@@ -5,6 +5,23 @@
<Fragment>
<!-- Regs for shortcuts are defined in "Fragments/ShortcutProperties.wxs" -->
<!-- Component that persists the property values to the registry so they are available during an upgrade/modify -->
<Property Id="CURRENT_SHARE_RDP" Secure="yes">
<RegistrySearch Id="CurrentShareRdpSearch" Root="HKLM" Key="Software\$(var.Product)\InstallState\$(var.Product)" Name="share_rdp" Type="raw" />
</Property>
<Property Id="LEGACY_SHARE_RDP" Secure="yes">
<RegistrySearch Id="LegacyShareRdpSearch" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="share_rdp" Type="raw" Bitness="always64" />
</Property>
<Property Id="LEGACY_SHARE_RDP32" Secure="yes">
<RegistrySearch Id="LegacyShareRdp32Search" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="share_rdp" Type="raw" Bitness="always32" />
</Property>
<Property Id="APP_WINDOWS_INSTALLER32" Secure="yes">
<RegistrySearch Id="AppWindowsInstaller32Search" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="WindowsInstaller" Type="raw" Bitness="always32" />
</Property>
<Property Id="SHARE_RDP" Secure="yes" />
<SetProperty Action="SetShareRdpFromCurrentState" Id="SHARE_RDP" Value="[CURRENT_SHARE_RDP]" After="AppSearch" Sequence="first" Condition="CURRENT_SHARE_RDP" />
<SetProperty Action="SetShareRdpFromLegacyState" Id="SHARE_RDP" Value="[LEGACY_SHARE_RDP]" After="SetShareRdpFromCurrentState" Sequence="first" Condition="NOT CURRENT_SHARE_RDP AND LEGACY_SHARE_RDP" />
<SetProperty Action="SetShareRdpFromLegacyState32" Id="SHARE_RDP" Value="[LEGACY_SHARE_RDP32]" After="SetShareRdpFromLegacyState" Sequence="first" Condition="NOT CURRENT_SHARE_RDP AND NOT LEGACY_SHARE_RDP AND LEGACY_SHARE_RDP32" />
<DirectoryRef Id="INSTALLFOLDER_INNER">
<Component Id="Product.Registry.InstallFolder" Guid="3196EDA7-9AEF-4705-A0C8-E3F3ECCCB153">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
@@ -40,17 +57,29 @@
</RegistryKey>
</Component>
<!--For compatibility with registry values from previous versions-->
<Component Id="Product.Registry.UninstallApp" Guid="FC1A3D2E-5642-FBD8-CFA6-5ECAC6DE69A8">
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" >
<Component Id="Product.Registry.InstallState" Guid="E310003A-C292-4851-AE20-7A9B186060CE">
<RegistryKey Root="HKLM" Key="Software\$(var.Product)\InstallState\$(var.Product)" ForceDeleteOnUninstall="yes">
<RegistryValue Type="string" Name="BuildDate" Value="$(var.BuildDate)" />
<RegistryValue Type="string" Name="share_rdp" Value="" />
<!--$ArpStart$-->
<!--$ArpEnd$-->
<RegistryValue Type="string" Name="share_rdp" Value="[SHARE_RDP]" />
<RegistryValue Type="string" Name="InstallLocation" Value="[INSTALLFOLDER_INNER]" />
<RegistryValue Type="integer" Name="WindowsInstaller" Value="1" />
<RegistryValue Type="string" Name="MsiProductCode" Value="[ProductCode]" KeyPath="yes" />
<!--$InstallStateStart$-->
<!--$InstallStateEnd$-->
</RegistryKey>
</Component>
</DirectoryRef>
<StandardDirectory Id="CommonAppDataFolder">
<Component Id="Product.Registry.RemoveLegacyUninstall64" Guid="7524B741-94C2-4C50-A151-DDB37860DC20" Bitness="always64" KeyPath="yes" Condition="APP_WINDOWS_INSTALLER=&quot;#1&quot;">
<RemoveRegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Action="removeOnInstall" />
</Component>
<Component Id="Product.Registry.RemoveLegacyUninstall32" Guid="CB50B9AA-B83A-4AC2-BB27-81358F0E74E6" Bitness="always32" KeyPath="yes" Condition="APP_WINDOWS_INSTALLER32=&quot;#1&quot;">
<RemoveRegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Action="removeOnInstall" />
</Component>
</StandardDirectory>
</Fragment>
</Wix>
@@ -27,10 +27,12 @@
<!--$ArpStart$-->
<!--$ArpEnd$-->
<SetProperty Action="SetArpInstallLocation" Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER_INNER]" After="CostFinalize" Sequence="execute" />
<!--$CustomClientPropsStart$-->
<!--$CustomClientPropsEnd$-->
<Property Id="APP_WINDOWS_INSTALLER">
<Property Id="APP_WINDOWS_INSTALLER" Secure="yes">
<RegistrySearch Id="AppWindowsInstallerFolderSearch" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="WindowsInstaller" Type="raw" />
</Property>
</Fragment>
+6 -2
View File
@@ -14,6 +14,7 @@
<Media Id="1" Cabinet="cab1.cab" EmbedCab="yes" CompressionLevel="high" />
<Icon Id="AppIcon" SourceFile="Resources\icon.ico" />
<CustomAction Id="BlockSelfInstalledApp" Error="!(loc.AnotherAppDialogDescription)" />
<!-- User Interface -->
<WixVariable Id="WixUILicenseRtf" Value="License.rtf" />
@@ -22,10 +23,11 @@
<UIRef Id="WixUI_ErrorProgressText" />
<InstallUISequence>
<Show Dialog="UI_AnotherAppDialog" Before="WelcomeDlg" Condition="Not installed AND APP_WINDOWS_INSTALLER=&quot;#0&quot;"/>
<Show Dialog="UI_AnotherAppDialog" Before="WelcomeDlg" Condition="NOT Installed AND (APP_WINDOWS_INSTALLER=&quot;#0&quot; OR APP_WINDOWS_INSTALLER32=&quot;#0&quot;)"/>
</InstallUISequence>
<InstallExecuteSequence>
<Custom Action="BlockSelfInstalledApp" After="AppSearch" Condition="NOT Installed AND (APP_WINDOWS_INSTALLER=&quot;#0&quot; OR APP_WINDOWS_INSTALLER32=&quot;#0&quot;)" />
<InstallExecute After="RemoveExistingProducts" />
<!--Only do InstallValidate if is not Uninstall-->
@@ -45,7 +47,9 @@
<ComponentRef Id="Product.Registry.CommandPlay" />
<ComponentRef Id="Product.Registry.URLProtocol" />
<ComponentRef Id="Product.Registry.Command" />
<ComponentRef Id="Product.Registry.UninstallApp" />
<ComponentRef Id="Product.Registry.InstallState" />
<ComponentRef Id="Product.Registry.RemoveLegacyUninstall64" />
<ComponentRef Id="Product.Registry.RemoveLegacyUninstall32" />
<ComponentRef Id="App.StartMenu" />
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties1" />
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties0" />
+32 -104
View File
@@ -12,6 +12,7 @@ import platform
from pathlib import Path
from itertools import chain
import shutil
from xml.sax.saxutils import quoteattr
g_indent_unit = "\t"
g_version = ""
@@ -54,7 +55,7 @@ def make_parser():
parser.add_argument(
"--arp",
action="store_true",
help="Is ARPSYSTEMCOMPONENT",
help="Deprecated; native MSI ARP registration is always used.",
default=False,
)
parser.add_argument(
@@ -258,25 +259,19 @@ def gen_custom_dialog_bitmaps():
)
def gen_custom_ARPSYSTEMCOMPONENT_False(args):
def gen_native_arp_properties():
def func(lines, index_start):
indent = g_indent_unit * 2
lines_new = []
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/arpsystemcomponent?redirectedfrom=MSDN-->\n"
)
lines_new.append(
f'{indent}<!--<Property Id="ARPSYSTEMCOMPONENT" Value="1" />-->\n\n'
)
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
)
for _, v in g_arpsystemcomponent.items():
if "msi" in v and "v" in v:
lines_new.append(
f'{indent}<Property Id="{v["msi"]}" Value="{v["v"]}" />\n'
f'{indent}<Property Id={quoteattr(str(v["msi"]))} '
f'Value={quoteattr(str(v["v"]))} />\n'
)
for i, line in enumerate(lines_new):
@@ -291,94 +286,16 @@ def gen_custom_ARPSYSTEMCOMPONENT_False(args):
)
def get_folder_size(folder_path):
total_size = 0
folder = Path(folder_path)
for file in folder.glob("**/*"):
if file.is_file():
total_size += file.stat().st_size
return total_size
def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
def gen_install_state_values():
def func(lines, index_start):
indent = g_indent_unit * 5
lines_new = []
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayName" Value="{args.app_name}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayIcon" Value="[INSTALLFOLDER_INNER]{args.app_name}.exe" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayVersion" Value="{g_version}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="Publisher" Value="{args.manufacturer}" />\n'
)
installDate = datetime.datetime.now().strftime("%Y%m%d")
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallDate" Value="{installDate}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallLocation" Value="[INSTALLFOLDER_INNER]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallSource" Value="[InstallSource]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
)
# EstimatedSize in uninstall registry must be in KB.
estimated_size_bytes = get_folder_size(dist_dir)
estimated_size = max(1, (estimated_size_bytes + 1023) // 1024)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="ModifyPath" Value="MsiExec.exe /X [ProductCode]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Id="NoModify" Value="1" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="UninstallString" Value="MsiExec.exe /X [ProductCode]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="QuietUninstallString" Value="MsiExec.exe /qn /X [ProductCode]" />\n'
)
vs = g_version.split(".")
major, minor, build = vs[0], vs[1], vs[2]
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="Version" Value="{g_version}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionMajor" Value="{major}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionMinor" Value="{minor}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionBuild" Value="{build}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="WindowsInstaller" Value="1" />\n'
)
for k, v in g_arpsystemcomponent.items():
if "v" in v:
t = v["t"] if "t" in v is None else "string"
for name, value in g_arpsystemcomponent.items():
if "msi" not in value and "v" in value:
value_type = value.get("t", "string")
lines_new.append(
f'{indent}<RegistryValue Type="{t}" Name="{k}" Value="{v["v"]}" />\n'
f'{indent}<RegistryValue Type={quoteattr(str(value_type))} '
f'Name={quoteattr(str(name))} Value={quoteattr(str(value["v"]))} />\n'
)
for i, line in enumerate(lines_new):
@@ -387,24 +304,35 @@ def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
return gen_content_between_tags(
"Package/Components/Regs.wxs",
"<!--$ArpStart$-->",
"<!--$ArpEnd$-->",
"<!--$InstallStateStart$-->",
"<!--$InstallStateEnd$-->",
func,
)
def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
def gen_custom_ARPSYSTEMCOMPONENT(args, _dist_dir):
try:
custom_arp = json.loads(args.custom_arp)
g_arpsystemcomponent.update(custom_arp)
except json.JSONDecodeError as e:
custom_arp = dict(json.loads(args.custom_arp))
except (json.JSONDecodeError, TypeError, ValueError) as e:
print(f"Failed to decode custom arp: {e}")
return False
if args.arp:
return gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir)
else:
return gen_custom_ARPSYSTEMCOMPONENT_False(args)
if any(not isinstance(value, dict) for value in custom_arp.values()):
print("Custom arp entries must be objects.")
return False
if any(
isinstance(value, dict) and value.get("msi") == "ARPSYSTEMCOMPONENT"
for value in custom_arp.values()
):
print("ARPSYSTEMCOMPONENT is not allowed; native MSI ARP registration must remain visible.")
return False
g_arpsystemcomponent.update(custom_arp)
if not gen_native_arp_properties():
return False
return gen_install_state_values()
def gen_conn_type(args):
def func(lines, index_start):
+175 -51
View File
@@ -100,6 +100,7 @@ use winreg::{enums::*, RegKey};
mod acl;
mod installer_handoff;
mod installer_shell;
mod msi_registry;
pub(crate) use acl::current_process_user_sid_string;
pub use acl::{
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
@@ -119,6 +120,13 @@ pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW";
const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS";
const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS";
pub const REG_NAME_INSTALL_PRINTER: &str = "PRINTER";
const REG_NAME_MSI_PRODUCT_CODE: &str = "MsiProductCode";
const REG_NAME_UNINSTALL_STRING: &str = "UninstallString";
const REG_NAME_WINDOWS_INSTALLER: &str = "WindowsInstaller";
const MSI_WINDOWS_INSTALLER_VALUE: u32 = 1;
const MSI_EXIT_SUCCESS_REBOOT_INITIATED: u32 = 1641;
const MSI_EXIT_SUCCESS_REBOOT_REQUIRED: u32 = 3010;
const HKLM_PREFIX: &str = "HKEY_LOCAL_MACHINE\\";
fn validate_install_app_name(app_name: &str) -> ResultType<()> {
if app_name.is_empty()
@@ -1305,6 +1313,11 @@ fn get_subkey(name: &str, wow: bool) -> String {
}
fn get_valid_subkey() -> String {
let app_name = crate::get_app_name();
let subkey = format!("{HKLM_PREFIX}Software\\{app_name}\\InstallState\\{app_name}");
if !get_reg_of(&subkey, "InstallLocation").is_empty() {
return subkey;
}
let subkey = get_subkey(IS1, false);
if !get_reg_of(&subkey, "InstallLocation").is_empty() {
return subkey;
@@ -1313,7 +1326,6 @@ fn get_valid_subkey() -> String {
if !get_reg_of(&subkey, "InstallLocation").is_empty() {
return subkey;
}
let app_name = crate::get_app_name();
let subkey = get_subkey(&app_name, true);
if !get_reg_of(&subkey, "InstallLocation").is_empty() {
return subkey;
@@ -1572,7 +1584,12 @@ fn get_after_install(
}
pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> {
let uninstall_str = get_uninstall(false, false);
// MSI and EXE installations use different registry layouts, so MSI-to-EXE upgrades are not supported.
let (installed_subkey, _, _, _) = get_install_info();
if get_windows_installer_state(&installed_subkey)? == Some(true) {
bail!("Cannot install the EXE package over an existing MSI installation");
}
let uninstall_str = get_uninstall(false, false)?;
let mut path = path.trim_end_matches('\\').to_owned();
let (subkey, _path, start_menu, exe) = get_default_install_info();
let mut exe = exe;
@@ -1804,10 +1821,14 @@ fn get_before_uninstall(kill_self: bool) -> String {
/// The `uninstall_printer` parameter determines whether the command to uninstall the remote printer
/// is included in the generated uninstall script. If `uninstall_printer` is `false`, the printer
/// related command is omitted from the script.
fn get_uninstall(kill_self: bool, uninstall_printer: bool) -> String {
let reg_uninstall_string = get_reg("UninstallString");
if reg_uninstall_string.to_lowercase().contains("msiexec.exe") {
return reg_uninstall_string;
fn get_uninstall(kill_self: bool, uninstall_printer: bool) -> ResultType<String> {
let (subkey, path, start_menu, _) = get_install_info();
let installer_state = get_windows_installer_state(&subkey)?;
if let Some(product_code) = get_msi_product_code(&subkey, installer_state)? {
return Ok(build_msi_uninstall_command(&product_code));
}
if installer_state == Some(true) {
bail!("MSI product code was not found in {subkey}");
}
let mut uninstall_cert_cmd = "".to_string();
@@ -1820,8 +1841,7 @@ fn get_uninstall(kill_self: bool, uninstall_printer: bool) -> String {
}
}
}
let (subkey, path, start_menu, _) = get_install_info();
format!(
Ok(format!(
"
{before_uninstall}
{uninstall_printer_cmd}
@@ -1836,11 +1856,11 @@ fn get_uninstall(kill_self: bool, uninstall_printer: bool) -> String {
before_uninstall=get_before_uninstall(kill_self),
uninstall_amyuni_idd=get_uninstall_amyuni_idd(),
app_name = crate::get_app_name(),
)
))
}
pub fn uninstall_me(kill_self: bool) -> ResultType<()> {
run_cmds(get_uninstall(kill_self, true), true, "uninstall")
run_cmds(get_uninstall(kill_self, true)?, true, "uninstall")
}
fn write_vbs(cmds: String, tip: &str) -> ResultType<PathBuf> {
@@ -3389,6 +3409,8 @@ pub fn update_me(debug: bool) -> ResultType<()> {
if !is_installed {
bail!("{} is not installed.", &app_name);
}
let is_msi = is_msi_installed().ok();
let reg_msi_key = get_reg_msi_key(&subkey, is_msi)?;
let app_exe_name = &format!("{}.exe", &app_name);
// NOTE: The pids below are matched by command line, which can silently come
@@ -3439,8 +3461,6 @@ pub fn update_me(debug: bool) -> ResultType<()> {
// Use the icon in the previous installation directory if possible.
let display_icon = get_custom_icon("", &exe).unwrap_or(exe.to_string());
let is_msi = is_msi_installed().ok();
fn get_reg_cmd(
subkey: &str,
is_msi: Option<bool>,
@@ -3486,18 +3506,10 @@ reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
&version_build,
size,
);
let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) {
get_reg_cmd(
&reg_msi_key,
is_msi,
&display_icon,
&version,
&build_date,
&version_major,
&version_minor,
&version_build,
size,
)
let reg_cmd_msi = if let Some(reg_msi_key) = &reg_msi_key {
// This is best-effort: failure may leave a stale version in the Windows app list,
// but should not interrupt the update.
format!("reg add {reg_msi_key} /f /v DisplayVersion /t REG_SZ /d \"{version}\"")
} else {
"".to_owned()
};
@@ -3621,34 +3633,147 @@ taskkill /F /IM {app_name}.exe{filter}
Ok(())
}
fn get_reg_msi_key(subkey: &str, is_msi: Option<bool>) -> Option<String> {
fn normalize_msi_product_code(value: &str) -> Option<String> {
let value = value.trim().trim_matches('"');
let value = value.strip_prefix('{')?.strip_suffix('}')?;
let product_code = uuid::Uuid::parse_str(value).ok()?;
Some(format!("{{{}}}", product_code.hyphenated()).to_uppercase())
}
fn build_msi_uninstall_command(product_code: &str) -> String {
format!(
"set \"RUSTDESK_MSI_EXIT_CODE=\"\n\
MsiExec.exe /X {product_code} /norestart REBOOT=ReallySuppress\n\
set \"RUSTDESK_MSI_EXIT_CODE=%ERRORLEVEL%\"\n\
if \"%RUSTDESK_MSI_EXIT_CODE%\"==\"{MSI_EXIT_SUCCESS_REBOOT_REQUIRED}\" echo MSI uninstall succeeded with a reboot recommendation; continuing without reboot.\n\
if \"%RUSTDESK_MSI_EXIT_CODE%\"==\"{MSI_EXIT_SUCCESS_REBOOT_INITIATED}\" echo MSI uninstall succeeded with a reboot request; continuing without forcing reboot.\n\
if not \"%RUSTDESK_MSI_EXIT_CODE%\"==\"0\" if not \"%RUSTDESK_MSI_EXIT_CODE%\"==\"{MSI_EXIT_SUCCESS_REBOOT_REQUIRED}\" if not \"%RUSTDESK_MSI_EXIT_CODE%\"==\"{MSI_EXIT_SUCCESS_REBOOT_INITIATED}\" exit /b %RUSTDESK_MSI_EXIT_CODE%\n\
ver > nul"
)
}
fn get_reg_string_of(subkey: &str, name: &str) -> ResultType<Option<String>> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let path = subkey.strip_prefix(HKLM_PREFIX).unwrap_or(subkey);
let key = match hklm.open_subkey(path) {
Ok(key) => key,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => bail!("Failed to open registry key {subkey}: {err}"),
};
match key.get_value::<String, _>(name) {
Ok(value) => Ok(Some(value)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => bail!("Failed to read {name} from registry key {subkey}: {err}"),
}
}
fn get_windows_installer_state(subkey: &str) -> ResultType<Option<bool>> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let path = subkey.strip_prefix(HKLM_PREFIX).unwrap_or(subkey);
let key = match hklm.open_subkey(path) {
Ok(key) => key,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => bail!("Failed to open registry key {subkey}: {err}"),
};
match key.get_value::<u32, _>(REG_NAME_WINDOWS_INSTALLER) {
Ok(value) => Ok(Some(value == MSI_WINDOWS_INSTALLER_VALUE)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => bail!("Failed to read {REG_NAME_WINDOWS_INSTALLER} from {subkey}: {err}"),
}
}
fn parse_msi_product_code_from_uninstall_string(
uninstall_string: &str,
subkey: &str,
) -> ResultType<Option<String>> {
if !uninstall_string
.to_ascii_lowercase()
.contains("msiexec.exe")
{
return Ok(None);
}
let start = uninstall_string
.rfind('{')
.ok_or_else(|| anyhow!("MSI uninstall string has no product code in {subkey}"))?;
let end = uninstall_string
.rfind('}')
.ok_or_else(|| anyhow!("MSI uninstall string has no product code in {subkey}"))?;
if start >= end {
bail!("Invalid MSI uninstall string in {subkey}");
}
let product_code = uninstall_string
.get(start..=end)
.and_then(normalize_msi_product_code)
.ok_or_else(|| anyhow!("Invalid MSI uninstall string in {subkey}"))?;
Ok(Some(product_code))
}
fn get_msi_product_code(subkey: &str, installer_state: Option<bool>) -> ResultType<Option<String>> {
if installer_state == Some(false) {
return Ok(None);
}
let product_code = get_reg_string_of(subkey, REG_NAME_MSI_PRODUCT_CODE)?;
if let Some(product_code) = product_code.filter(|value| !value.is_empty()) {
return normalize_msi_product_code(&product_code)
.map(Some)
.ok_or_else(|| anyhow!("Invalid MSI product code in {subkey}"));
}
let uninstall_string =
get_reg_string_of(subkey, REG_NAME_UNINSTALL_STRING)?.unwrap_or_default();
match parse_msi_product_code_from_uninstall_string(&uninstall_string, subkey)? {
Some(product_code) => Ok(Some(product_code)),
None if installer_state == Some(true) => {
msi_registry::find_product_code(&crate::get_app_name())
}
None => Ok(None),
}
}
fn is_msi_uninstall_entry_in_view(subkey: &str, wow: bool, app_name: &str) -> ResultType<bool> {
let flags = KEY_READ
| if wow {
KEY_WOW64_32KEY
} else {
KEY_WOW64_64KEY
};
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let path = subkey.strip_prefix(HKLM_PREFIX).unwrap_or(subkey);
let key = match hklm.open_subkey_with_flags(path, flags) {
Ok(key) => key,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(err) => return Err(anyhow!("Failed to open registry key {subkey}: {err}")),
};
msi_registry::is_matching_entry(&key, app_name, subkey)
}
fn get_msi_uninstall_subkey(product_code: &str) -> ResultType<String> {
let app_name = crate::get_app_name();
let subkey = get_subkey(product_code, false);
if is_msi_uninstall_entry_in_view(&subkey, false, &app_name)? {
return Ok(subkey);
}
if is_msi_uninstall_entry_in_view(&subkey, true, &app_name)? {
return Ok(get_subkey(product_code, true));
}
bail!("Matching native MSI uninstall entry {product_code} was not found")
}
fn get_reg_msi_key(subkey: &str, is_msi: Option<bool>) -> ResultType<Option<String>> {
// Only proceed if it's a custom client and MSI is installed.
// `is_msi.unwrap_or(true)` is intentional: subsequent code validates the registry,
// hence no early return is required upon MSI detection failure.
if !(crate::common::is_custom_client() && is_msi.unwrap_or(true)) {
return None;
return Ok(None);
}
// Get the uninstall string from registry
let uninstall_string = get_reg_of(subkey, "UninstallString");
if uninstall_string.is_empty() {
return None;
}
// Find the product code (GUID) in the uninstall string
// Handle both quoted and unquoted GUIDs: /X {GUID} or /X "{GUID}"
let start = uninstall_string.rfind('{')?;
let end = uninstall_string.rfind('}')?;
if start >= end {
return None;
}
let product_code = &uninstall_string[start..=end];
// Build the MSI registry key path
let pos = subkey.rfind('\\')?;
let reg_msi_key = format!("{}{}", &subkey[..=pos], product_code);
Some(reg_msi_key)
let Some(product_code) = get_msi_product_code(subkey, is_msi)? else {
if is_msi == Some(true) {
bail!("MSI product code was not found in {subkey}");
}
return Ok(None);
};
Ok(Some(get_msi_uninstall_subkey(&product_code)?))
}
// Double confirm the process name
@@ -4422,12 +4547,11 @@ fn get_pids<S: AsRef<str>>(name: S) -> ResultType<Vec<u32>> {
}
pub fn is_msi_installed() -> std::io::Result<bool> {
let (subkey, _, _, _) = get_install_info();
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let uninstall_key = hklm.open_subkey(format!(
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}",
crate::get_app_name()
))?;
Ok(1 == uninstall_key.get_value::<u32, _>("WindowsInstaller")?)
let install_key = hklm.open_subkey(subkey.strip_prefix(HKLM_PREFIX).unwrap_or(&subkey))?;
Ok(MSI_WINDOWS_INSTALLER_VALUE
== install_key.get_value::<u32, _>(REG_NAME_WINDOWS_INSTALLER)?)
}
pub fn is_cur_exe_the_installed() -> bool {
+96
View File
@@ -0,0 +1,96 @@
use super::{
normalize_msi_product_code, ResultType, MSI_WINDOWS_INSTALLER_VALUE, REG_NAME_WINDOWS_INSTALLER,
};
use hbb_common::{anyhow::anyhow, bail, log};
use std::collections::BTreeSet;
use winreg::{enums::*, RegKey};
const REG_NAME_DISPLAY_NAME: &str = "DisplayName";
const UNINSTALL_SUBKEY: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
pub(super) fn find_product_code(app_name: &str) -> ResultType<Option<String>> {
let product_codes = find_product_codes_in_view(app_name, false)?
.into_iter()
.chain(find_product_codes_in_view(app_name, true)?)
.collect::<BTreeSet<_>>();
let mut product_codes = product_codes.into_iter();
let product_code = product_codes.next();
if product_codes.next().is_some() {
bail!("Multiple native MSI uninstall entries were found for {app_name}");
}
Ok(product_code)
}
fn find_product_codes_in_view(app_name: &str, wow: bool) -> ResultType<Vec<String>> {
let flags = KEY_READ
| if wow {
KEY_WOW64_32KEY
} else {
KEY_WOW64_64KEY
};
let view_name = if wow { "32-bit" } else { "64-bit" };
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let uninstall_key = match hklm.open_subkey_with_flags(UNINSTALL_SUBKEY, flags) {
Ok(uninstall_key) => uninstall_key,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => bail!("Failed to open {view_name} MSI uninstall registry: {err}"),
};
let mut matches = Vec::new();
for key_name in uninstall_key.enum_keys() {
let key_name = match key_name {
Ok(key_name) => key_name,
Err(err) => {
log::warn!("Skipping unreadable {view_name} MSI uninstall key name: {err}");
continue;
}
};
let Some(product_code) = normalize_msi_product_code(&key_name) else {
continue;
};
let is_match = uninstall_key
.open_subkey_with_flags(&key_name, flags)
.map_err(|err| {
anyhow!("Failed to open {view_name} MSI uninstall entry {key_name}: {err}")
})
.and_then(|entry| is_matching_entry(&entry, app_name, &key_name));
if scanned_entry_matches(is_match) {
matches.push(product_code);
}
}
Ok(matches)
}
pub(super) fn scanned_entry_matches(result: ResultType<bool>) -> bool {
match result {
Ok(is_match) => is_match,
Err(err) => {
log::warn!("Skipping invalid MSI uninstall entry: {err}");
false
}
}
}
pub(super) fn is_matching_entry(
entry: &RegKey,
app_name: &str,
key_name: &str,
) -> ResultType<bool> {
match entry.get_value::<u32, _>(REG_NAME_WINDOWS_INSTALLER) {
Ok(value) if value == MSI_WINDOWS_INSTALLER_VALUE => {}
Ok(_) => return Ok(false),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(err) => bail!(
"Failed to read {REG_NAME_WINDOWS_INSTALLER} from MSI uninstall entry {key_name}: {err}"
),
}
match entry.get_value::<String, _>(REG_NAME_DISPLAY_NAME) {
Ok(display_name) => Ok(display_name.eq_ignore_ascii_case(app_name)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(err) => bail!(
"Failed to read {REG_NAME_DISPLAY_NAME} from MSI uninstall entry {key_name}: {err}"
),
}
}