Ensure finalizer doesn't remove workloads during upgrades (#10999)

This commit is contained in:
Jacques Eloff 2021-07-07 08:04:20 -07:00 committed by GitHub
parent addb1b1961
commit ef74b6ca71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 290 additions and 161 deletions

View file

@ -5,207 +5,338 @@
extern "C" HRESULT Initialize(int argc, wchar_t* argv[]) extern "C" HRESULT Initialize(int argc, wchar_t* argv[])
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
// We're not going to do any clever parsing. This is intended to be called from // 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: // the standalone bundle only and there will only be a fixed set of parameters:
// 1. The path of the log file, created by the bundle. // 1. The path of the log file, created by the bundle.
// 2. The dependent we're trying to clean up. // 2. The full SDK version, e.g. 6.0.105 or 6.0.398-preview19
if (argc != 3) // 3. Target platform to search under the registry key to locate installed SDKs.
{ if (4 != argc)
return HRESULT_FROM_WIN32(ERROR_INVALID_COMMAND_LINE); {
} return HRESULT_FROM_WIN32(ERROR_INVALID_COMMAND_LINE);
}
LogInitialize(::GetModuleHandleW(NULL)); LogInitialize(::GetModuleHandleW(NULL));
#ifdef _DEBUG #ifdef _DEBUG
LogSetLevel(REPORT_DEBUG, FALSE); LogSetLevel(REPORT_DEBUG, FALSE);
#else #else
LogSetLevel(REPORT_VERBOSE, FALSE); // FALSE means don't write an additional text line to the log saying the level changed LogSetLevel(REPORT_VERBOSE, FALSE); // FALSE means don't write an additional text line to the log saying the level changed
#endif #endif
hr = LogOpen(NULL, argv[1], NULL, NULL, FALSE, TRUE, NULL); hr = LogOpen(NULL, argv[1], NULL, NULL, FALSE, TRUE, NULL);
ExitOnFailure(hr, "Failed to create log file."); ExitOnFailure(hr, "Failed to create log file.");
hr = RegInitialize(); hr = RegInitialize();
ExitOnFailure(hr, "Failed to initialize the registry."); ExitOnFailure(hr, "Failed to initialize the registry.");
hr = WiuInitialize(); hr = WiuInitialize();
ExitOnFailure(hr, "Failed to initialize Windows Installer."); ExitOnFailure(hr, "Failed to initialize Windows Installer.");
LExit: LExit:
return hr; return hr;
} }
extern "C" HRESULT RemoveDependent(LPWSTR sczDependent, BOOL* pbRestartRequired) extern "C" HRESULT RemoveDependent(LPWSTR sczDependent, BOOL * pbRestartRequired)
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
HKEY hkInstallerDependenciesKey = NULL; HKEY hkInstallerDependenciesKey = NULL;
HKEY hkProviderKey = NULL; HKEY hkProviderKey = NULL;
HKEY hkDependentsKey = NULL; HKEY hkDependentsKey = NULL;
LPWSTR sczProviderKey = NULL; LPWSTR sczProviderKey = NULL;
LPWSTR sczDependentsKey = NULL; LPWSTR sczDependentsKey = NULL;
LPWSTR sczProductId = NULL; LPWSTR sczProductId = NULL;
LPWSTR sczProductName = NULL; LPWSTR sczProductName = NULL;
DWORD cSubKeys = 0; DWORD cSubKeys = 0;
DWORD dwExitCode = 0; DWORD dwExitCode = 0;
WIU_RESTART restart = WIU_RESTART_NONE; WIU_RESTART restart = WIU_RESTART_NONE;
// Optional workloads are always per-machine installs, so we don't need to check HKCU. // 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); hr = RegOpen(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Classes\\Installer\\Dependencies", KEY_READ, &hkInstallerDependenciesKey);
ExitOnFailure(hr, "Failed to read installer dependencies key."); 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 // 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. // that could be registered against any provider key.
for (DWORD dwIndex = 0;; ++dwIndex) for (DWORD dwIndex = 0;; ++dwIndex)
{ {
// Get the next provider key name // Get the next provider key name
hr = RegKeyEnum(hkInstallerDependenciesKey, dwIndex, &sczProviderKey); hr = RegKeyEnum(hkInstallerDependenciesKey, dwIndex, &sczProviderKey);
if (E_NOMOREITEMS == hr) if (E_NOMOREITEMS == hr)
{ {
hr = S_OK; hr = S_OK;
break; break;
} }
ExitOnFailure(hr, "Failed to enumerate installer dependency provider keys."); ExitOnFailure(hr, "Failed to enumerate installer dependency provider keys.");
LogStringLine(REPORT_STANDARD, "Processing provider key: %ls", sczProviderKey); LogStringLine(REPORT_STANDARD, "Processing provider key: %ls", sczProviderKey);
hr = RegOpen(hkInstallerDependenciesKey, sczProviderKey, KEY_READ, &hkProviderKey); hr = RegOpen(hkInstallerDependenciesKey, sczProviderKey, KEY_READ, &hkProviderKey);
ExitOnFailure(hr, "Unable to open provider key."); ExitOnFailure(hr, "Unable to open provider key.");
// Open the dependents key with write permissions so we can modify it if it matches // Open the dependents key with write permissions so we can modify it if it matches
// the target dependent value. // the target dependent value.
hr = RegOpen(hkProviderKey, L"Dependents", KEY_READ | KEY_WRITE, &hkDependentsKey); hr = RegOpen(hkProviderKey, L"Dependents", KEY_READ | KEY_WRITE, &hkDependentsKey);
if (E_FILENOTFOUND == hr) if (E_FILENOTFOUND == hr)
{ {
// Providers can sometimes become orphaned during uninstalls. If there's no Dependents subkey, we just // Providers can sometimes become orphaned during uninstalls. If there's no Dependents subkey, we just
// release the handle and continue to the next provider key. // release the handle and continue to the next provider key.
hr = S_OK; hr = S_OK;
ReleaseRegKey(hkProviderKey); ReleaseRegKey(hkProviderKey);
continue; continue;
} }
ExitOnFailure(hr, "Unable to open dependents key."); ExitOnFailure(hr, "Unable to open dependents key.");
// Enumerate over all the dependent keys // Enumerate over all the dependent keys
for (DWORD dwDependentsKeyIndex = 0;; ++dwDependentsKeyIndex) for (DWORD dwDependentsKeyIndex = 0;; ++dwDependentsKeyIndex)
{ {
hr = RegKeyEnum(hkDependentsKey, dwDependentsKeyIndex, &sczDependentsKey); hr = RegKeyEnum(hkDependentsKey, dwDependentsKeyIndex, &sczDependentsKey);
if (E_NOMOREITEMS == hr) if (E_NOMOREITEMS == hr)
{ {
hr = S_OK; hr = S_OK;
break; break;
} }
ExitOnFailure(hr, "Failed to read provider's dependent key."); ExitOnFailure(hr, "Failed to read provider's dependent key.");
if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, sczDependentsKey, -1, sczDependent, -1)) if (CSTR_EQUAL == ::CompareStringW(LOCALE_INVARIANT, 0, sczDependentsKey, -1, sczDependent, -1))
{ {
LogStringLine(REPORT_STANDARD, " Dependent match found: %ls", sczDependentsKey); LogStringLine(REPORT_STANDARD, " Dependent match found: %ls", sczDependentsKey);
hr = RegDelete(hkDependentsKey, sczDependent, REG_KEY_DEFAULT, TRUE); hr = RegDelete(hkDependentsKey, sczDependent, REG_KEY_DEFAULT, TRUE);
ExitOnFailure(hr, "Failed to delete dependent \"%ls\"", sczDependent); ExitOnFailure(hr, "Failed to delete dependent \"%ls\"", sczDependent);
LogStringLine(REPORT_STANDARD, " Dependent deleted"); LogStringLine(REPORT_STANDARD, " Dependent deleted");
// Reset the index since we're deleting keys while enumerating // Reset the index since we're deleting keys while enumerating
dwDependentsKeyIndex = dwDependentsKeyIndex > 1 ? dwDependentsKeyIndex-- : 0; dwDependentsKeyIndex = dwDependentsKeyIndex > 1 ? dwDependentsKeyIndex-- : 0;
// Check if there are any subkeys remaining under the dependents key. If not, we // 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 // 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. // provider key to make sure we don't have orphaned keys.
hr = RegQueryKey(hkDependentsKey, &cSubKeys, NULL); hr = RegQueryKey(hkDependentsKey, &cSubKeys, NULL);
ExitOnFailure(hr, "Failed to query dependents key."); ExitOnFailure(hr, "Failed to query dependents key.");
LogStringLine(REPORT_STANDARD, " Remaining dependents: %i", cSubKeys); LogStringLine(REPORT_STANDARD, " Remaining dependents: %i", cSubKeys);
if (0 == cSubKeys) if (0 == cSubKeys)
{ {
// This was the final dependent, so now we can remove the installation if the provider wasn't corrupted and // This was the final dependent, so now we can remove the installation if the provider wasn't corrupted and
// still contains the product ID. // still contains the product ID.
hr = RegReadString(hkProviderKey, NULL, &sczProductId); hr = RegReadString(hkProviderKey, NULL, &sczProductId);
if (E_FILENOTFOUND == hr) if (E_FILENOTFOUND == hr)
{ {
LogStringLine(REPORT_STANDARD, " No product ID found, provider key: %ls", sczProviderKey); LogStringLine(REPORT_STANDARD, " No product ID found, provider key: %ls", sczProviderKey);
hr = S_OK; hr = S_OK;
break; break;
} }
else else
{ {
ExitOnFailure(hr, "Failed to read product ID."); ExitOnFailure(hr, "Failed to read product ID.");
} }
// Let's make sure the product is actually installed. The provider key for an MSI typically // 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, // stores the ProductCode, DisplayName, and Version, but by calling into MsiGetProductInfo,
// we're doing an implicit detect and getting a property back. // we're doing an implicit detect and getting a property back.
hr = WiuGetProductInfo(sczProductId, L"ProductName", &sczProductName); hr = WiuGetProductInfo(sczProductId, L"ProductName", &sczProductName);
if (SUCCEEDED(hr)) if (SUCCEEDED(hr))
{ {
// The provider key *should* have the ProductName and ProductVersion properties, but since // The provider key *should* have the ProductName and ProductVersion properties, but since
// we know it's installed, we just query the installer service. // we know it's installed, we just query the installer service.
MsiSetInternalUI(INSTALLUILEVEL_NONE, NULL); MsiSetInternalUI(INSTALLUILEVEL_NONE, NULL);
hr = WiuConfigureProductEx(sczProductId, INSTALLLEVEL_DEFAULT, INSTALLSTATE_ABSENT, L"MSIFASTINSTALL=7 IGNOREDEPENDENCIES=ALL REBOOT=ReallySuppress", &restart); 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); 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. // Flag any reboot since we need to return that to the bundle.
if (WIU_RESTART_INITIATED == restart || WIU_RESTART_REQUIRED == restart) if (WIU_RESTART_INITIATED == restart || WIU_RESTART_REQUIRED == restart)
{ {
LogStringLine(REPORT_STANDARD, " Reboot requested, deferring."); LogStringLine(REPORT_STANDARD, " Reboot requested, deferring.");
*pbRestartRequired = TRUE; *pbRestartRequired = TRUE;
} }
// Reset potential failures so we can continue to remove as many dependents as possible. // Reset potential failures so we can continue to remove as many dependents as possible.
hr = S_OK; hr = S_OK;
} }
else if (HRESULT_FROM_WIN32(ERROR_UNKNOWN_PRODUCT) == hr || HRESULT_FROM_WIN32(ERROR_UNKNOWN_PROPERTY) == hr) 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. // 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); LogStringLine(REPORT_STANDARD, " Product is not installed, ProductCode:%ls, result: 0x%.8x", sczProductId, hr);
hr = S_OK; hr = S_OK;
} }
} }
} }
} }
ReleaseRegKey(hkDependentsKey); ReleaseRegKey(hkDependentsKey);
ReleaseRegKey(hkProviderKey); ReleaseRegKey(hkProviderKey);
} }
LExit: LExit:
ReleaseStr(sczProductName); ReleaseStr(sczProductName);
ReleaseStr(sczProductId); ReleaseStr(sczProductId);
ReleaseStr(sczProviderKey); ReleaseStr(sczProviderKey);
ReleaseStr(sczDependentsKey); ReleaseStr(sczDependentsKey);
ReleaseRegKey(hkDependentsKey); ReleaseRegKey(hkDependentsKey);
ReleaseRegKey(hkProviderKey); ReleaseRegKey(hkProviderKey);
ReleaseRegKey(hkInstallerDependenciesKey); ReleaseRegKey(hkInstallerDependenciesKey);
return hr; return hr;
}
extern "C" HRESULT ParseSdkVersion(LPWSTR sczSdkVersion, INT * piMajor, INT * piMinor, INT * piFeatureBand)
{
HRESULT hr = S_OK;
UINT cVersionParts = 0;
LPWSTR* rgsczVersionParts = NULL;
int iPatch = 0;
hr = StrSplitAllocArray(&rgsczVersionParts, &cVersionParts, sczSdkVersion, L".");
ExitOnFailure(hr, "Failed to split version.");
// We only care about the major.minor.patch values
// to convert to a feature band. If we don't have at least
// all 3 parts, we'll ignore the value.
if (3 > cVersionParts)
{
ExitOnFailure(E_INVALIDARG, "Invalid SDK version: %ls %li", sczSdkVersion, cVersionParts);
}
hr = StrStringToInt32(rgsczVersionParts[0], 0, piMajor);
ExitOnFailure(hr, "Invalid major version.");
hr = StrStringToInt32(rgsczVersionParts[1], 0, piMinor);
ExitOnFailure(hr, "Invalid minor version.");
// If this is a valid SDK version the 'patch' should be a 3 digit field
// containing the feature band and patch level, e.g. 100 or 207. We
// can discard any prerelease labels from the semantic version.
hr = StrStringToInt32(rgsczVersionParts[2], 3, &iPatch);
ExitOnFailure(hr, "Invalid patch version.");
if (100 > iPatch)
{
hr = E_INVALIDARG;
ExitOnFailure(hr, "Invalid SDK feature band and patch level.");
}
*piFeatureBand = iPatch - (iPatch % 100);
LExit:
ReleaseStrArray(rgsczVersionParts, cVersionParts);
return hr;
}
extern "C" HRESULT DetectSdk(LPWSTR sczSdkFeatureBandVersion, LPWSTR sczArchitecture, BOOL * pbInstalled)
{
HRESULT hr = S_OK;
HKEY hkInstalledSdkVersionsKey = NULL;
LPWSTR sczInstalledSdkVersionsKeyName = NULL;
LPWSTR sczSdkVersion = NULL;
DWORD dwSdkVersionValueType = 0;
int iInstalledMajor = 0;
int iInstalledMinor = 0;
int iInstalledFeatureBand = 0;
int iExpectedMajor = 0;
int iExpectedMinor = 0;
int iExpectedFeatureBand = 0;
hr = ParseSdkVersion(sczSdkFeatureBandVersion, &iExpectedMajor, &iExpectedMinor, &iExpectedFeatureBand);
LogStringLine(REPORT_STANDARD, "Detecting installed SDK versions for %ls", sczSdkFeatureBandVersion);
// Scan the registry to see if any SDK matching the feature band we're trying to
// clean up is still installed. All the installation keys reside in the 32-bit hive.
hr = StrAllocConcatFormatted(&sczInstalledSdkVersionsKeyName, L"SOFTWARE\\WOW6432Node\\dotnet\\Setup\\InstalledVersions\\%ls\\sdk", sczArchitecture);
ExitOnFailure(hr, "Failed to allocate string for installed SDK versions.");
LogStringLine(REPORT_STANDARD, "Scanning %ls", sczInstalledSdkVersionsKeyName);
hr = RegOpen(HKEY_LOCAL_MACHINE, sczInstalledSdkVersionsKeyName, KEY_READ, &hkInstalledSdkVersionsKey);
ExitOnFailure(hr, "Failed to read installed versions key.");
for (DWORD dwSdkVersionsValueIndex = 0;; ++dwSdkVersionsValueIndex)
{
hr = RegValueEnum(hkInstalledSdkVersionsKey, dwSdkVersionsValueIndex, &sczSdkVersion, &dwSdkVersionValueType);
if (E_NOMOREITEMS == hr)
{
hr = S_OK;
break;
}
ExitOnFailure(hr, "Failed to read SDK version values from registry.");
hr = ParseSdkVersion(sczSdkVersion, &iInstalledMajor, &iInstalledMinor, &iInstalledFeatureBand);
ExitOnFailure(hr, "Failed to parse %ls", sczSdkVersion);
LogStringLine(REPORT_STANDARD, "SDK version detected: %ls, mapping to %li.%li.%li.", sczSdkVersion, iInstalledMajor, iInstalledMinor, iInstalledFeatureBand);
// Bail out on the first match.
if ((iInstalledMajor == iExpectedMajor) && (iInstalledMinor == iExpectedMinor) && (iInstalledFeatureBand == iExpectedFeatureBand))
{
*pbInstalled = TRUE;
break;
}
}
LExit:
ReleaseRegKey(hkInstalledSdkVersionsKey);
ReleaseStr(sczInstalledSdkVersionsKeyName);
ReleaseStr(sczSdkVersion);
return hr;
} }
int wmain(int argc, wchar_t* argv[]) int wmain(int argc, wchar_t* argv[])
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
DWORD dwExitCode = 0; DWORD dwExitCode = 0;
LPWSTR sczDependent = NULL; LPWSTR sczDependent = NULL;
BOOL bRestartRequired = FALSE; LPWSTR sczFeatureBandVersion = NULL;
BOOL bRestartRequired = FALSE;
BOOL bSdkFeatureBandInstalled = FALSE;
int iMajor = 0;
int iMinor = 0;
int iFeatureBand = 0;
hr = ::Initialize(argc, argv); hr = ::Initialize(argc, argv);
ExitOnFailure(hr, "Failed to initialize."); ExitOnFailure(hr, "Failed to initialize.");
sczDependent = argv[2]; // Convert the full SDK version to a feature band version
hr = ::RemoveDependent(sczDependent, &bRestartRequired); hr = ParseSdkVersion(argv[2], &iMajor, &iMinor, &iFeatureBand);
ExitOnFailure(hr, "Failed to remove dependent \"%ls\".", sczDependent); ExitOnFailure(hr, "Failed to parse version, %ls.", argv[2]);
if (bRestartRequired) hr = StrAllocConcatFormatted(&sczFeatureBandVersion, L"%li.%li.%li", iMajor, iMinor, iFeatureBand);
{ ExitOnFailure(hr, "Failed to create feature band version.");
dwExitCode = ERROR_SUCCESS_REBOOT_REQUIRED;
} // Create the dependent value, e.g., Microsoft.NET.Sdk,6.0.300,arm64
hr = StrAllocConcatFormatted(&sczDependent, L"Microsoft.NET.Sdk,%ls,%ls", sczFeatureBandVersion, argv[3]);
ExitOnFailure(hr, "Failed to create dependent.");
LogStringLine(REPORT_STANDARD, "Setting target dependent to %ls.", sczDependent);
hr = ::DetectSdk(sczFeatureBandVersion, argv[3], &bSdkFeatureBandInstalled);
ExitOnFailure(hr, "Failed to detect installed SDKs.");
if (bSdkFeatureBandInstalled)
{
goto LExit;
}
LogStringLine(REPORT_STANDARD, "SDK with feature band %ls could not be found.", sczFeatureBandVersion);
hr = ::RemoveDependent(sczDependent, &bRestartRequired);
ExitOnFailure(hr, "Failed to remove dependent \"%ls\".", sczDependent);
if (bRestartRequired)
{
dwExitCode = ERROR_SUCCESS_REBOOT_REQUIRED;
}
LExit: LExit:
LogUninitialize(TRUE); ReleaseStr(sczDependent);
RegUninitialize(); ReleaseStr(sczFeatureBandVersion);
WiuUninitialize(); LogUninitialize(TRUE);
return FAILED(hr) ? (int)hr : (int)dwExitCode; RegUninitialize();
WiuUninitialize();
return FAILED(hr) ? (int)hr : (int)dwExitCode;
} }

View file

@ -206,7 +206,7 @@
DetectCondition="WixBundleAction >= 3" DetectCondition="WixBundleAction >= 3"
Id="Finalizer" Id="Finalizer"
InstallCondition="WixBundleAction >= 4" InstallCondition="WixBundleAction >= 4"
UninstallCommand=""[WixBundleLog_Finalizer]" $(var.SdkDependent)" UninstallCommand=""[WixBundleLog_Finalizer]" $(var.NugetVersion) $(var.Platform)"
Vital="no" /> Vital="no" />
</Chain> </Chain>
</Bundle> </Bundle>

View file

@ -45,6 +45,4 @@
<?define DependencyKey = "$(var.DependencyKeyName)_$(var.SDKBundleVersion)_$(var.Platform)"?> <?define DependencyKey = "$(var.DependencyKeyName)_$(var.SDKBundleVersion)_$(var.Platform)"?>
<?define DependencyKeyId = "$(var.DependencyKey)" ?> <?define DependencyKeyId = "$(var.DependencyKey)" ?>
<!-- The dependent value associated with this SDK when creating reference counts for optional workloads. -->
<?define SdkDependent = "Microsoft.NET.SDK,$(var.NugetVersion),$(var.Platform)"?>
</Include> </Include>