File: Program.cs
Web Access
Project: ..\..\..\src\Layout\finalizer\finalizer.csproj (finalizer)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.NET.Sdk.WorkloadManifestReader;
using Microsoft.Win32;
using Microsoft.Win32.Msi;
 
if (args.Length < 4)
{
    return (int)Error.INVALID_COMMAND_LINE;
}
 
string logPath = args[0];
string sdkVersion = args[1];
string platform = args[2];
int bundleAction = Convert.ToInt32(args[3]);
 
using StreamWriter logStream = new StreamWriter(logPath);
 
Logger.Init(logStream);
 
Logger.Log($"{nameof(logPath)}: {logPath}");
Logger.Log($"{nameof(sdkVersion)}: {sdkVersion}");
Logger.Log($"{nameof(platform)}: {platform}");
Logger.Log($"{nameof(bundleAction)}: {bundleAction}");
int exitCode = (int)Error.SUCCESS;
 
// The finalizer should only run when the parent bundle is being removed if WixBundleAction is set to BOOTSTRAPPER_ACTION_UNINSTALL
// or BOOTSTRAPPER_ACTION_UNSAFE_UNINSTALL.
if (bundleAction < 3 || bundleAction > 4)
{
    return exitCode;
}
 
try
{
    // Step 1: Parse and format SDK feature band version
    SdkFeatureBand featureBandVersion = new SdkFeatureBand(sdkVersion);
    string dependent = $"Microsoft.NET.Sdk,{featureBandVersion},{platform}";
 
    // Step 2: Check if SDK feature band is installed
    if (DetectSdk(featureBandVersion, platform))
    {
        return (int)Error.SUCCESS;
    }
 
    // Step 3: Remove dependent components if necessary
    if (RemoveDependent(dependent))
    {
        // Pass potential restart exit codes back to the bundle based on executing the
        // workload related MSIs. The bundle may take additional actions such as prompting the user.
        exitCode = (int)Error.SUCCESS_REBOOT_REQUIRED;
    };
 
    // Step 4: Delete workload records
    DeleteWorkloadRecords(featureBandVersion, platform);
 
    // Step 5: Clean up install state file
    RemoveInstallStateFile(featureBandVersion, platform);
}
catch (Exception ex)
{
    Logger.Log($"Error: {ex}");
    exitCode = ex.HResult;
}
 
return exitCode;
 
static bool DetectSdk(SdkFeatureBand featureBandVersion, string platform)
{
    string registryPath = $@"SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\{platform}\sdk";
    using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(registryPath))
    {
        if (key is null)
        {
            Logger.Log("SDK registry path not found.");
            return false;
        }
 
        foreach (var valueName in key.GetValueNames())
        {
            try
            {
                // Convert the full SDK version into an SdkFeatureBand to see whether it matches the
                // SDK being removed.
                SdkFeatureBand installedFeatureBand = new SdkFeatureBand(valueName);
 
                if (installedFeatureBand.Equals(featureBandVersion))
                {
                    Logger.Log($"Another SDK with the same feature band is installed: {valueName} ({installedFeatureBand})");
                    return true;
                }
            }
            catch
            {
                Logger.Log($"Failed to check installed SDK version: {valueName}");
            }
        }
    }
    return false;
}
 
static bool RemoveDependent(string dependent)
{
    bool restartRequired = false;
 
    // Open the installer dependencies registry 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.
    using var hkInstallerDependenciesKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Classes\Installer\Dependencies", writable: true);
    if (hkInstallerDependenciesKey is null)
    {
        Logger.Log("Installer dependencies key does not exist.");
        return false;
    }
 
    // Iterate over each provider key in the dependencies
    foreach (string providerKeyName in hkInstallerDependenciesKey.GetSubKeyNames())
    {
        Logger.Log($"Processing provider key: {providerKeyName}");
 
        using var hkProviderKey = hkInstallerDependenciesKey.OpenSubKey(providerKeyName, writable: true);
        if (hkProviderKey is null)
        {
            continue;
        }
 
        // Open the Dependents subkey
        using var hkDependentsKey = hkProviderKey.OpenSubKey("Dependents", writable: true);
        if (hkDependentsKey is null)
        {
            continue;
        }
 
        // Check if the dependent exists and continue if it does not
        bool dependentExists = false;
        foreach (string dependentsKeyName in hkDependentsKey.GetSubKeyNames())
        {
            if (string.Equals(dependentsKeyName, dependent, StringComparison.OrdinalIgnoreCase))
            {
                dependentExists = true;
                break;
            }
        }
 
        if (!dependentExists)
        {
            continue;
        }
 
        Logger.Log($"Dependent match found: {dependent}");
 
        // Attempt to remove the dependent key
        try
        {
            hkDependentsKey.DeleteSubKey(dependent);
            Logger.Log("Dependent deleted");
        }
        catch (Exception ex)
        {
            Logger.Log($"Exception while removing dependent key: {ex.Message}");
            return false;
        }
 
        // Check if any dependents are left
        if (hkDependentsKey.SubKeyCount == 0)
        {
            // No remaining dependents, handle product uninstallation
            try
            {
                // Default value should be a REG_SZ containing the product code.
                string? productCode = hkProviderKey.GetValue(null) as string;
 
                if (productCode is null)
                {
                    Logger.Log($"No product ID found, provider key: {providerKeyName}");
                    continue;
                }
 
                // 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. This avoids reading additional
                // registry keys.
                uint error = WindowsInstaller.GetProductInfo(productCode, "ProductName", out string productName);
 
                if (error != Error.SUCCESS)
                {
                    Logger.Log($"Failed to detect product, ProductCode: {productCode}, result: 0x{error:x8}");
                    continue;
                }
 
                // Need to set the UI level before executing the MSI.
                _ = WindowsInstaller.SetInternalUI(InstallUILevel.None);
 
                // Configure the product to be absent (uninstall the product)
                error = WindowsInstaller.ConfigureProduct(productCode,
                    WindowsInstaller.INSTALLLEVEL_DEFAULT,
                    InstallState.ABSENT,
                    "MSIFASTINSTALL=7 IGNOREDEPENDENCIES=ALL REBOOT=ReallySuppress");
                Logger.Log($"Uninstall of {productName} ({productCode}) exited with 0x{error:x8}");
 
                if (error == Error.SUCCESS_REBOOT_INITIATED || error == Error.SUCCESS_REBOOT_REQUIRED)
                {
                    restartRequired = true;
                }
 
                // Remove the provider key. Typically these are removed by the engine, but since the workload
                // packs and manifest were installed by the CLI, the finalizer needs to clean these up.
                hkInstallerDependenciesKey.DeleteSubKeyTree(providerKeyName, throwOnMissingSubKey: false);
            }
            catch (Exception ex)
            {
                Logger.Log($"Failed to process dependentprocess: {ex.Message}");
                return restartRequired;
            }
        }
    }
 
    return restartRequired;
}
 
static void DeleteWorkloadRecords(SdkFeatureBand featureBandVersion, string platform)
{
    string? workloadKey = $@"SOFTWARE\Microsoft\dotnet\InstalledWorkloads\Standalone\{platform}";
 
    using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(workloadKey, writable: true))
    {
        if (key is not null)
        {
            key.DeleteSubKeyTree(featureBandVersion.ToString(), throwOnMissingSubKey: false);
            Logger.Log($"Deleted workload records for '{featureBandVersion}'.");
        }
        else
        {
            Logger.Log("No workload records found to delete.");
        }
    }
 
    DeleteEmptyKeyToRoot(Registry.LocalMachine, workloadKey);
}
 
static void DeleteEmptyKeyToRoot(RegistryKey key, string name)
{
    string? subKeyName = Path.GetFileName(name.TrimEnd(Path.DirectorySeparatorChar));
    string? tempName = name;
 
    while (!string.IsNullOrWhiteSpace(tempName))
    {
        using (RegistryKey? k = key.OpenSubKey(tempName))
        {
            if (k is not null && k.SubKeyCount == 0 && k.ValueCount == 0)
            {
                tempName = Path.GetDirectoryName(tempName);
 
                if (tempName is not null)
                {
                    try
                    {
                        using (RegistryKey? parentKey = key.OpenSubKey(tempName, writable: true))
                        {
                            if (parentKey is not null)
                            {
                                parentKey.DeleteSubKeyTree(subKeyName, throwOnMissingSubKey: false);
                                Logger.Log($"Deleted empty key: {subKeyName}");
                                subKeyName = Path.GetFileName(tempName.TrimEnd(Path.DirectorySeparatorChar));
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Log($"Failed to delete key: {tempName}, error: {ex.Message}");
                        break;
                    }
                }
            }
            else
            {
                break;
            }
        }
    }
}
 
static void RemoveInstallStateFile(SdkFeatureBand featureBandVersion, string platform)
{
    string programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
    string installStatePath = Path.Combine(programDataPath, "dotnet", "workloads", platform, featureBandVersion.ToString(), "installstate", "default.json");
 
    if (File.Exists(installStatePath))
    {
        File.Delete(installStatePath);
        Logger.Log($"Deleted install state file: {installStatePath}");
 
        var dir = new DirectoryInfo(installStatePath).Parent;
        while (dir is not null && dir.Exists && dir.GetFiles().Length == 0 && dir.GetDirectories().Length == 0)
        {
            dir.Delete();
            dir = dir.Parent;
        }
    }
    else
    {
        Logger.Log("Install state file does not exist.");
    }
}
 
static class Logger
{
    static StreamWriter? s_logStream;
 
    public static void Init(StreamWriter logStream) => s_logStream = logStream;
 
    public static void Log(string message)
    {
        var pid = Environment.ProcessId;
        var tid = Environment.CurrentManagedThreadId;
        s_logStream!.WriteLine($"[{pid:X4}:{tid:X4}][{DateTime.Now:yyyy-MM-ddTHH:mm:ss}] Finalizer: {message}");
    }
}