mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-24 23:26:47 +00:00
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:
@@ -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="#1"">
|
||||
<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="#1"">
|
||||
<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>
|
||||
|
||||
@@ -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="#0""/>
|
||||
<Show Dialog="UI_AnotherAppDialog" Before="WelcomeDlg" Condition="NOT Installed AND (APP_WINDOWS_INSTALLER="#0" OR APP_WINDOWS_INSTALLER32="#0")"/>
|
||||
</InstallUISequence>
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="BlockSelfInstalledApp" After="AppSearch" Condition="NOT Installed AND (APP_WINDOWS_INSTALLER="#0" OR APP_WINDOWS_INSTALLER32="#0")" />
|
||||
<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
@@ -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
@@ -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(
|
||||
®_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) = ®_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 {
|
||||
|
||||
@@ -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}"
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user