|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if NET
using System.Runtime.Versioning;
#endif
using Microsoft.Win32;
namespace Microsoft.DotNet.Cli.Utils;
/// <summary>
/// <para>
/// Represents an installer dependency provider used to manage reference counts against an installer (MSI).
/// A dependency provider is an artificial construct introduced in WiX v3.6 to support reference counting installation
/// packages. MSIs that support this include a custom table, WixDependencyProvider, and a registry entry that writes the
/// provider key to SOFTWARE\Classes\Installer\Dependencies\{provider key name} under HKLM or HKCU.
/// </para>
/// <para>
/// Installers like chainers register a reference by writing a value under the Dependents subkey of the provider key. When the
/// MSI is removed, a custom action is executed to determine if there are any remaining dependents and block the uninstall.
/// The check can be bypassed by setting IGNOREDEPENDENCIES=ALL. When a chainer removes the MSI, it first removes its
/// dependent entry. If there are no other dependents, it can proceed to remove the MSI, otherwise it should do nothing.
/// </para>
/// </summary>
#if NET
[SupportedOSPlatform("windows")]
#endif
public sealed class DependencyProvider
{
/// <summary>
/// The key name used by Visual Studio 2015 and later to register a dependency.
/// </summary>
internal const string VisualStudioDependentKeyName = "VS.{AEF703B8-D2CC-4343-915C-F54A30B90937}";
/// <summary>
/// The relative path from the <see cref="BaseKey"/> to the Dependencies key.
/// </summary>
internal const string DependenciesKeyRelativePath = @"SOFTWARE\Classes\Installer\Dependencies";
/// <summary>
/// <see langword="true"/> if the dependency provider is associated with a per-machine
/// installation; <see langword="false"/> otherwise.
/// </summary>
public readonly bool AllUsers;
/// <summary>
/// Returns the root key to use: <see cref="Registry.LocalMachine"/> for per-machine installations or
/// <see cref="Registry.CurrentUser"/> for per-user installations.
/// </summary>
public readonly RegistryKey BaseKey;
/// <summary>
/// Gets all dependents associated with the provider key. The property always enumerates the
/// provider's dependent entries in the registry.
/// </summary>
public IEnumerable<string> Dependents => GetDependents();
/// <summary>
/// The path of the key where the provider's dependents are stored, relative to the <see cref="BaseKey"/>.
/// </summary>
public readonly string DependentsKeyPath;
/// <summary>
/// <see langword="true"/> if Visual Studio 2015 or later is registered as a dependent. Visual Studio only
/// writes a single entry, regardless of how many instances have taken dependencies.
/// </summary>
public bool HasVisualStudioDependency => Dependents.Contains(VisualStudioDependentKeyName);
/// <summary>
/// The name of the dependency provider key used for tracking reference counts.
/// </summary>
public readonly string ProviderKeyName;
/// <summary>
/// The product code of the MSI associated with the dependency provider.
/// </summary>
public string? ProductCode => GetProductCode();
/// <summary>
/// The path of the provider key, relative to the <see cref="BaseKey"/>.
/// </summary>
public readonly string ProviderKeyPath;
/// <summary>
/// Creates a new <see cref="DependencyProvider"/> instance.
/// </summary>
/// <param name="providerKeyName">The name of the dependency provider key.</param>
/// <param name="allUsers"><see langword="true" /> if the provider belongs to a per-machine installation;
/// <see langword="false"/> otherwise.</param>
public DependencyProvider(string providerKeyName, bool allUsers = true)
{
if (providerKeyName is null)
{
throw new ArgumentNullException(nameof(providerKeyName));
}
if (string.IsNullOrWhiteSpace(providerKeyName))
{
throw new ArgumentException($"{nameof(providerKeyName)} cannot be empty.");
}
ProviderKeyName = providerKeyName;
AllUsers = allUsers;
BaseKey = AllUsers ? Registry.LocalMachine : Registry.CurrentUser;
ProviderKeyPath = $@"{DependenciesKeyRelativePath}\{ProviderKeyName}";
DependentsKeyPath = $@"{ProviderKeyPath}\Dependents";
}
/// <summary>
/// Adds the specified dependent to the provider key. The dependent is stored as a subkey under the Dependents key of
/// the provider."/>
/// </summary>
/// <param name="dependent">The dependent to add.</param>
public void AddDependent(string dependent)
{
if (dependent is null)
{
throw new ArgumentNullException(nameof(dependent));
}
if (string.IsNullOrWhiteSpace(dependent))
{
throw new ArgumentException($"{nameof(dependent)} cannot be empty.");
}
using RegistryKey dependentsKey = BaseKey.CreateSubKey(Path.Combine(DependentsKeyPath, dependent), writable: true);
}
/// <summary>
/// Remove the specified dependent from the provider key. Optionally, if this is the final dependent,
/// the provider key can also be removed. This is typically done during an uninstall.
/// </summary>
/// <param name="dependent">The dependent to remove.</param>
/// <param name="removeProvider">When <see langword="true"/>, delete the provider key if the dependent being
/// removed is the last dependent.</param>
public void RemoveDependent(string dependent, bool removeProvider)
{
if (dependent is null)
{
throw new ArgumentNullException(nameof(dependent));
}
if (string.IsNullOrWhiteSpace(dependent))
{
throw new ArgumentException($"{nameof(dependent)} cannot be empty.");
}
using RegistryKey? dependentsKey = BaseKey.OpenSubKey(DependentsKeyPath, writable: true);
dependentsKey?.DeleteSubKeyTree(dependent);
if ((removeProvider) && (Dependents.Count() == 0))
{
using RegistryKey? providerKey = BaseKey.OpenSubKey(DependenciesKeyRelativePath, writable: true);
providerKey?.DeleteSubKeyTree(ProviderKeyName);
}
}
/// <summary>
/// Gets all the dependents associated with the provider key.
/// </summary>
/// <returns>All dependents of the provider key.</returns>
private IEnumerable<string> GetDependents()
{
using RegistryKey? dependentsKey = BaseKey.OpenSubKey(DependentsKeyPath);
return dependentsKey?.GetSubKeyNames() ?? Enumerable.Empty<string>();
}
/// <summary>
/// Gets the ProductCode associated with this dependency provider. The ProductCode is stored in the default
/// value.
/// </summary>
/// <returns>The ProductCode associated with this dependency provider or <see langword="null"/> if it does not exist.</returns>
private string? GetProductCode()
{
using RegistryKey? providerKey = BaseKey.OpenSubKey(ProviderKeyPath);
return providerKey?.GetValue(null) as string ?? null;
}
public override string ToString() => ProviderKeyName;
public static DependencyProvider? GetFromProductCode(string productCode, bool allUsers = true)
{
var baseKey = allUsers ? Registry.LocalMachine : Registry.CurrentUser;
using RegistryKey? dependenciesKey = baseKey.OpenSubKey(DependenciesKeyRelativePath);
foreach (var providerKeyName in dependenciesKey?.GetSubKeyNames() ?? [])
{
using RegistryKey? providerKey = dependenciesKey?.OpenSubKey(providerKeyName);
var thisProductCode = providerKey?.GetValue(null) as string ?? null;
if (string.Equals(thisProductCode, productCode, StringComparison.OrdinalIgnoreCase))
{
return new DependencyProvider(providerKeyName, allUsers);
}
}
return null;
}
}
|