|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#if NET
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.PooledObjects;
namespace Microsoft.CodeAnalysis
{
internal sealed partial class AnalyzerAssemblyLoader
{
internal static IAnalyzerAssemblyResolver DiskAnalyzerAssemblyResolver => DiskResolver.Instance;
internal static IAnalyzerAssemblyResolver StreamAnalyzerAssemblyResolver => StreamResolver.Instance;
/// <summary>
/// Map of resolved directory paths to load contexts that manage their assemblies.
/// </summary>
private readonly Dictionary<string, DirectoryLoadContext> _loadContextByDirectory = new Dictionary<string, DirectoryLoadContext>(GeneratedPathComparer);
public IAnalyzerAssemblyResolver CompilerAnalyzerAssemblyResolver { get; }
public AssemblyLoadContext CompilerLoadContext { get; }
public ImmutableArray<IAnalyzerAssemblyResolver> AnalyzerAssemblyResolvers { get; }
internal AnalyzerAssemblyLoader()
: this(pathResolvers: [])
{
}
internal AnalyzerAssemblyLoader(ImmutableArray<IAnalyzerPathResolver> pathResolvers)
: this(pathResolvers, assemblyResolvers: [DiskAnalyzerAssemblyResolver], compilerLoadContext: null)
{
}
/// <summary>
/// Create a new <see cref="AnalyzerAssemblyLoader"/> with the given resolvers.
/// </summary>
/// <param name="compilerLoadContext">This is the <see cref="AssemblyLoadContext"/> where the compiler resides. This parameter
/// is primarily used for testing purposes but is also useful in hosted scenarios where the compiler may be loaded outside
/// the default context. When null this will be the <see cref="AssemblyLoadContext"/> the compiler currently resides
/// in </param>
/// <exception cref="ArgumentException"></exception>
internal AnalyzerAssemblyLoader(
ImmutableArray<IAnalyzerPathResolver> pathResolvers,
ImmutableArray<IAnalyzerAssemblyResolver> assemblyResolvers,
AssemblyLoadContext? compilerLoadContext)
{
if (assemblyResolvers.Length == 0)
{
throw new ArgumentException("Cannot be empty", nameof(assemblyResolvers));
}
CompilerLoadContext = compilerLoadContext ?? AssemblyLoadContext.GetLoadContext(typeof(SyntaxTree).GetTypeInfo().Assembly)!;
CompilerAnalyzerAssemblyResolver = new CompilerResolver(CompilerLoadContext);
AnalyzerPathResolvers = pathResolvers;
// The CompilerAnalyzerAssemblyResolver must be first here as the host is _always_ given a chance
// to resolve the assembly before any other resolver. This is crucial to allow for items like
// unification of System.Collections.Immutable or other core assemblies for a host.
AnalyzerAssemblyResolvers = [CompilerAnalyzerAssemblyResolver, .. assemblyResolvers];
}
public bool IsHostAssembly(Assembly assembly)
{
CheckIfDisposed();
var alc = AssemblyLoadContext.GetLoadContext(assembly);
return alc == CompilerLoadContext || alc == AssemblyLoadContext.Default;
}
private partial Assembly Load(AssemblyName assemblyName, string resolvedPath)
{
DirectoryLoadContext? loadContext;
var fullDirectoryPath = Path.GetDirectoryName(resolvedPath) ?? throw new ArgumentException(message: null, paramName: nameof(resolvedPath));
lock (_guard)
{
if (!_loadContextByDirectory.TryGetValue(fullDirectoryPath, out loadContext))
{
CodeAnalysisEventSource.Log.CreateAssemblyLoadContext(fullDirectoryPath);
loadContext = new DirectoryLoadContext(fullDirectoryPath, this);
_loadContextByDirectory[fullDirectoryPath] = loadContext;
}
}
return loadContext.LoadFromAssemblyName(assemblyName);
}
/// <summary>
/// Is this a registered analyzer file path that the loader knows about.
///
/// Note: this is using resolved paths, not the original file paths
/// </summary>
private bool IsRegisteredAnalyzerPath(string resolvedPath)
{
CheckIfDisposed();
lock (_guard)
{
return _resolvedToOriginalPathMap.ContainsKey(resolvedPath);
}
}
private string? GetAssemblyLoadPath(AssemblyName assemblyName, string directory)
{
// Prefer registered dependencies in the same directory first.
var simpleName = assemblyName.Name!;
var assemblyPath = Path.Combine(directory, simpleName + ".dll");
if (IsRegisteredAnalyzerPath(assemblyPath))
{
return assemblyPath;
}
// Next if this is a resource assembly for a known assembly then load it from the
// appropriate sub directory if it exists
//
// Note: when loading from disk the .NET runtime has a fallback step that will handle
// satellite assembly loading if the call to Load(satelliteAssemblyName) fails. This
// loader has a mode where it loads from Stream though and the runtime will not handle
// that automatically. Rather than bifurcate our loading behavior between Disk and
// Stream both modes just handle satellite loading directly
if (assemblyName.CultureInfo is not null && simpleName.EndsWith(".resources", SimpleNameComparer.Comparison))
{
var analyzerFileName = Path.ChangeExtension(simpleName, ".dll");
var analyzerFilePath = Path.Combine(directory, analyzerFileName);
return GetSatelliteLoadPath(analyzerFilePath, assemblyName.CultureInfo);
}
// Next prefer registered dependencies from other directories. Ideally this would not
// be necessary but msbuild target defaults have caused a number of customers to
// fall into this path. See discussion here for where it comes up
// https://github.com/dotnet/roslyn/issues/56442
var (_, bestResolvedPath) = GetBestResolvedPath(assemblyName);
if (bestResolvedPath is not null)
{
return bestResolvedPath;
}
// No analyzer registered this dependency. Time to fail
return null;
}
private partial bool IsMatch(AssemblyName requestedName, AssemblyName candidateName) =>
requestedName.Name == candidateName.Name;
internal DirectoryLoadContext[] GetDirectoryLoadContextsSnapshot()
{
CheckIfDisposed();
lock (_guard)
{
return _loadContextByDirectory.Values.OrderBy(v => v.Directory).ToArray();
}
}
private partial void DisposeWorker()
{
var contexts = ArrayBuilder<DirectoryLoadContext>.GetInstance();
lock (_guard)
{
foreach (var (_, context) in _loadContextByDirectory)
contexts.Add(context);
_loadContextByDirectory.Clear();
}
foreach (var context in contexts)
{
try
{
context.Unload();
CodeAnalysisEventSource.Log.DisposeAssemblyLoadContext(context.Directory);
}
catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical))
{
CodeAnalysisEventSource.Log.DisposeAssemblyLoadContextException(context.Directory, ex.ToString());
}
}
contexts.Free();
}
internal sealed class DirectoryLoadContext : AssemblyLoadContext
{
internal string Directory { get; }
private readonly AnalyzerAssemblyLoader _loader;
public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader)
: base(isCollectible: true)
{
Directory = directory;
_loader = loader;
}
protected override Assembly? Load(AssemblyName assemblyName)
{
foreach (var resolver in _loader.AnalyzerAssemblyResolvers)
{
var assembly = resolver.Resolve(_loader, assemblyName, this, Directory);
if (assembly is not null)
{
return assembly;
}
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var assemblyPath = Path.Combine(Directory, unmanagedDllName + ".dll");
if (_loader.IsRegisteredAnalyzerPath(assemblyPath))
{
return LoadUnmanagedDllFromPath(assemblyPath);
}
return IntPtr.Zero;
}
}
/// <summary>
/// A resolver which allows a passed in <see cref="AssemblyLoadContext"/> from the compiler
/// to control assembly resolution. This is important because there are many exchange types
/// that need to unify across the multiple analyzer ALCs. These include common types from
/// <c>Microsoft.CodeAnalysis.dll</c> etc, as well as platform assemblies provided by a
/// host such as visual studio.
/// </summary>
/// <remarks>
/// This resolver essentially forces any assembly that was loaded as a 'core' part of the
/// compiler to be shared across analyzers, and not loaded multiple times into each individual
/// analyzer ALC, even if the analyzer itself shipped a copy of said assembly.
/// </remarks>
/// <param name="compilerContext">The <see cref="AssemblyLoadContext"/> that the core
/// compiler assemblies are already loaded into.</param>
private sealed class CompilerResolver(AssemblyLoadContext compilerContext) : IAnalyzerAssemblyResolver
{
private readonly AssemblyLoadContext _compilerAlc = compilerContext;
public Assembly? Resolve(AnalyzerAssemblyLoader loader, AssemblyName assemblyName, AssemblyLoadContext directoryContext, string directory)
{
try
{
return _compilerAlc.LoadFromAssemblyName(assemblyName);
}
catch
{
// The LoadFromAssemblyName method will throw if the assembly cannot be found. Need
// to catch this exception and return null to satisfy the interface contract.
return null;
}
}
}
private sealed class DiskResolver : IAnalyzerAssemblyResolver
{
public static readonly IAnalyzerAssemblyResolver Instance = new DiskResolver();
public Assembly? Resolve(AnalyzerAssemblyLoader loader, AssemblyName assemblyName, AssemblyLoadContext directoryContext, string directory)
{
var assemblyPath = loader.GetAssemblyLoadPath(assemblyName, directory);
return assemblyPath is not null ? directoryContext.LoadFromAssemblyPath(assemblyPath) : null;
}
}
/// <summary>
/// This loads the assemblies from a <see cref="Stream"/> which is advantageous because it does
/// not lock the underlying assembly on disk.
/// </summary>
/// <remarks>
/// This should be avoided on Windows. Yes <see cref="DiskResolver"/> locks files on disks but it also
/// amortizes the cost of AV scanning the assemblies. When loading from <see cref="Stream"/>
/// the AV will scan the assembly every single time. That cost is significant and easily shows up in
/// performance profiles.
/// </remarks>
private sealed class StreamResolver : IAnalyzerAssemblyResolver
{
public static readonly IAnalyzerAssemblyResolver Instance = new StreamResolver();
public Assembly? Resolve(AnalyzerAssemblyLoader loader, AssemblyName assemblyName, AssemblyLoadContext directoryContext, string directory)
{
var assemblyPath = loader.GetAssemblyLoadPath(assemblyName, directory);
if (assemblyPath is null)
{
return null;
}
using var stream = File.Open(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return directoryContext.LoadFromStream(stream);
}
}
}
}
#endif
|