Skip to content

Commit

Permalink
Merge pull request #1483 from nunit/issue-1466
Browse files Browse the repository at this point in the history
Reorganize assembly resolution; fix error loading WindowsBase.dll
  • Loading branch information
CharliePoole authored Sep 17, 2024
2 parents d22ffe9 + fac8914 commit 03c865e
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ internal sealed class TestAssemblyLoadContext : AssemblyLoadContext
{
private static readonly Logger log = InternalTrace.GetLogger(typeof(TestAssemblyLoadContext));

private readonly string _testAssemblyPath;
private readonly string _basePath;
private readonly TestAssemblyResolver _resolver;
private readonly System.Runtime.Loader.AssemblyDependencyResolver _runtimeResolver;

public TestAssemblyLoadContext(string testAssemblyPath)
{
_testAssemblyPath = testAssemblyPath;
_resolver = new TestAssemblyResolver(this, testAssemblyPath);
_basePath = Path.GetDirectoryName(testAssemblyPath);
_runtimeResolver = new AssemblyDependencyResolver(testAssemblyPath);
Expand Down
256 changes: 169 additions & 87 deletions src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,171 +2,251 @@

#if NETCOREAPP3_1_OR_GREATER

using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.DependencyModel.Resolution;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.DependencyModel.Resolution;
using Microsoft.Win32;
using TestCentric.Metadata;

namespace NUnit.Engine.Internal
{
internal sealed class TestAssemblyResolver : IDisposable
{
private static readonly Logger log = InternalTrace.GetLogger(typeof(TestAssemblyResolver));

private readonly ICompilationAssemblyResolver _assemblyResolver;
private readonly DependencyContext _dependencyContext;
private readonly AssemblyLoadContext _loadContext;

private static readonly string INSTALL_DIR;
private static readonly string WINDOWS_DESKTOP_DIR;
private static readonly string ASP_NET_CORE_DIR;
private static readonly List<string> AdditionalFrameworkDirectories;

// Our Strategies for resolving references
List<ResolutionStrategy> ResolutionStrategies;

static TestAssemblyResolver()
{
INSTALL_DIR = GetDotNetInstallDirectory();
WINDOWS_DESKTOP_DIR = Path.Combine(INSTALL_DIR, "shared", "Microsoft.WindowsDesktop.App");
ASP_NET_CORE_DIR = Path.Combine(INSTALL_DIR, "shared", "Microsoft.AspNetCore.App");

AdditionalFrameworkDirectories = new List<string>();
if (Directory.Exists(WINDOWS_DESKTOP_DIR))
AdditionalFrameworkDirectories.Add(WINDOWS_DESKTOP_DIR);
if (Directory.Exists(ASP_NET_CORE_DIR))
AdditionalFrameworkDirectories.Add(ASP_NET_CORE_DIR);
}

public TestAssemblyResolver(AssemblyLoadContext loadContext, string assemblyPath)
public TestAssemblyResolver(AssemblyLoadContext loadContext, string testAssemblyPath)
{
_loadContext = loadContext;
_dependencyContext = DependencyContext.Load(loadContext.LoadFromAssemblyPath(assemblyPath));

_assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[]
{
new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(assemblyPath)),
new ReferenceAssemblyPathResolver(),
new PackageCompilationAssemblyResolver()
});
InitializeResolutionStrategies(loadContext, testAssemblyPath);

_loadContext.Resolving += OnResolving;
}

private void InitializeResolutionStrategies(AssemblyLoadContext loadContext, string testAssemblyPath)
{
// First, looking only at direct references by the test assembly, try to determine if
// this assembly is using WindowsDesktop (either SWF or WPF) and/or AspNetCore.
AssemblyDefinition assemblyDef = AssemblyDefinition.ReadAssembly(testAssemblyPath);
bool isWindowsDesktop = false;
bool isAspNetCore = false;
foreach (var reference in assemblyDef.MainModule.GetTypeReferences())
{
string fn = reference.FullName;
if (fn.StartsWith("System.Windows.") || fn.StartsWith("PresentationFramework"))
isWindowsDesktop = true;
if (fn.StartsWith("Microsoft.AspNetCore."))
isAspNetCore = true;
}

// Initialize the list of ResolutionStrategies in the best order depending on
// what we learned.
ResolutionStrategies = new List<ResolutionStrategy>();

if (isWindowsDesktop && Directory.Exists(WINDOWS_DESKTOP_DIR))
ResolutionStrategies.Add(new AdditionalDirectoryStrategy(WINDOWS_DESKTOP_DIR));
if (isAspNetCore && Directory.Exists(ASP_NET_CORE_DIR))
ResolutionStrategies.Add(new AdditionalDirectoryStrategy(ASP_NET_CORE_DIR));
ResolutionStrategies.Add(new TrustedPlatformAssembliesStrategy());
ResolutionStrategies.Add(new RuntimeLibrariesStrategy(loadContext, testAssemblyPath));
if (!isWindowsDesktop && Directory.Exists(WINDOWS_DESKTOP_DIR))
ResolutionStrategies.Add(new AdditionalDirectoryStrategy(WINDOWS_DESKTOP_DIR));
if (!isAspNetCore && Directory.Exists(ASP_NET_CORE_DIR))
ResolutionStrategies.Add(new AdditionalDirectoryStrategy(ASP_NET_CORE_DIR));
}

public void Dispose()
{
_loadContext.Resolving -= OnResolving;
}

public Assembly Resolve(AssemblyLoadContext context, AssemblyName name)
public Assembly Resolve(AssemblyLoadContext context, AssemblyName assemblyName)
{
return OnResolving(context, assemblyName);
}

private Assembly OnResolving(AssemblyLoadContext loadContext, AssemblyName assemblyName)
{
return OnResolving(context, name);
if (loadContext == null) throw new ArgumentNullException("context");

Assembly loadedAssembly;
foreach (var strategy in ResolutionStrategies)
if (strategy.TryToResolve(loadContext, assemblyName, out loadedAssembly))
return loadedAssembly;

log.Info("Cannot resolve assembly '{0}'", assemblyName);
return null;
}

private Assembly OnResolving(AssemblyLoadContext context, AssemblyName name)
#region Nested ResolutionStrategy Classes

public abstract class ResolutionStrategy
{
context = context ?? _loadContext;
public abstract bool TryToResolve(
AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly);
}

if (TryLoadFromTrustedPlatformAssemblies(context, name, out var loadedAssembly))
public class TrustedPlatformAssembliesStrategy : ResolutionStrategy
{
public override bool TryToResolve(
AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
{
log.Info("'{0}' assembly is loaded from trusted path '{1}'", name, loadedAssembly.Location);
return loadedAssembly;
return TryLoadFromTrustedPlatformAssemblies(loadContext, assemblyName, out loadedAssembly);
}

foreach (var library in _dependencyContext.RuntimeLibraries)
private static bool TryLoadFromTrustedPlatformAssemblies(
AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
{
var wrapper = new CompilationLibrary(
library.Type,
library.Name,
library.Version,
library.Hash,
library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths),
library.Dependencies,
library.Serviceable);

var assemblies = new List<string>();
_assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblies);

foreach (var assemblyPath in assemblies)
// https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing
loadedAssembly = null;
var trustedAssemblies = System.AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string;
if (string.IsNullOrEmpty(trustedAssemblies))
{
if (name.Name == Path.GetFileNameWithoutExtension(assemblyPath))
return false;
}

var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":";
foreach (var assemblyPath in trustedAssemblies.Split(separator))
{
var fileName = Path.GetFileNameWithoutExtension(assemblyPath);
if (FileMatchesAssembly(fileName) && File.Exists(assemblyPath))
{
loadedAssembly = context.LoadFromAssemblyPath(assemblyPath);
log.Info("'{0}' ({1}) assembly is loaded from runtime libraries {2} dependencies",
name,
loadedAssembly.Location,
library.Name);
loadedAssembly = loadContext.LoadFromAssemblyPath(assemblyPath);
log.Info("'{0}' assembly is loaded from trusted path '{1}'", assemblyPath, loadedAssembly.Location);

return true;
}
}

return false;

bool FileMatchesAssembly(string fileName) =>
string.Equals(fileName, assemblyName.Name, StringComparison.InvariantCultureIgnoreCase);
}
}

public class RuntimeLibrariesStrategy : ResolutionStrategy
{
private DependencyContext _dependencyContext;
private readonly ICompilationAssemblyResolver _assemblyResolver;

public RuntimeLibrariesStrategy(AssemblyLoadContext loadContext, string testAssemblyPath)
{
_dependencyContext = DependencyContext.Load(loadContext.LoadFromAssemblyPath(testAssemblyPath));

_assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[]
{
new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(testAssemblyPath)),
new ReferenceAssemblyPathResolver(),
new PackageCompilationAssemblyResolver()
});
}

return loadedAssembly;
public override bool TryToResolve(
AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
{
foreach (var library in _dependencyContext.RuntimeLibraries)
{
var wrapper = new CompilationLibrary(
library.Type,
library.Name,
library.Version,
library.Hash,
library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths),
library.Dependencies,
library.Serviceable);

var assemblies = new List<string>();
_assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblies);

foreach (var assemblyPath in assemblies)
{
if (assemblyName.Name == Path.GetFileNameWithoutExtension(assemblyPath))
{
loadedAssembly = loadContext.LoadFromAssemblyPath(assemblyPath);
log.Info("'{0}' ({1}) assembly is loaded from runtime libraries {2} dependencies",
assemblyName,
loadedAssembly.Location,
library.Name);

return true;
}
}
}

loadedAssembly = null;
return false;
}
}

public class AdditionalDirectoryStrategy : ResolutionStrategy
{
private string _frameworkDirectory;

if (name.Version == null)
public AdditionalDirectoryStrategy(string frameworkDirectory)
{
return null;
_frameworkDirectory = frameworkDirectory;
}

foreach (string frameworkDirectory in AdditionalFrameworkDirectories)
public override bool TryToResolve(
AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly)
{
var versionDir = FindBestVersionDir(frameworkDirectory, name.Version);
loadedAssembly = null;
if (assemblyName.Version == null)
return false;

var versionDir = FindBestVersionDir(_frameworkDirectory, assemblyName.Version);

if (versionDir != null)
{
string candidate = Path.Combine(frameworkDirectory, versionDir, name.Name + ".dll");
string candidate = Path.Combine(_frameworkDirectory, versionDir, assemblyName.Name + ".dll");
if (File.Exists(candidate))
{
loadedAssembly = context.LoadFromAssemblyPath(candidate);
loadedAssembly = loadContext.LoadFromAssemblyPath(candidate);
log.Info("'{0}' ({1}) assembly is loaded from AdditionalFrameworkDirectory {2} dependencies with best candidate version {3}",
name,
assemblyName,
loadedAssembly.Location,
frameworkDirectory,
_frameworkDirectory,
versionDir);

return loadedAssembly;
return true;
}
else
{
log.Debug("Best version dir for {0} is {1}, but there is no {2} file", frameworkDirectory, versionDir, candidate);
log.Debug("Best version dir for {0} is {1}, but there is no {2} file", _frameworkDirectory, versionDir, candidate);
return false;
}
}
}

log.Info("Cannot resolve assembly '{0}'", name);
return null;
}

private static bool TryLoadFromTrustedPlatformAssemblies(AssemblyLoadContext context, AssemblyName assemblyName, out Assembly loadedAssembly)
{
// https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing
loadedAssembly = null;
var trustedAssemblies = System.AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string;
if (string.IsNullOrEmpty(trustedAssemblies))
{
return false;
}
}

var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":";
foreach (var assemblyPath in trustedAssemblies.Split(separator))
{
var fileName = Path.GetFileNameWithoutExtension(assemblyPath);
if (string.Equals(fileName, assemblyName.Name, StringComparison.InvariantCultureIgnoreCase) == false)
{
continue;
}

if (File.Exists(assemblyPath))
{
loadedAssembly = context.LoadFromAssemblyPath(assemblyPath);
return true;
}
}
#endregion

return false;
}
#region HelperMethods

private static string GetDotNetInstallDirectory()
{
Expand Down Expand Up @@ -232,6 +312,8 @@ private static bool TryGetVersionFromString(string text, out Version newVersion)
return false;
}
}

#endregion
}
}
#endif
Loading

0 comments on commit 03c865e

Please sign in to comment.