File: AssemblyDependency\AssemblyFoldersExResolver.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#if FEATURE_WIN32_REGISTRY
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;
using ProcessorArchitecture = System.Reflection.ProcessorArchitecture;
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Resolve searchpath type {Registry: *}
    /// </summary>
    internal class AssemblyFoldersExResolver : Resolver
    {
        /// <summary>
        /// Regex for breaking up the searchpath pieces.
        /// </summary>
        private static readonly Lazy<Regex> s_crackAssemblyFoldersExSentinel = new Lazy<Regex>(
            () => new Regex(
                AssemblyResolutionConstants.assemblyFoldersExSentinel +
                "(?<REGISTRYKEYROOT>[^,]*),(?<TARGETRUNTIMEVERSION>[^,]*),(?<REGISTRYKEYSUFFIX>[^,]*)([,]*)(?<CONDITIONS>.*)}",
                RegexOptions.IgnoreCase | RegexOptions.Compiled));
 
        /// <summary>
        /// Delegate.
        /// </summary>
        private readonly GetRegistrySubKeyNames _getRegistrySubKeyNames;
 
        /// <summary>
        /// Delegate
        /// </summary>
        private readonly GetRegistrySubKeyDefaultValue _getRegistrySubKeyDefaultValue;
 
        /// <summary>
        /// Open the base registry key given a hive and a view
        /// </summary>
        private readonly OpenBaseKey _openBaseKey;
 
        /// <summary>
        /// Whether or not the search path could be cracked.
        /// </summary>
        private bool _wasMatch;
 
        /// <summary>
        /// From the search path.
        /// </summary>
        private string _registryKeyRoot;
 
        /// <summary>
        /// From the search path.
        /// </summary>
        private string _targetRuntimeVersion;
 
        /// <summary>
        /// From the search path.
        /// </summary>
        private string _registryKeySuffix;
 
        /// <summary>
        /// From the search path.
        /// </summary>
        private string _osVersion;
 
        /// <summary>
        /// From the search path.
        /// </summary>
        private string _platform;
 
        /// <summary>
        /// Whether regex initialization has happened.
        /// </summary>
        private bool _isInitialized; // is initialized to false automatically
 
        /// <summary>
        /// List of assembly folders to search for keys in.
        /// </summary>
        private AssemblyFoldersExCache _assemblyFoldersCache;
 
        /// <summary>
        /// BuildEngine
        /// </summary>
        private readonly IBuildEngine4 _buildEngine;
 
        /// <summary>
        /// If it is not initialized then just return the null object, that would mean the resolver was not called.
        /// </summary>
        internal AssemblyFoldersEx AssemblyFoldersExLocations => _assemblyFoldersCache?.AssemblyFoldersEx;
 
        /// <summary>
        /// Construct.
        /// </summary>
        public AssemblyFoldersExResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetRegistrySubKeyNames getRegistrySubKeyNames, GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, GetAssemblyRuntimeVersion getRuntimeVersion, OpenBaseKey openBaseKey, Version targetedRuntimeVesion, ProcessorArchitecture targetProcessorArchitecture, bool compareProcessorArchitecture, IBuildEngine buildEngine)
            : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, targetProcessorArchitecture, compareProcessorArchitecture)
        {
            _buildEngine = buildEngine as IBuildEngine4;
            _getRegistrySubKeyNames = getRegistrySubKeyNames;
            _getRegistrySubKeyDefaultValue = getRegistrySubKeyDefaultValue;
            _openBaseKey = openBaseKey;
        }
 
        /// <summary>
        /// Initialize this class if it hasn't been initialized yet.
        /// </summary>
        private void LazyInitialize()
        {
            if (_isInitialized)
            {
                return;
            }
 
            _isInitialized = true;
 
            // Crack the search path just one time.
            Match match = s_crackAssemblyFoldersExSentinel.Value.Match(this.searchPathElement);
            _wasMatch = false;
 
            if (match.Success)
            {
                _registryKeyRoot = match.Groups["REGISTRYKEYROOT"].Value.Trim();
                _targetRuntimeVersion = match.Groups["TARGETRUNTIMEVERSION"].Value.Trim();
                _registryKeySuffix = match.Groups["REGISTRYKEYSUFFIX"].Value.Trim();
                _osVersion = null;
                _platform = null;
                Group conditions = match.Groups["CONDITIONS"];
 
                // Disregard if there are any empty values in the {Registry} tag.
                if (_registryKeyRoot.Length != 0 && _targetRuntimeVersion.Length != 0 && _registryKeySuffix.Length != 0)
                {
                    // Tolerate version keys that don't begin with "v" as these could come from user input
                    if (!_targetRuntimeVersion.StartsWith("v", StringComparison.OrdinalIgnoreCase))
                    {
                        _targetRuntimeVersion = _targetRuntimeVersion.Insert(0, "v");
                    }
 
                    if (conditions?.Value != null && conditions.Length > 0 && conditions.Value.Length > 0)
                    {
                        string value = conditions.Value.Trim();
 
                        // Parse the condition statement for OSVersion and Platform
                        foreach (string c in value.Split(MSBuildConstants.ColonChar))
                        {
                            if (String.Compare(c, 0, "OSVERSION=", 0, 10, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                _osVersion = c.Substring(10);
                            }
                            else if (String.Compare(c, 0, "PLATFORM=", 0, 9, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                _platform = c.Substring(9);
                            }
                        }
                    }
                    _wasMatch = true;
 
                    bool useCache = Environment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") == null;
                    string key = "ca22615d-aa83-444b-80b9-b32f3d5db097" + this.searchPathElement;
                    if (useCache && _buildEngine != null)
                    {
                        _assemblyFoldersCache = _buildEngine.GetRegisteredTaskObject(key, RegisteredTaskObjectLifetime.Build) as AssemblyFoldersExCache;
                    }
 
                    if (_assemblyFoldersCache == null)
                    {
                        AssemblyFoldersEx assemblyFolders = new AssemblyFoldersEx(_registryKeyRoot, _targetRuntimeVersion, _registryKeySuffix, _osVersion, _platform, _getRegistrySubKeyNames, _getRegistrySubKeyDefaultValue, this.targetProcessorArchitecture, _openBaseKey);
                        _assemblyFoldersCache = new AssemblyFoldersExCache(assemblyFolders, fileExists);
                        if (useCache)
                        {
                            _buildEngine?.RegisterTaskObject(key, _assemblyFoldersCache, RegisteredTaskObjectLifetime.Build, true /* dispose early ok*/);
                        }
                    }
 
                    fileExists = _assemblyFoldersCache.FileExists;
                }
            }
        }
 
        /// <inheritdoc/>
        public override bool Resolve(
            AssemblyNameExtension assemblyName,
            string sdkName,
            string rawFileNameCandidate,
            bool isPrimaryProjectReference,
            bool isImmutableFrameworkReference,
            bool wantSpecificVersion,
            string[] executableExtensions,
            string hintPath,
            string assemblyFolderKey,
            List<ResolutionSearchLocation> assembliesConsideredAndRejected,
            out string foundPath,
            out bool userRequestedSpecificFile)
        {
            foundPath = null;
            userRequestedSpecificFile = false;
 
            if (assemblyName != null)
            {
                LazyInitialize();
 
                if (_wasMatch)
                {
                    string resolvedPath = null;
                    if (_assemblyFoldersCache != null)
                    {
                        foreach (AssemblyFoldersExInfo assemblyFolder in _assemblyFoldersCache.AssemblyFoldersEx)
                        {
                            string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder.DirectoryPath, assembliesConsideredAndRejected);
 
                            // We have a full path returned
                            if (candidatePath != null)
                            {
                                if (resolvedPath == null)
                                {
                                    resolvedPath = candidatePath;
                                }
 
                                // We are not targeting MSIL thus we must have a match because ResolveFromDirectory only will return a match if we find an assembly matching the targeted processor architecture
                                if (targetProcessorArchitecture != ProcessorArchitecture.MSIL && targetProcessorArchitecture != ProcessorArchitecture.None)
                                {
                                    foundPath = candidatePath;
                                    return true;
                                }
                                else
                                {
                                    // Lets see if the processor architecture matches, note this this method will cache the result when it was first called.
                                    AssemblyNameExtension foundAssembly = getAssemblyName(candidatePath);
 
                                    // If the processor architecture does not match the we should continue to see if there is a better match.
                                    if (foundAssembly != null && (foundAssembly.AssemblyName.ProcessorArchitecture == ProcessorArchitecture.MSIL || foundAssembly.AssemblyName.ProcessorArchitecture == ProcessorArchitecture.None))
                                    {
                                        foundPath = candidatePath;
                                        return true;
                                    }
                                }
                            }
                        }
                    }
 
                    // If we get to this point and have not returned then we have the best assembly we could find, lets return it.
                    if (resolvedPath != null)
                    {
                        foundPath = resolvedPath;
                        return true;
                    }
                }
            }
 
            return false;
        }
    }
 
    /// <summary>
    /// Contains information about entries in the AssemblyFoldersEx registry keys.
    /// </summary>
    internal class AssemblyFoldersExCache
    {
        /// <summary>
        /// Set of files in ALL assemblyfoldersEx directories
        /// </summary>
        private readonly HashSet<string> _filesInDirectories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        /// <summary>
        /// File exists delegate we are replacing
        /// </summary>
        private readonly FileExists _fileExists;
 
        /// <summary>
        /// Should we use the original on or use our own
        /// </summary>
        private readonly bool _useOriginalFileExists;
 
        /// <summary>
        /// Constructor
        /// </summary>
        internal AssemblyFoldersExCache(AssemblyFoldersEx assemblyFoldersEx, FileExists fileExists)
        {
            AssemblyFoldersEx = assemblyFoldersEx;
            _fileExists = fileExists;
 
            if (Environment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") != null)
            {
                _useOriginalFileExists = true;
            }
            else
            {
                var lockobject = new Object();
 
                Parallel.ForEach(assemblyFoldersEx.UniqueDirectoryPaths, assemblyFolder =>
                {
                    if (FileUtilities.DirectoryExistsNoThrow(assemblyFolder))
                    {
                        string[] files = Directory.GetFiles(assemblyFolder, "*.*", SearchOption.TopDirectoryOnly);
 
                        lock (lockobject)
                        {
                            foreach (string file in files)
                            {
                                _filesInDirectories.Add(file);
                            }
                        }
                    }
                });
            }
        }
 
        /// <summary>
        /// AssemblyfoldersEx object which contains the set of directories in assmblyfoldersex
        /// </summary>
        internal AssemblyFoldersEx AssemblyFoldersEx { get; }
 
        /// <summary>
        ///  Fast file exists for assemblyfoldersex.
        /// </summary>
        internal bool FileExists(string path)
        {
            // Make sure that the file is in one of the directories under the assembly folders ex location
            // if it is not then we can not use this fast file existence check
            if (!_useOriginalFileExists)
            {
                bool exists = _filesInDirectories.Contains(path);
                return exists;
            }
            return _fileExists(path);
        }
    }
}
#endif