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

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>

View File

@@ -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>

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" />

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):