Skip to content

Commit

Permalink
Optional Workload: Finalizer (#10356)
Browse files Browse the repository at this point in the history
MSI finalizer to remove optional workloads when uninstalling the SDK.
  • Loading branch information
joeloff authored May 5, 2021
1 parent 84bbbde commit 3c87114
Show file tree
Hide file tree
Showing 19 changed files with 440 additions and 8 deletions.
8 changes: 4 additions & 4 deletions .vsts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ stages:
pool:
${{ if eq(variables['System.TeamProject'], 'public') }}:
name: NetCorePublic-Pool
queue: buildpool.windows.10.amd64.vs2017.open
queue: buildpool.windows.10.amd64.vs2019.open
${{ if eq(variables['System.TeamProject'], 'internal') }}:
name: NetCoreInternal-Pool
queue: buildpool.windows.10.amd64.vs2017
queue: buildpool.windows.10.amd64.vs2019
timeoutInMinutes: 180
strategy:
matrix:
Expand Down Expand Up @@ -103,10 +103,10 @@ stages:
pool:
${{ if eq(variables['System.TeamProject'], 'public') }}:
name: NetCorePublic-Pool
queue: buildpool.windows.10.amd64.vs2017.open
queue: buildpool.windows.10.amd64.vs2019.open
${{ if eq(variables['System.TeamProject'], 'internal') }}:
name: NetCoreInternal-Pool
queue: buildpool.windows.10.amd64.vs2017
queue: buildpool.windows.10.amd64.vs2019
timeoutInMinutes: 180
strategy:
matrix:
Expand Down
42 changes: 42 additions & 0 deletions Native.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28603.18
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{ED2FE3E2-F7E7-4389-8231-B65123F2076F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "finalizer_shim", "src\finalizer_shim\finalizer_shim.csproj", "{688E2883-C5A9-4D66-A207-772C9160989C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
Debug|x64 = Debug|x64
Debug|arm64 = Debug|arm64
Release|x86 = Release|x86
Release|x64 = Release|x64
Release|arm64 = Release|arm64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{688E2883-C5A9-4D66-A207-772C9160989C}.Debug|x86.ActiveCfg = Debug|x86
{688E2883-C5A9-4D66-A207-772C9160989C}.Debug|x86.Build.0 = Debug|x86
{688E2883-C5A9-4D66-A207-772C9160989C}.Debug|x64.ActiveCfg = Debug|x64
{688E2883-C5A9-4D66-A207-772C9160989C}.Debug|x64.Build.0 = Debug|x64
{688E2883-C5A9-4D66-A207-772C9160989C}.Debug|arm64.ActiveCfg = Debug|arm64
{688E2883-C5A9-4D66-A207-772C9160989C}.Debug|arm64.Build.0 = Debug|arm64
{688E2883-C5A9-4D66-A207-772C9160989C}.Release|x86.ActiveCfg = Release|x86
{688E2883-C5A9-4D66-A207-772C9160989C}.Release|x86.Build.0 = Release|x86
{688E2883-C5A9-4D66-A207-772C9160989C}.Release|x64.ActiveCfg = Release|x64
{688E2883-C5A9-4D66-A207-772C9160989C}.Release|x64.Build.0 = Release|x64
{688E2883-C5A9-4D66-A207-772C9160989C}.Release|arm64.ActiveCfg = Release|arm64
{688E2883-C5A9-4D66-A207-772C9160989C}.Release|arm64.Build.0 = Release|arm64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{688E2883-C5A9-4D66-A207-772C9160989C} = {ED2FE3E2-F7E7-4389-8231-B65123F2076F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {18FCFCA3-D1A8-4D3A-9763-6A658D0D726F}
EndGlobalSection
EndGlobal
8 changes: 8 additions & 0 deletions eng/Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>
<ItemGroup>
<ProjectToBuild Include="$(RepoRoot)Microsoft.DotNet.Cli.sln" />
<ProjectToBuild Condition="'$(OS)' == 'Windows_NT' And ('$(Architecture)' == 'x86' Or '$(Architecture)' == 'x64' Or '$(Architecture)' == 'arm64')"
Include="$(RepoRoot)eng\version.csproj;
$(RepoRoot)eng\native.proj" />
</ItemGroup>
</Project>
3 changes: 3 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@
<HostFxrVersion>$(MicrosoftNETCoreAppRuntimePackageVersion)</HostFxrVersion>
<SharedHostVersion>$(MicrosoftNETCoreAppRuntimePackageVersion)</SharedHostVersion>
</PropertyGroup>
<PropertyGroup>
<WixPackageVersion>3.14.0-dotnet</WixPackageVersion>
</PropertyGroup>
<PropertyGroup>
<!-- 6.0 Template versions -->
<MicrosoftDotnetWinFormsProjectTemplates60PackageVersion>$(MicrosoftDotnetWinFormsProjectTemplatesPackageVersion)</MicrosoftDotnetWinFormsProjectTemplates60PackageVersion>
Expand Down
2 changes: 2 additions & 0 deletions eng/configure-toolset.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

$script:useInstalledDotNetCli = $false

# Add CMake to the path.
$env:PATH = "$PSScriptRoot\..\.tools\bin;$env:PATH"
5 changes: 4 additions & 1 deletion eng/configure-toolset.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# SdkTests do not currently work with globally installed CLI as they use dotnet-install.ps1 to install more runtimes

useInstalledDotNetCli="false"
useInstalledDotNetCli="false"

# Working around issue https://github.com/dotnet/arcade/issues/7327
DisableNativeToolsetInstalls=true
16 changes: 16 additions & 0 deletions eng/native.proj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. -->
<Project>
<PropertyGroup>
<Platform Condition="'$(Platform)'=='' Or $(Platform)=='AnyCPU'">$(Architecture)</Platform>
</PropertyGroup>

<Target Name="Build">
<MSBuild Projects="$(RepoRoot)Native.sln" Properties="Platform=$(Platform)" Targets="Restore;Build" />
</Target>

<Target Name="Test" />
<Target Name="Pack" />
<Target Name="Rebuild" />
<Target Name="IntegrationTest" />
<Target Name="PerformanceTest" />
</Project>
8 changes: 8 additions & 0 deletions eng/version.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFramework>$(CoreSdkTargetFramework)</TargetFramework>
<NativeVersionFile>$(ArtifactsObjDir)sdk_version.h</NativeVersionFile>
</PropertyGroup>

<Target Name="GenerateSdkVersionFile" BeforeTargets="CoreCompile" DependsOnTargets="GenerateNativeVersionFile" />
</Project>
6 changes: 5 additions & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
]
}
},
"native-tools": {
"cmake": "3.16.4"
},
"msbuild-sdks": {
"Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21253.2"
"Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21253.2",
"Microsoft.DotNet.CMake.Sdk": "6.0.0-beta.21253.2"
}
}
39 changes: 39 additions & 0 deletions src/finalizer/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
cmake_minimum_required(VERSION 3.15.5)

# Create project named finalizer, this will
# will generate Finalizer.vcxproj
project(Finalizer)

set(CMAKE_MACOSX_RPATH 1)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS /ENTRY:wmainCRTStartup")

# The WiX SDK is extracted from a NuGet package using an SDK .csproj (finalizer_shim)
# that copies the "lib" and "inc" folders to a stable location.

include_directories(../../artifacts/WixSdk/inc)
include_directories(../../artifacts/obj)
link_directories(../../artifacts/WixSdk/lib/${Platform})

add_compile_options(/MT)

add_executable(Finalizer
finalizer.cpp
native.rc
)

# These are normally part of a .vcxproj in Visual Studio, but
# appears to be missing when CMAKE generates a .vcxproj
# for arm64.
target_link_libraries(Finalizer shell32.lib)
target_link_libraries(Finalizer advapi32.lib)
target_link_libraries(Finalizer version.lib)
target_link_libraries(Finalizer msi.lib)

# Add WiX libraries
target_link_libraries(Finalizer wcautil.lib)
target_link_libraries(Finalizer dutil.lib)

install(TARGETS Finalizer)
211 changes: 211 additions & 0 deletions src/finalizer/finalizer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#include "precomp.h"

extern "C" HRESULT Initialize(int argc, wchar_t* argv[])
{
HRESULT hr = S_OK;

// We're not going to do any clever parsing. This is intended to be called from
// the standalone bundle only and there will only be two parameters:
// 1. The path of the log file, created by the bundle.
// 2. The dependent we're trying to clean up.
if (argc != 3)
{
return HRESULT_FROM_WIN32(ERROR_INVALID_COMMAND_LINE);
}

LogInitialize(::GetModuleHandleW(NULL));

#ifdef _DEBUG
LogSetLevel(REPORT_DEBUG, FALSE);
#else
LogSetLevel(REPORT_VERBOSE, FALSE); // FALSE means don't write an additional text line to the log saying the level changed
#endif

hr = LogOpen(NULL, argv[1], NULL, NULL, FALSE, TRUE, NULL);
ExitOnFailure(hr, "Failed to create log file.");

hr = RegInitialize();
ExitOnFailure(hr, "Failed to initialize the registry.");

hr = WiuInitialize();
ExitOnFailure(hr, "Failed to initialize Windows Installer.");

LExit:
return hr;
}

extern "C" HRESULT RemoveDependent(LPWSTR sczDependent, BOOL* pbRestartRequired)
{
HRESULT hr = S_OK;
HKEY hkInstallerDependenciesKey = NULL;
HKEY hkProviderKey = NULL;
HKEY hkDependentsKey = NULL;
LPWSTR sczProviderKey = NULL;
LPWSTR sczDependentsKey = NULL;
LPWSTR sczProductId = NULL;
LPWSTR sczProductName = NULL;
DWORD cSubKeys = 0;
DWORD dwExitCode = 0;
WIU_RESTART restart = WIU_RESTART_NONE;

// Optional workloads are always per-machine installs, so we don't need to check HKCU.
hr = RegOpen(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Classes\\Installer\\Dependencies", KEY_READ, &hkInstallerDependenciesKey);
ExitOnFailure(hr, "Failed to read installer dependencies key.");

// This has to be an exhaustive search as we're not looking for a specific provider key, but for a specific dependent
// that could be registered against any provider key.
for (DWORD dwIndex = 0;; ++dwIndex)
{
// Get the next provider key name
hr = RegKeyEnum(hkInstallerDependenciesKey, dwIndex, &sczProviderKey);

if (E_NOMOREITEMS == hr)
{
hr = S_OK;
break;
}

ExitOnFailure(hr, "Failed to enumerate installer dependency provider keys.");
LogStringLine(REPORT_STANDARD, "Processing provider key: %ls", sczProviderKey);

hr = RegOpen(hkInstallerDependenciesKey, sczProviderKey, KEY_READ, &hkProviderKey);
ExitOnFailure(hr, "Unable to open provider key.");

// Open the dependents key with write permissions so we can modify it if it matches
// the target dependent value.
hr = RegOpen(hkProviderKey, L"Dependents", KEY_READ | KEY_WRITE, &hkDependentsKey);
if (E_FILENOTFOUND == hr)
{
// Providers can sometimes become orphaned duirng uninstalls. If there's no Dependents subkey, we just
// release the handle and continue to the next provider key.
hr = S_OK;
ReleaseRegKey(hkProviderKey);

continue;
}

ExitOnFailure(hr, "Unable to open dependents key.");

// Enumerate over all the dependent keys
for (DWORD dwDepdentsKeyIndex = 0;; ++dwDepdentsKeyIndex)
{
hr = RegKeyEnum(hkDependentsKey, dwDepdentsKeyIndex, &sczDependentsKey);

if (E_NOMOREITEMS == hr)
{
hr = S_OK;
break;
}

ExitOnFailure(hr, "Failed to read provider's dependent key.");

if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, sczDependentsKey, -1, sczDependent, -1))
{
LogStringLine(REPORT_STANDARD, " Dependent match found: %ls", sczDependentsKey);

hr = RegDelete(hkDependentsKey, sczDependent, REG_KEY_DEFAULT, TRUE);
ExitOnFailure(hr, "Failed to delete dependent \"%ls\"", sczDependent);
LogStringLine(REPORT_STANDARD, " Dependent deleted");
// Reset the index since we're deleting keys while enumerating
dwDepdentsKeyIndex = dwDepdentsKeyIndex > 1 ? dwDepdentsKeyIndex-- : 0;

// Check if there are any subkeys remaining under the dependents key. If not, we
// can uninstall the MSI. We'll recheck the key again in case the MSI fails to clean up the
// provider key to make sure we don't have orphaned keys.
hr = RegQueryKey(hkDependentsKey, &cSubKeys, NULL);
ExitOnFailure(hr, "Failed to query dependents key.");

LogStringLine(REPORT_STANDARD, " Remaining dependents: %i", cSubKeys);

if (0 == cSubKeys)
{
// This was the final dependent, so now we can remove the installation if the provider wasn't corrupted and
// still contains the product ID.
hr = RegReadString(hkProviderKey, NULL, &sczProductId);

if (E_FILENOTFOUND == hr)
{
LogStringLine(REPORT_STANDARD, " No product ID found, provider key: %ls", sczProviderKey);
hr = S_OK;
break;
}
else
{
ExitOnFailure(hr, "Failed to read product ID.");
}

// Let's make sure the product is actually installed. The provider key for an MSI typically
// stores the ProductCode, DisplayName, and Version, but by calling into MsiGetProductInfo,
// we're doing an implicit detect and getting a property back.
hr = WiuGetProductInfo(sczProductId, L"ProductName", &sczProductName);
if (SUCCEEDED(hr))
{
// The provider key *should* have the ProductName and ProductVersion properties, but since
// we know it's installed, we just query the installer service.
MsiSetInternalUI(INSTALLUILEVEL_NONE, NULL);
hr = WiuConfigureProductEx(sczProductId, INSTALLLEVEL_DEFAULT, INSTALLSTATE_ABSENT, L"MSIFASTINSTALL=7 IGNOREDEPENDENCIES=ALL REBOOT=ReallySuppress", &restart);
LogStringLine(REPORT_STANDARD, " Uninstall of \"%ls\" (%ls%) exited with 0x%.8x", sczProductName, sczProductId, hr);

// Flag any reboot since we need to return that to the bundle.
if (WIU_RESTART_INITIATED == restart || WIU_RESTART_REQUIRED == restart)
{
LogStringLine(REPORT_STANDARD, " Reboot requested, deferring.");
*pbRestartRequired = TRUE;
}

// Reset potential failures so we can continue to remove as many dependents as possible.
hr = S_OK;
}
else if (HRESULT_FROM_WIN32(ERROR_UNKNOWN_PRODUCT) == hr || HRESULT_FROM_WIN32(ERROR_UNKNOWN_PROPERTY) == hr)
{
// Possibly a corrupted provider key that wasn't cleaned up. We'll just ignore it.
LogStringLine(REPORT_STANDARD, " Product is not installed, ProductCode:%ls, result: 0x%.8x", sczProductId, hr);
hr = S_OK;
}
}
}
}

ReleaseRegKey(hkDependentsKey);
ReleaseRegKey(hkProviderKey);
}

LExit:
ReleaseStr(sczProductName);
ReleaseStr(sczProductId);
ReleaseStr(sczProviderKey);
ReleaseStr(sczDependentsKey);
ReleaseRegKey(hkDependentsKey);
ReleaseRegKey(hkProviderKey);
ReleaseRegKey(hkInstallerDependenciesKey);
return hr;
}

int wmain(int argc, wchar_t* argv[])
{
HRESULT hr = S_OK;
DWORD dwExitCode = 0;
LPWSTR sczDependent = NULL;
BOOL bRestartRequired = FALSE;

hr = ::Initialize(argc, argv);
ExitOnFailure(hr, "Failed to initialize.");

sczDependent = argv[2];
hr = ::RemoveDependent(sczDependent, &bRestartRequired);
ExitOnFailure(hr, "Failed to remove dependent \"%ls\".", sczDependent);

if (bRestartRequired)
{
dwExitCode = ERROR_SUCCESS_REBOOT_REQUIRED;
}

LExit:
LogUninitialize(TRUE);
RegUninitialize();
WiuUninitialize();
return FAILED(hr) ? (int)hr : (int)dwExitCode;
}
Loading

0 comments on commit 3c87114

Please sign in to comment.