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=""[SystemFolder]netsh.exe" advfirewall firewall add rule name="$(var.Product) Service" 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=""[SystemFolder]netsh.exe" advfirewall firewall add rule name="$(var.Product) Service" 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=""[SystemFolder]netsh.exe" advfirewall firewall delete rule name="$(var.Product) Service"" 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~="ALL"" /> - <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() {