From 64020758d91b9da54b0f61df39d9febeda1bf226 Mon Sep 17 00:00:00 2001
From: fufesou <fufesou@users.noreply.github.com>
Date: Thu, 11 Apr 2024 11:51:35 +0800
Subject: [PATCH] Feat. Msi (#7684)

Signed-off-by: fufesou <shuanglongchen@yeah.net>
---
 res/msi/CustomActions/CustomActions.cpp       | 168 ++++++++++-
 res/msi/CustomActions/CustomActions.def       |   2 +
 res/msi/CustomActions/CustomActions.vcxproj   |   2 +-
 res/msi/Package/Components/Regs.wxs           |  12 +-
 res/msi/Package/Components/RustDesk.wxs       |  40 ++-
 .../Package/Fragments/AddRemoveProperties.wxs |   9 +-
 res/msi/Package/Fragments/CustomActions.wxs   |  15 +-
 res/msi/Package/Package.wxs                   |  33 +-
 res/msi/README.md                             |  31 +-
 res/msi/preprocess.py                         | 281 +++++++++++++++++-
 src/platform/windows.rs                       |   5 +
 11 files changed, 514 insertions(+), 84 deletions(-)

diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp
index f836edb3b..85ea27fe8 100644
--- a/res/msi/CustomActions/CustomActions.cpp
+++ b/res/msi/CustomActions/CustomActions.cpp
@@ -1,11 +1,13 @@
 // CustomAction.cpp : Defines the entry point for the custom action.
 #include "pch.h"
+
 #include <strutil.h>
 #include <shellapi.h>
+#include <tlhelp32.h>
+#include <winternl.h>
 
 UINT __stdcall CustomActionHello(
-    __in MSIHANDLE hInstall
-)
+    __in MSIHANDLE hInstall)
 {
     HRESULT hr = S_OK;
     DWORD er = ERROR_SUCCESS;
@@ -24,8 +26,7 @@ LExit:
 }
 
 UINT __stdcall RemoveInstallFolder(
-    __in MSIHANDLE hInstall
-)
+    __in MSIHANDLE hInstall)
 {
     HRESULT hr = S_OK;
     DWORD er = ERROR_SUCCESS;
@@ -46,7 +47,7 @@ UINT __stdcall RemoveInstallFolder(
     ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
 
     SHFILEOPSTRUCTW fileOp;
-    ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT)); 
+    ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT));
 
     fileOp.wFunc = FO_DELETE;
     fileOp.pFrom = installFolder;
@@ -68,3 +69,160 @@ LExit:
     er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
     return WcaFinalize(er);
 }
+
+#include "../../../src/platform/windows_delete_test_cert.cc";
+UINT __stdcall DeleteTestCerts(
+    __in MSIHANDLE hInstall)
+{
+    HRESULT hr = S_OK;
+    DWORD er = ERROR_SUCCESS;
+
+    hr = WcaInitialize(hInstall, "DeleteTestCerts");
+    ExitOnFailure(hr, "Failed to initialize");
+
+    WcaLog(LOGMSG_STANDARD, "Initialized.");
+
+    DeleteRustDeskTestCertsW();
+    WcaLog(LOGMSG_STANDARD, "DeleteRustDeskTestCertsW finished.");
+
+LExit:
+    er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
+    return WcaFinalize(er);
+}
+
+// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess
+// **NtQueryInformationProcess** may be altered or unavailable in future versions of Windows.
+// Applications should use the alternate functions listed in this topic.
+// But I do not find the alternate functions.
+// https://github.com/heim-rs/heim/issues/105#issuecomment-683647573
+typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);
+bool TerminateProcessIfNotContainsParam(pfnNtQueryInformationProcess NtQueryInformationProcess, HANDLE process, LPCWSTR excludeParam)
+{
+    bool processClosed = false;
+    PROCESS_BASIC_INFORMATION processInfo;
+    NTSTATUS status = NtQueryInformationProcess(process, ProcessBasicInformation, &processInfo, sizeof(processInfo), NULL);
+    if (status == 0 && processInfo.PebBaseAddress != NULL)
+    {
+        PEB peb;
+        SIZE_T dwBytesRead;
+        if (ReadProcessMemory(process, processInfo.PebBaseAddress, &peb, sizeof(peb), &dwBytesRead))
+        {
+            RTL_USER_PROCESS_PARAMETERS pebUpp;
+            if (ReadProcessMemory(process,
+                                  peb.ProcessParameters,
+                                  &pebUpp,
+                                  sizeof(RTL_USER_PROCESS_PARAMETERS),
+                                  &dwBytesRead))
+            {
+                if (pebUpp.CommandLine.Length > 0)
+                {
+                    WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length);
+                    if (commandLine != NULL)
+                    {
+                        if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer,
+                                              commandLine, pebUpp.CommandLine.Length, &dwBytesRead))
+                        {
+                            if (wcsstr(commandLine, excludeParam) == NULL)
+                            {
+                                WcaLog(LOGMSG_STANDARD, "Terminate process : %ls", commandLine);
+                                TerminateProcess(process, 0);
+                                processClosed = true;
+                            }
+                        }
+                        free(commandLine);
+                    }
+                }
+            }
+        }
+    }
+    return processClosed;
+}
+
+// Terminate processes that do not have parameter [excludeParam]
+// Note. This function relies on "NtQueryInformationProcess",
+//       which may not be found.
+//       Then all processes of [processName] will be terminated.
+bool TerminateProcessesByNameW(LPCWSTR processName, LPCWSTR excludeParam)
+{
+    HMODULE hntdll = GetModuleHandleW(L"ntdll.dll");
+    if (hntdll == NULL)
+    {
+        WcaLog(LOGMSG_STANDARD, "Failed to load ntdll.");
+    }
+
+    pfnNtQueryInformationProcess NtQueryInformationProcess = NULL;
+    if (hntdll != NULL)
+    {
+        NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(
+            hntdll, "NtQueryInformationProcess");
+    }
+    if (NtQueryInformationProcess == NULL)
+    {
+        WcaLog(LOGMSG_STANDARD, "Failed to get address of NtQueryInformationProcess.");
+    }
+
+    bool processClosed = false;
+    // Create a snapshot of the current system processes
+    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+    if (snapshot != INVALID_HANDLE_VALUE)
+    {
+        PROCESSENTRY32W processEntry;
+        processEntry.dwSize = sizeof(PROCESSENTRY32W);
+        if (Process32FirstW(snapshot, &processEntry))
+        {
+            do
+            {
+                if (lstrcmpW(processName, processEntry.szExeFile) == 0)
+                {
+                    HANDLE process = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processEntry.th32ProcessID);
+                    if (process != NULL)
+                    {
+                        if (NtQueryInformationProcess == NULL)
+                        {
+                            WcaLog(LOGMSG_STANDARD, "Terminate process : %ls, while NtQueryInformationProcess is NULL", processName);
+                            TerminateProcess(process, 0);
+                            processClosed = true;
+                        }
+                        else
+                        {
+                            processClosed = TerminateProcessIfNotContainsParam(
+                                NtQueryInformationProcess,
+                                process,
+                                excludeParam);
+                        }
+                        CloseHandle(process);
+                    }
+                }
+            } while (Process32Next(snapshot, &processEntry));
+        }
+        CloseHandle(snapshot);
+    }
+    if (hntdll != NULL)
+    {
+        CloseHandle(hntdll);
+    }
+    return processClosed;
+}
+
+UINT __stdcall TerminateProcesses(
+    __in MSIHANDLE hInstall)
+{
+    HRESULT hr = S_OK;
+    DWORD er = ERROR_SUCCESS;
+
+    int nResult = 0;
+    wchar_t szProcess[256] = {0};
+    DWORD cchProcess = sizeof(szProcess) / sizeof(szProcess[0]);
+
+    hr = WcaInitialize(hInstall, "TerminateProcesses");
+    ExitOnFailure(hr, "Failed to initialize");
+
+    MsiGetPropertyW(hInstall, L"TerminateProcesses", szProcess, &cchProcess);
+
+    WcaLog(LOGMSG_STANDARD, "Try terminate processes : %ls", szProcess);
+    TerminateProcessesByNameW(szProcess, L"--install");
+
+LExit:
+    er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
+    return WcaFinalize(er);
+}
diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def
index 8eb50ce0d..08a539cb7 100644
--- a/res/msi/CustomActions/CustomActions.def
+++ b/res/msi/CustomActions/CustomActions.def
@@ -3,3 +3,5 @@ LIBRARY "CustomActions"
 EXPORTS
     CustomActionHello
     RemoveInstallFolder
+    DeleteTestCerts
+    TerminateProcesses
diff --git a/res/msi/CustomActions/CustomActions.vcxproj b/res/msi/CustomActions/CustomActions.vcxproj
index 3667b3bc8..b9491b73b 100644
--- a/res/msi/CustomActions/CustomActions.vcxproj
+++ b/res/msi/CustomActions/CustomActions.vcxproj
@@ -12,7 +12,7 @@
     <Keyword>Win32Proj</Keyword>
     <ProjectGuid>{6b3647e0-b4a3-46ae-8757-a22ee51c1dac}</ProjectGuid>
     <RootNamespace>CustomActions</RootNamespace>
-    <PlatformToolset>v143</PlatformToolset>
+    <PlatformToolset>v142</PlatformToolset>
     <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
   </PropertyGroup>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
diff --git a/res/msi/Package/Components/Regs.wxs b/res/msi/Package/Components/Regs.wxs
index 4aec900df..ee14da48d 100644
--- a/res/msi/Package/Components/Regs.wxs
+++ b/res/msi/Package/Components/Regs.wxs
@@ -39,7 +39,17 @@
 					<RegistryValue Type="string" Value='"[INSTALLFOLDER]$(var.Product)" "%1"' />
 				</RegistryKey>
 			</Component>
-			
+
+			<!--For compatibility with registry values from previous versions-->
+			<Component Id="Product.Registry.UninstallRustDesk" Guid="FC1A3D2E-5642-FBD8-CFA6-5ECAC6DE69A8">
+				<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" >
+					<RegistryValue Type="string" Name="BuildDate" Value="$(var.BuildDate)" />
+					<RegistryValue Type="string" Name="share_rdp" Value="" />
+
+					<!--$ArpStart$-->
+					<!--$ArpEnd$-->
+				</RegistryKey>
+			</Component>
 		</DirectoryRef>
 
 	</Fragment>
diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs
index 297f35a77..cf41ba01a 100644
--- a/res/msi/Package/Components/RustDesk.wxs
+++ b/res/msi/Package/Components/RustDesk.wxs
@@ -7,10 +7,7 @@
 		<DirectoryRef Id="INSTALLFOLDER" FileSource="$(var.BuildDir)">
 			<Component Id="RustDesk.exe" Guid="620F0F69-4C17-4320-A619-495E329712A4">
 				<File Id="$(var.Product).exe" Name="$(var.Product).exe" KeyPath="yes" Checksum="yes">
-					<fire:FirewallException Id="RustDeskExTCPDom" Name="$(var.Product) TCP Domain" Profile="domain" Protocol="tcp" Scope="any" IgnoreFailure="yes" />
-					<fire:FirewallException Id="RustDeskExTCPPriv" Name="$(var.Product) TCP Private" Profile="private" Protocol="tcp" Scope="any" IgnoreFailure="yes" />
-					<fire:FirewallException Id="RustDeskExUDPDom" Name="$(var.Product) UDP Domain" Profile="domain" Protocol="udp" Scope="any" IgnoreFailure="yes" />
-					<fire:FirewallException Id="RustDeskExUDPPriv" Name="$(var.Product) UDP Private" Profile="private" Protocol="udp" Scope="any" IgnoreFailure="yes" />
+					<fire:FirewallException Id="RustDeskEx" Name="$(var.Product) Service" Scope="any" IgnoreFailure="yes" />
 				</File>
 				<ServiceInstall Id="ServiceInstaller" Type="ownProcess" Vital="yes" Name="$(var.Product)" DisplayName="!(loc.Service_DisplayName)" Description="!(loc.Service_Description)" Start="auto" Account="LocalSystem" ErrorControl="ignore" Interactive="no" Arguments="--service" />
 				<ServiceControl Id="StartService" Start="install" Stop="both" Remove="uninstall" Name="$(var.Product)" Wait="yes" />
@@ -33,51 +30,47 @@
 
 			<Custom Action="RemoveInstallFolder.SetParam" After="RemoveFiles"/>
 			<Custom Action="RemoveInstallFolder" After="RemoveInstallFolder.SetParam"/>
+			<Custom Action="DeleteTestCerts" After="RemoveFiles"/>
 		</InstallExecuteSequence>
 
 		<!-- Shortcuts -->
 		<DirectoryRef Id="App.StartMenu">
 			<Component Id="App.StartMenu" Guid="30F6D57A-B805-4DA4-A071-05A3B22400CA">
-				<RegistryValue Root="HKCU" Key="$(var.RegKeyInstall)" Name="App.StartMenu" Type="string" Value="1" KeyPath="yes" />
+				<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.StartMenu" Type="string" Value="1" KeyPath="yes" />
 				<RemoveFolder Id="Remove.App.StartMenu" On="uninstall" />
 			</Component>
 		</DirectoryRef>
 
 		<DirectoryRef Id="App.StartMenu">
 			<Component Id="App.StartMenu.Shortcut" Guid="43ABCAC7-E47D-42D8-A408-25EC70DBB993" Condition="STARTMENUSHORTCUTS = 1">
-
 				<Shortcut Id="App.StartMenu.Shortcut" Name="!(loc.SC_Client)" Description="!(loc.SC_Client_Desc)" Target="[!RustDesk.exe]" Icon="AppIcon" WorkingDirectory="INSTALLFOLDER" />
 				<!--
-        Fix ICE 38 by adding a dummy registry key that is the key for this shortcut.
-        http://msdn.microsoft.com/library/en-us/msi/setup/ice38.asp
-        -->
-				<RegistryValue Root="HKCU" Key="$(var.RegKeyInstall)" Name="App.StartMenu.Shortcut" Type="string" Value="1" KeyPath="yes" />
+					Fix ICE 38 by adding a dummy registry key that is the key for this shortcut.
+					https://learn.microsoft.com/en-us/windows/win32/msi/ice38
+				-->
+				<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.StartMenu.Shortcut" Type="string" Value="1" KeyPath="yes" />
 			</Component>
-			<Component Id="App.StartMenu.ShortcutTray" Guid="9362C316-40BB-41C1-859C-08182AA47E8D" Condition="STARTMENUSHORTCUTS = 1">
 
+			<Component Id="App.StartMenu.ShortcutTray" Guid="9362C316-40BB-41C1-859C-08182AA47E8D" Condition="STARTMENUSHORTCUTS = 1">
 				<Shortcut Id="App.StartMenu.ShortcutTray" Name="!(loc.SC_Client_Tray)" Description="!(loc.SC_Client_Tray_Desc)" Target="[!RustDesk.exe]" Arguments="--tray" Icon="AppIcon" WorkingDirectory="INSTALLFOLDER" />
-				<!--
-        Fix ICE 38 by adding a dummy registry key that is the key for this shortcut.
-        http://msdn.microsoft.com/library/en-us/msi/setup/ice38.asp
-        -->
-				<RegistryValue Root="HKCU" Key="$(var.RegKeyInstall)" Name="App.StartMenu.Shortcut" Type="string" Value="1" KeyPath="yes" />
+				<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.StartMenu.ShortcutTray" Type="string" Value="1" KeyPath="yes" />
+			</Component>
+
+			<Component Id="App.StartMenu.ShortcutUninstall" Guid="E100D7F8-D607-4513-28DA-2C95E5EA698E" Condition="STARTMENUSHORTCUTS = 1">
+				<Shortcut Id="App.StartMenu.ShortcutUninstall" Name="!(loc.SC_Uninstall)" Description="!(loc.SC_Uninstall_Desc)" Target="[System6432Folder]msiexec.exe" Arguments="/x [ProductCode]" Icon="AppIcon" />
+				<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.StartMenu.ShortcutUninstall" Type="string" Value="1" KeyPath="yes" />
 			</Component>
 		</DirectoryRef>
 		<StandardDirectory Id="DesktopFolder">
 			<Component Id="App.Desktop.Shortcut" Guid="CA8FB7AA-17F7-4E36-A58A-5A016A303709" Condition="DESKTOPSHORTCUTS = 1">
-
 				<Shortcut Id="App.Desktop.Shortcut" Name="!(loc.SC_Client)" Description="!(loc.SC_Client_Desc)" Target="[!RustDesk.exe]" Icon="AppIcon" WorkingDirectory="INSTALLFOLDER" />
-				<!--
-        Fix ICE 38 by adding a dummy registry key that is the key for this shortcut.
-        http://msdn.microsoft.com/library/en-us/msi/setup/ice38.asp
-        -->
-				<RegistryValue Root="HKCU" Key="$(var.RegKeyInstall)" Name="App.Desktop.Shortcut" Type="string" Value="1" KeyPath="yes" />
+				<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.Desktop.Shortcut" Type="string" Value="1" KeyPath="yes" />
 			</Component>
 		</StandardDirectory>
 
 		<DirectoryRef Id="INSTALLFOLDER">
 			<Component Id="App.UninstallShortcut" Guid="FB0F2AC7-2AE5-4C54-B860-5E472620B6B1">
-				<Shortcut Id="App.UninstallShortcut" Name="!(loc.SC_Uninstall)" Description="!(loc.SC_Uninstall_Desc)" Target="[System64Folder]msiexec.exe" Arguments="/x [ProductCode]" IconIndex="0" />
+				<Shortcut Id="App.UninstallShortcut" Name="!(loc.SC_Uninstall)" Description="!(loc.SC_Uninstall_Desc)" Target="[System6432Folder]msiexec.exe" Arguments="/x [ProductCode]" IconIndex="0" />
 			</Component>
 		</DirectoryRef>
 
@@ -87,6 +80,7 @@
 			<ComponentRef Id="App.UninstallShortcut" />
 			<ComponentRef Id="App.StartMenu.Shortcut" />
 			<ComponentRef Id="App.StartMenu.ShortcutTray" />
+			<ComponentRef Id="App.StartMenu.ShortcutUninstall" />
 
 			<!--$AutoComonentStart$-->
 			<!--$AutoComponentEnd$-->
diff --git a/res/msi/Package/Fragments/AddRemoveProperties.wxs b/res/msi/Package/Fragments/AddRemoveProperties.wxs
index 39d0775da..56dfb1854 100644
--- a/res/msi/Package/Fragments/AddRemoveProperties.wxs
+++ b/res/msi/Package/Fragments/AddRemoveProperties.wxs
@@ -8,16 +8,19 @@
 
 		<!--
 		Support entries shown when clicking "Click here for support information"
-		in Control Panel's Add/Remove Programs http://msdn.microsoft.com/library/default.asp?url=/library/en-us/msi/setup/configuration_properties.asp
+		in Control Panel's Add/Remove Programs https://learn.microsoft.com/en-us/windows/win32/msi/property-reference
 		-->
-		<Property Id="ARPCOMMENTS" Value="!(loc.AR_Comment)" />
+		<!--<Property Id="ARPCOMMENTS" Value="!(loc.AR_Comment)" />
 		<Property Id="ARPCONTACT" Value="https://github.com/rustdesk/rustdesk" />
 		<Property Id="ARPHELPLINK" Value="https://github.com/rustdesk/rustdesk" />
 		<Property Id="ARPREADME" Value="https://github.com/rustdesk/rustdesk" />
 		<Property Id="ARPURLINFOABOUT" Value="https://github.com/rustdesk/rustdesk" />
-		<Property Id="ARPURLUPDATEINFO" Value="https://github.com/rustdesk/rustdesk" />
+		<Property Id="ARPURLUPDATEINFO" Value="https://github.com/rustdesk/rustdesk" />-->
 
 		<Property Id="ARPPRODUCTICON" Value="AppIcon" />
+		
+		<!--$ArpStart$-->
+		<!--$ArpEnd$-->
 
 		<Property Id="RUSTDESKNATIVEINSTALL">
 			<RegistrySearch Id="RustDeskNativeInstallSearch" Root="HKCR" Key="$(var.RegKeyRoot)\DefaultIcon" Type="raw" />
diff --git a/res/msi/Package/Fragments/CustomActions.wxs b/res/msi/Package/Fragments/CustomActions.wxs
index 09d9b5698..500b5da40 100644
--- a/res/msi/Package/Fragments/CustomActions.wxs
+++ b/res/msi/Package/Fragments/CustomActions.wxs
@@ -6,18 +6,7 @@
 
     <CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
     <CustomAction Id="RemoveInstallFolder" DllEntry="RemoveInstallFolder" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
-
-    <!-- Use WixQuietExec to run the commands to avoid the command window popping up. The command line to run needs to be stored
-				 in a property with the same id as the WixQuietExec custom action and the path to the exe needs to be quoted.
-				 A SetProperty action is used to allow the [SystemFolder] reference to be resolved and needs to be scheduled to run before the action.-->
-
-    <SetProperty Id="FirewallPortOutAdd" Value="&quot;[SystemFolder]netsh.exe&quot; advfirewall firewall add rule name=&quot;$(var.Product) Service&quot; dir=out action=allow program=$(var.ProductLower) enable=yes" Before="FirewallPortOutAdd" Sequence="execute" />
-    <CustomAction Id="FirewallPortOutAdd" DllEntry="WixQuietExec" Execute="deferred" Return="asyncWait" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)" />
-    <SetProperty Id="FirewallPortInAdd" Value="&quot;[SystemFolder]netsh.exe&quot; advfirewall firewall add rule name=&quot;$(var.Product) Service&quot; dir=in action=allow program=$(var.ProductLower) enable=yes" Before="FirewallPortInAdd" Sequence="execute" />
-    <CustomAction Id="FirewallPortInAdd" DllEntry="WixQuietExec" Execute="deferred" Return="asyncWait" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)" />
-
-    <SetProperty Id="FirewallPortRemove" Value="&quot;[SystemFolder]netsh.exe&quot; advfirewall firewall delete rule name=&quot;$(var.Product) Service&quot;" Before="FirewallPortRemove" Sequence="execute" />
-    <CustomAction Id="FirewallPortRemove" DllEntry="WixQuietExec" Execute="deferred" Return="asyncWait" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)" />
-
+    <CustomAction Id="DeleteTestCerts" DllEntry="DeleteTestCerts" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
+    <CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
   </Fragment>
 </Wix>
diff --git a/res/msi/Package/Package.wxs b/res/msi/Package/Package.wxs
index d9f7d125a..cd6b29936 100644
--- a/res/msi/Package/Package.wxs
+++ b/res/msi/Package/Package.wxs
@@ -4,7 +4,7 @@
 
 	<?include Includes.wxi?>
 
-	<Package Name="$(var.Product)" Version="$(var.Version)" Manufacturer="$(var.Manufacturer)" Language="!(loc.ProductLanguage)" Codepage="0" UpgradeCode="$(var.UpgradeCode)" Scope="perMachine">
+	<Package Name="$(var.Product)" Version="$(var.Version)" Manufacturer="$(var.Manufacturer)" Language="!(loc.ProductLanguage)" UpgradeCode="$(var.UpgradeCode)" Scope="perMachine">
 
 		<SummaryInformation Keywords="Installer" Description="$(var.Description)" Codepage="!(loc.SummaryCodepage)" />
 
@@ -23,38 +23,26 @@
 
 		<InstallUISequence>
 			<!--<Custom Action="ReadCustomPathsFromExistingPathsFile" Before="CostFinalize" Condition="NOT Installed" />-->
-			<Show Dialog="AnotherAppDialog" Before="WelcomeDlg" Condition="Not installed AND RUSTDESKNATIVEINSTALL AND Not RUSTDESKNATIVEINSTALLFOLDER"/>
-
+			<!--<Show Dialog="AnotherAppDialog" Before="WelcomeDlg" Condition="Not installed AND RUSTDESKNATIVEINSTALL AND Not RUSTDESKNATIVEINSTALLFOLDER"/>-->
 		</InstallUISequence>
 
 		<InstallExecuteSequence>
-			<!-- Stop all MP2 processes -->
-			<!--<Custom Action="StopProcesses" Before="ReadCustomPathsFromExistingPathsFile" />-->
-
-			<!-- Reads custom paths which maybe have been changed by the user in a former installation -->
-			<!--<Custom Action="ReadCustomPathsFromExistingPathsFile" Before="PrepareXmlPathVariables" Condition="(NOT Installed) AND (INSTALLTYPE_CUSTOM = 0)" />-->
-
-			<!--<Custom Action="PrepareXmlPathVariables" Before="FileCost" Condition="NOT Installed" />-->
-			<!--<Custom Action="AttachClientToServer" After="InstallFinalize" Condition="NOT Installed" />-->
-
-			<Custom Action="FirewallPortRemove" Before="InstallFinalize" Condition="REMOVE~=&quot;ALL&quot;" />
-			<Custom Action="FirewallPortOutAdd" Before="InstallFinalize" Condition="NOT Installed" />
-			<Custom Action="FirewallPortInAdd" Before="InstallFinalize" Condition="NOT Installed" />
-
 			<!-- Launch ClientLauncher if installing or already installed and not uninstalling -->
 			<Custom Action="LaunchApp" After="InstallFinalize" />
 			<Custom Action="LaunchAppTray" After="InstallFinalize" />
 
 			<InstallExecute After="RemoveExistingProducts" />
 
-			<Custom Action="CloseProcesses" Before="RemoveFiles" />
+			<Custom Action="TerminateProcesses" Before="RemoveFiles"/>
+			<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
 
 		</InstallExecuteSequence>
 		<CustomAction Id="LaunchApp" ExeCommand="" Return="asyncNoWait" FileRef="RustDesk.exe" />
 		<CustomAction Id="LaunchAppTray" ExeCommand=" --tray" Return="asyncNoWait" FileRef="RustDesk.exe" />
-		<CustomAction Id="CloseProcesses" ExeCommand='taskkill /F /IM "$(var.Product).exe"' Return="asyncNoWait" Directory="INSTALLFOLDER" />
-
-		<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" Schedule="afterInstallInitialize" />
+		<Property Id="TerminateProcesses" Value="RustDeskTest.exe" />
+		<CustomAction Id="TerminateProcesses.SetParam" Return="check" Property="TerminateProcesses" Value="$(var.Product).exe" />
+		
+		<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" Schedule="afterInstallInitialize" AllowSameVersionUpgrades="yes" />
 
 		<Feature Id="App" Level="1" AllowAdvertise="no" Display="expand" Title="!(loc.F_App)" Description="!(loc.F_App_Desc)" AllowAbsent="no">
 			<ComponentGroupRef Id="Components" />
@@ -64,8 +52,13 @@
 			<ComponentRef Id="Product.Registry.CommandPlay" />
 			<ComponentRef Id="Product.Registry.URLProtocol" />
 			<ComponentRef Id="Product.Registry.Command" />
+			<ComponentRef Id="Product.Registry.UninstallRustDesk" />
 			<ComponentRef Id="App.StartMenu" />
 			<ComponentRef Id="Product.Registry.PersistedShortcutProperties" />
 		</Feature>
+
+		<!--https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set-->
+		<!--$CustomBitmapsStart$-->
+		<!--$CustomBitmapsEnd$-->
 	</Package>
 </Wix>
diff --git a/res/msi/README.md b/res/msi/README.md
index 80a2490bf..2650ad69b 100644
--- a/res/msi/README.md
+++ b/res/msi/README.md
@@ -6,19 +6,38 @@ This project is mainly derived from <https://github.com/MediaPortal/MediaPortal-
 
 ## Steps
 
-1. `python preprocess.py`
+1. `python preprocess.py`, see `python preprocess.py -h` for help.
 2. Build the .sln solution.
 
 Run `msiexec /i package.msi /l*v install.log` to record the log.
 
+## Usage
+
+1. Put the custom dialog bitmaps in "Resources" directory. The supported bitmaps are `['WixUIBannerBmp', 'WixUIDialogBmp', 'WixUIExclamationIco', 'WixUIInfoIco', 'WixUINewIco', 'WixUIUpIco']`.
+
+## Knowledge
+
+### properties
+
+[wix-toolset-set-custom-action-run-only-on-uninstall](https://www.advancedinstaller.com/versus/wix-toolset/wix-toolset-set-custom-action-run-only-on-uninstall.html)
+
+| Property Name | Install | Uninstall | Change | Repair | Upgrade |
+| ------ | ------ | ------ | ------ | ------ | ------ |
+| Installed | False | True | True | True | True |
+| REINSTALL | False | False | False | True | False |
+| UPGRADINGPRODUCTCODE | False | False | False | False | True |
+| REMOVE | False | True | False | False | True |
+
 ## TODOs
 
-1. tray, uninstall shortcut
-1. launch client after installation
-1. github ci
-1. options
+1. Start menu. Uninstall
+1. custom options
 1. Custom client.
     1. firewall and tcp allow. Outgoing
-    1. Custom icon. Current `Resources/icon.ico`.
     1. Show license ?
     1. Do create service. Outgoing.
+
+## Refs
+
+1. [wxs](https://wixtoolset.org/docs/schema/wxs/)
+1. [wxs github](https://github.com/wixtoolset/wix)
diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py
index 23f1e28ea..b91d2302f 100644
--- a/res/msi/preprocess.py
+++ b/res/msi/preprocess.py
@@ -1,12 +1,38 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
+import json
 import sys
 import uuid
 import argparse
+import datetime
+import re
 from pathlib import Path
 
 g_indent_unit = "\t"
+g_version = ""
+g_build_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
+
+# https://learn.microsoft.com/en-us/windows/win32/msi/property-reference
+g_arpsystemcomponent = {
+    "Comments": {
+        "msi": "ARPCOMMENTS",
+        "t": "string",
+        "v": "!(loc.AR_Comment)",
+    },
+    "Contact": {
+        "msi": "ARPCONTACT",
+        "v": "https://github.com/rustdesk/rustdesk",
+    },
+    "HelpLink": {
+        "msi": "ARPHELPLINK",
+        "v": "https://github.com/rustdesk/rustdesk/issues/",
+    },
+    "ReadMe": {
+        "msi": "ARPREADME",
+        "v": "https://github.com/fufesou/rustdesk",
+    },
+}
 
 
 def make_parser():
@@ -14,6 +40,23 @@ def make_parser():
     parser.add_argument(
         "-d", "--debug", action="store_true", help="Is debug", default=False
     )
+    parser.add_argument(
+        "-ci", "--github-ci", action="store_true", help="Is github ci", default=False
+    )
+    parser.add_argument(
+        "-arp",
+        "--arp",
+        action="store_true",
+        help="Is ARPSYSTEMCOMPONENT",
+        default=False,
+    )
+    parser.add_argument(
+        "-custom-arp",
+        "--custom-arp",
+        type=str,
+        default="{}",
+        help='Custom arp properties, e.g. \'["Comments": {"msi": "ARPCOMMENTS", "v": "Remote control application."}]\'',
+    )
     parser.add_argument(
         "-c", "--custom", action="store_true", help="Is custom client", default=False
     )
@@ -21,13 +64,13 @@ def make_parser():
         "-an", "--app-name", type=str, default="RustDesk", help="The app name."
     )
     parser.add_argument(
-        "-v", "--version", type=str, default="1.2.4", help="The app version."
+        "-v", "--version", type=str, default="", help="The app version."
     )
     parser.add_argument(
         "-m",
         "--manufacturer",
         type=str,
-        default="Purslane Ltd",
+        default="RustDesk",
         help="The app manufacturer.",
     )
     return parser
@@ -103,7 +146,7 @@ def gen_pre_vars(args, build_dir):
 
         indent = g_indent_unit * 1
         to_insert_lines = [
-            f'{indent}<?define Version="{args.version}" ?>\n',
+            f'{indent}<?define Version="{g_version}" ?>\n',
             f'{indent}<?define Manufacturer="{args.manufacturer}" ?>\n',
             f'{indent}<?define Product="{args.app_name}" ?>\n',
             f'{indent}<?define Description="{args.app_name} Installer" ?>\n',
@@ -111,6 +154,7 @@ def gen_pre_vars(args, build_dir):
             f'{indent}<?define RegKeyRoot=".$(var.ProductLower)" ?>\n',
             f'{indent}<?define RegKeyInstall="$(var.RegKeyRoot)\Install" ?>\n',
             f'{indent}<?define BuildDir="{build_dir}" ?>\n',
+            f'{indent}<?define BuildDate="{g_build_date}" ?>\n',
             "\n",
             f"{indent}<!-- The UpgradeCode must be consistent for each product. ! -->\n"
             f'{indent}<?define UpgradeCode = "{upgrade_code}" ?>\n',
@@ -118,6 +162,7 @@ def gen_pre_vars(args, build_dir):
 
         for i, line in enumerate(to_insert_lines):
             lines.insert(index_start + i + 1, line)
+        return lines
 
     return gen_content_between_tags(
         "Package/Includes.wxi", "<!--$PreVarsStart$-->", "<!--$PreVarsEnd$-->", func
@@ -135,15 +180,15 @@ def replace_app_name_in_lans(app_name):
             f.writelines(lines)
 
 
-def gen_upgrade_info(version):
+def gen_upgrade_info():
     def func(lines, index_start):
         indent = g_indent_unit * 3
 
-        major, _, _ = version.split(".")
+        major, _, _ = g_version.split(".")
         upgrade_id = uuid.uuid4()
         to_insert_lines = [
             f'{indent}<Upgrade Id="{upgrade_id}">\n',
-            f'{indent}{g_indent_unit}<UpgradeVersion Property="OLD_VERSION_FOUND" Minimum="{major}.0.0.0" Maximum="{major}.99.99" IncludeMinimum="yes" IncludeMaximum="yes" OnlyDetect="no" IgnoreRemoveFailure="yes" MigrateFeatures="yes" />\n',
+            f'{indent}{g_indent_unit}<UpgradeVersion Property="OLD_VERSION_FOUND" Minimum="{major}.0.0" Maximum="{major}.99.99" IncludeMinimum="yes" IncludeMaximum="yes" OnlyDetect="no" IgnoreRemoveFailure="yes" MigrateFeatures="yes" />\n',
             f"{indent}</Upgrade>\n",
         ]
 
@@ -159,6 +204,175 @@ def gen_upgrade_info(version):
     )
 
 
+def gen_custom_dialog_bitmaps():
+    def func(lines, index_start):
+        indent = g_indent_unit * 2
+
+        # https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set
+        vars = [
+            "WixUIBannerBmp",
+            "WixUIDialogBmp",
+            "WixUIExclamationIco",
+            "WixUIInfoIco",
+            "WixUINewIco",
+            "WixUIUpIco",
+        ]
+        to_insert_lines = []
+        for var in vars:
+            if Path(f"Package/Resources/{var}.bmp").exists():
+                to_insert_lines.append(
+                    f'{indent}<WixVariable Id="{var}" Value="Resources\\{var}.bmp" />\n'
+                )
+
+        for i, line in enumerate(to_insert_lines):
+            lines.insert(index_start + i + 1, line)
+        return lines
+
+    return gen_content_between_tags(
+        "Package/Package.wxs",
+        "<!--$CustomBitmapsStart$-->",
+        "<!--$CustomBitmapsEnd$-->",
+        func,
+    )
+
+
+def gen_custom_ARPSYSTEMCOMPONENT_False(args):
+    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')
+
+        for i, line in enumerate(lines_new):
+            lines.insert(index_start + i + 1, line)
+        return lines
+
+    return gen_content_between_tags(
+        "Package/Fragments/AddRemoveProperties.wxs",
+        "<!--$ArpStart$-->",
+        "<!--$ArpEnd$-->",
+        func,
+    )
+
+
+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, build_dir):
+    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]{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]" />\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'
+        )
+
+        estimated_size = get_folder_size(build_dir)
+        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'
+        )
+
+        major, minor, build = g_version.split(".")
+        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'
+                lines_new.append(f'{indent}<RegistryValue Type="{t}" Name="{k}" Value="{v["v"]}" />\n')
+
+        for i, line in enumerate(lines_new):
+            lines.insert(index_start + i + 1, line)
+        return lines
+
+    return gen_content_between_tags(
+        "Package/Components/Regs.wxs",
+        "<!--$ArpStart$-->",
+        "<!--$ArpEnd$-->",
+        func,
+    )
+
+
+def gen_custom_ARPSYSTEMCOMPONENT(args, build_dir):
+    try:
+        custom_arp = json.loads(args.custom_arp)
+        g_arpsystemcomponent.update(custom_arp)
+    except json.JSONDecodeError as e:
+        print(f"Failed to decode custom arp: {e}")
+        return False
+
+    if args.arp:
+        return gen_custom_ARPSYSTEMCOMPONENT_True(args, build_dir)
+    else:
+        return gen_custom_ARPSYSTEMCOMPONENT_False(args)
+
+
 def gen_content_between_tags(filename, tag_start, tag_end, func):
     target_file = Path(sys.argv[0]).parent.joinpath(filename)
     lines, index_start = read_lines_and_start_index(target_file, tag_start, tag_end)
@@ -173,26 +387,69 @@ def gen_content_between_tags(filename, tag_start, tag_end, func):
     return True
 
 
+def init_global_vars(args):
+    var_file = "../../src/version.rs"
+    if not Path(var_file).exists():
+        print(f"Error: {var_file} not found")
+        return False
+
+    with open(var_file, "r") as f:
+        content = f.readlines()
+
+    global g_version
+    global g_build_date
+    g_version = args.version.replace("-", ".")
+    if g_version == "":
+        # pub const VERSION: &str = "1.2.4";
+        version_pattern = re.compile(r'.*VERSION: &str = "(.*)";.*')
+        for line in content:
+            match = version_pattern.match(line)
+            if match:
+                g_version = match.group(1)
+                break
+    if g_version == "":
+        print(f"Error: version not found in {var_file}")
+        return False
+
+    # pub const BUILD_DATE: &str = "2024-04-08 23:11";
+    build_date_pattern = re.compile(r'BUILD_DATE: &str = "(.*)";')
+    for line in content:
+        match = build_date_pattern.match(line)
+        if match:
+            g_build_date = match.group(1)
+            break
+
+    return True
+
+
 if __name__ == "__main__":
     parser = make_parser()
     args = parser.parse_args()
 
     app_name = args.app_name
     build_dir = (
-        Path(sys.argv[0])
-        .parent.joinpath(
-            f'../../flutter/build/windows/x64/runner/{"Debug" if args.debug else "Release"}'
-        )
-        .resolve()
+        f'../../flutter/build/windows/x64/runner/{"Debug" if args.debug else "Release"}'
     )
+    if args.github_ci:
+        build_dir = "../../rustdesk"
+    build_dir = Path(sys.argv[0]).parent.joinpath(build_dir).resolve()
+
+    if not init_global_vars(args):
+        sys.exit(-1)
 
     if not gen_pre_vars(args, build_dir):
         sys.exit(-1)
 
-    if not gen_upgrade_info(args.version):
+    if not gen_upgrade_info():
+        sys.exit(-1)
+
+    if not gen_custom_ARPSYSTEMCOMPONENT(args, build_dir):
         sys.exit(-1)
 
     if not gen_auto_component(app_name, build_dir):
         sys.exit(-1)
 
+    if not gen_custom_dialog_bitmaps():
+        sys.exit(-1)
+
     replace_app_name_in_lans(args.app_name)
diff --git a/src/platform/windows.rs b/src/platform/windows.rs
index 66317e127..baa932c40 100644
--- a/src/platform/windows.rs
+++ b/src/platform/windows.rs
@@ -1279,6 +1279,11 @@ fn get_before_uninstall(kill_self: bool) -> String {
 }
 
 fn get_uninstall(kill_self: bool) -> String {
+    let reg_uninstall_string = get_reg("UninstallString");
+    if reg_uninstall_string.to_lowercase().contains("msiexec.exe") {
+        return reg_uninstall_string;
+    }
+
     let mut uninstall_cert_cmd = "".to_string();
     if let Ok(exe) = std::env::current_exe() {
         if let Some(exe_path) = exe.to_str() {