pschelpdesk/Vendor/Prise/AssemblyLoading/DefaultPluginDependencyContext.cs
2024-11-04 20:45:34 +01:00

415 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyModel;
using NuGet.Versioning;
using Prise.Platform;
using Prise.Utils;
namespace Prise.AssemblyLoading
{
public interface IPluginDependencyContextProvider
{
Task<IPluginDependencyContext> FromPluginLoadContext(IPluginLoadContext pluginLoadContext);
}
public class DefaultPluginDependencyContextProvider : IPluginDependencyContextProvider
{
private readonly IPlatformAbstraction platformAbstraction;
private readonly IRuntimePlatformContext runtimePlatformContext;
public DefaultPluginDependencyContextProvider(Func<IPlatformAbstraction> platformAbstractionFactory, Func<IRuntimePlatformContext> runtimePlatformContextFactory)
{
this.platformAbstraction = platformAbstractionFactory.ThrowIfNull(nameof(platformAbstractionFactory))();
this.runtimePlatformContext = runtimePlatformContextFactory.ThrowIfNull(nameof(runtimePlatformContextFactory))();
}
public Task<IPluginDependencyContext> FromPluginLoadContext(IPluginLoadContext pluginLoadContext)
{
var hostDependencies = new List<HostDependency>();
var remoteDependencies = new List<RemoteDependency>();
// var runtimePlatformContext = pluginLoadContext.RuntimePlatformContext.ThrowIfNull(nameof(pluginLoadContext.RuntimePlatformContext));
foreach (var type in pluginLoadContext.HostTypes)
// Load host types from current app domain
LoadAssemblyAndReferencesFromCurrentAppDomain(type.Assembly.GetName(), hostDependencies, pluginLoadContext.DowngradableHostTypes, pluginLoadContext.DowngradableHostAssemblies);
foreach (var assemblyFileName in pluginLoadContext.HostAssemblies)
// Load host types from current app domain
LoadAssemblyAndReferencesFromCurrentAppDomain(assemblyFileName, hostDependencies, pluginLoadContext.DowngradableHostTypes, pluginLoadContext.DowngradableHostAssemblies);
foreach (var type in pluginLoadContext.RemoteTypes)
remoteDependencies.Add(new RemoteDependency
{
DependencyName = type.Assembly.GetName()
});
var dependencyContext = GetDependencyContext(pluginLoadContext.FullPathToPluginAssembly);
var pluginFramework = dependencyContext.Target.Framework;
CheckFrameworkCompatibility(pluginLoadContext.HostFramework, pluginFramework, pluginLoadContext.IgnorePlatformInconsistencies);
var pluginDependencies = GetPluginDependencies(dependencyContext);
var resourceDependencies = GetResourceDependencies(dependencyContext);
var platformDependencies = GetPlatformDependencies(dependencyContext, this.runtimePlatformContext.GetPlatformExtensions());
var pluginDependencyContext = new DefaultPluginDependencyContext(
pluginLoadContext.FullPathToPluginAssembly,
hostDependencies,
remoteDependencies,
pluginDependencies,
resourceDependencies,
platformDependencies,
pluginLoadContext.AdditionalProbingPaths
);
Validate(pluginDependencyContext);
return Task.FromResult<IPluginDependencyContext>(pluginDependencyContext);
}
private static void Validate(DefaultPluginDependencyContext dependencyContext)
{
var hostDependenciesThatExistInPlugin = dependencyContext.HostDependencies
.Join(dependencyContext.PluginDependencies, h => h.DependencyName.Name, p => p.DependencyNameWithoutExtension, (h, p) => new
{
Host = h,
Plugin = p
});
foreach (var duplicateDependency in hostDependenciesThatExistInPlugin)
{
Debug.WriteLine($"Plugin dependency {duplicateDependency.Plugin.DependencyNameWithoutExtension} {duplicateDependency.Plugin.SemVer} exists in the host");
if (duplicateDependency.Host.SemVer > duplicateDependency.Plugin.SemVer)
Debug.WriteLine($"Host dependency {duplicateDependency.Host.DependencyName.Name} version {duplicateDependency.Host.SemVer} is newer than the Plugin {duplicateDependency.Plugin.SemVer}");
if (duplicateDependency.Host.SemVer < duplicateDependency.Plugin.SemVer)
Debug.WriteLine($"Plugin dependency {duplicateDependency.Plugin.DependencyNameWithoutExtension} version {duplicateDependency.Plugin.SemVer} is newer than the Host {duplicateDependency.Host.SemVer}");
}
}
private static void CheckFrameworkCompatibility(string hostFramework, string pluginFramework, bool ignorePlatformInconsistencies)
{
if (ignorePlatformInconsistencies)
return;
if (pluginFramework != hostFramework)
{
Debug.WriteLine($"Plugin framework {pluginFramework} does not match host framework {hostFramework}");
var pluginFrameworkType = pluginFramework.Split(new String[] { ",Version=v" }, StringSplitOptions.RemoveEmptyEntries)[0];
var hostFrameworkType = hostFramework.Split(new String[] { ",Version=v" }, StringSplitOptions.RemoveEmptyEntries)[0];
if (pluginFrameworkType.ToLower() == ".netstandard")
throw new AssemblyLoadingException($"Plugin framework {pluginFramework} might have compatibility issues with the host {hostFramework}, use the IgnorePlatformInconsistencies flag to skip this check.");
if (pluginFrameworkType != hostFrameworkType)
throw new AssemblyLoadingException($"Plugin framework {pluginFramework} does not match the host {hostFramework}. Please target {hostFramework} in order to load the plugin.");
var pluginFrameworkVersion = pluginFramework.Split(new String[] { ",Version=v" }, StringSplitOptions.RemoveEmptyEntries)[1];
var hostFrameworkVersion = hostFramework.Split(new String[] { ",Version=v" }, StringSplitOptions.RemoveEmptyEntries)[1];
var pluginFrameworkVersionMajor = int.Parse(pluginFrameworkVersion.Split(new String[] { "." }, StringSplitOptions.RemoveEmptyEntries)[0]);
var pluginFrameworkVersionMinor = int.Parse(pluginFrameworkVersion.Split(new String[] { "." }, StringSplitOptions.RemoveEmptyEntries)[1]);
var hostFrameworkVersionMajor = int.Parse(hostFrameworkVersion.Split(new String[] { "." }, StringSplitOptions.RemoveEmptyEntries)[0]);
var hostFrameworkVersionMinor = int.Parse(hostFrameworkVersion.Split(new String[] { "." }, StringSplitOptions.RemoveEmptyEntries)[1]);
if (pluginFrameworkVersionMajor > hostFrameworkVersionMajor || // If the major version of the plugin is higher
(pluginFrameworkVersionMajor == hostFrameworkVersionMajor && pluginFrameworkVersionMinor > hostFrameworkVersionMinor)) // Or the major version is the same but the minor version is higher
throw new AssemblyLoadingException($"Plugin framework version {pluginFramework} is newer than the Host {hostFramework}. Please upgrade the Host to load this Plugin.");
}
}
private static void LoadAssemblyAndReferencesFromCurrentAppDomain(AssemblyName assemblyName, List<HostDependency> hostDependencies, IEnumerable<Type> downgradableHostTypes, IEnumerable<string> downgradableAssemblies)
{
if (assemblyName?.Name == null || hostDependencies.Any(h => h.DependencyName.Name == assemblyName.Name))
return; // Break condition
hostDependencies.Add(new HostDependency
{
DependencyName = assemblyName,
AllowDowngrade =
downgradableHostTypes.Any(t => t.Assembly.GetName().Name == assemblyName.Name) ||
downgradableAssemblies.Any(a => a == assemblyName.Name)
});
try
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
foreach (var reference in assembly.GetReferencedAssemblies())
LoadAssemblyAndReferencesFromCurrentAppDomain(reference, hostDependencies, downgradableHostTypes, downgradableAssemblies);
}
catch (FileNotFoundException)
{
// This happens when the assembly is a platform assembly, log it
// logger.LoadReferenceFromAppDomainFailed(assemblyName);
}
}
private static void LoadAssemblyAndReferencesFromCurrentAppDomain(string assemblyFileName, List<HostDependency> hostDependencies, IEnumerable<Type> downgradableHostTypes, IEnumerable<string> downgradableAssemblies)
{
var assemblyName = new AssemblyName(assemblyFileName);
if (assemblyFileName == null || hostDependencies.Any(h => h.DependencyName.Name == assemblyName.Name))
return; // Break condition
hostDependencies.Add(new HostDependency
{
DependencyName = assemblyName,
AllowDowngrade =
downgradableHostTypes.Any(t => t.Assembly.GetName().Name == assemblyName.Name) ||
downgradableAssemblies.Any(a => a == assemblyName.Name)
});
try
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
foreach (var reference in assembly.GetReferencedAssemblies())
LoadAssemblyAndReferencesFromCurrentAppDomain(reference, hostDependencies, downgradableHostTypes, downgradableAssemblies);
}
catch (FileNotFoundException)
{
// This happens when the assembly is a platform assembly, log it
// logger.LoadReferenceFromAppDomainFailed(assemblyName);
}
}
#if SUPPORTS_NATIVE_PLATFORM_ABSTRACTIONS
private string GetCorrectRuntimeIdentifier()
{
var runtimeIdentifier = RuntimeInformation.RuntimeIdentifier;
if (this.platformAbstraction.IsOSX() || this.platformAbstraction.IsWindows())
return runtimeIdentifier;
// Other: Linux, FreeBSD, ...
return $"linux-{RuntimeInformation.ProcessArchitecture.ToString().ToLower()}";
}
#else
private string GetCorrectRuntimeIdentifier()
{
var runtimeIdentifier = RuntimeInformation.RuntimeIdentifier;
if (this.platformAbstraction.IsOSX() || this.platformAbstraction.IsWindows())
return runtimeIdentifier;
return $"{RuntimeInformation.OSDescription.ToString().ToLower()}-{RuntimeInformation.ProcessArchitecture}";
}
#endif
private IEnumerable<PluginDependency> GetPluginDependencies(DependencyContext pluginDependencyContext)
{
var dependencies = new List<PluginDependency>();
var runtimeId = GetCorrectRuntimeIdentifier();
var dependencyGraph = DependencyContext.Default.RuntimeGraph.FirstOrDefault(g => g.Runtime == runtimeId);
// List of supported runtimes, includes the default runtime and the fallbacks for this dependency context
var runtimes = new List<string> { dependencyGraph?.Runtime }.AddRangeToList<string>(dependencyGraph?.Fallbacks);
foreach (var runtimeLibrary in pluginDependencyContext.RuntimeLibraries)
{
var assets = runtimeLibrary.RuntimeAssemblyGroups.GetDefaultAssets();
foreach (var runtime in runtimes)
{
var runtimeSpecificGroup = runtimeLibrary.RuntimeAssemblyGroups.FirstOrDefault(g => g.Runtime == runtime);
if (runtimeSpecificGroup != null)
{
assets = runtimeSpecificGroup.AssetPaths;
break;
}
}
foreach (var asset in assets)
{
var path = asset.StartsWith("lib/")
? Path.GetFileName(asset)
: asset;
dependencies.Add(new PluginDependency
{
DependencyNameWithoutExtension = Path.GetFileNameWithoutExtension(asset),
SemVer = ParseSemVer(runtimeLibrary.Version),
DependencyPath = path,
ProbingPath = Path.Combine(runtimeLibrary.Name.ToLowerInvariant(), runtimeLibrary.Version, path)
});
}
}
return dependencies;
}
private SemanticVersion ParseSemVer(string version)
{
if (SemanticVersion.TryParse(version, out var semVer))
return semVer;
var versions = version.Split('.');
if (versions.Length > 3)
return new SemanticVersion(int.Parse(versions[0]), int.Parse(versions[1]), int.Parse(versions[2]), versions[3]);
return new SemanticVersion(int.Parse(versions[0]), int.Parse(versions[1]), int.Parse(versions[2]));
}
private static IEnumerable<PluginDependency> GetPluginReferenceDependencies(DependencyContext pluginDependencyContext)
{
var dependencies = new List<PluginDependency>();
foreach (var referenceAssembly in pluginDependencyContext.CompileLibraries.Where(r => r.Type == "referenceassembly"))
{
foreach (var assembly in referenceAssembly.Assemblies)
{
dependencies.Add(new PluginDependency
{
DependencyNameWithoutExtension = Path.GetFileNameWithoutExtension(assembly),
SemVer = SemanticVersion.Parse(referenceAssembly.Version),
DependencyPath = Path.Join("refs", assembly)
});
}
}
return dependencies;
}
private IEnumerable<PlatformDependency> GetPlatformDependencies(DependencyContext pluginDependencyContext, IEnumerable<string> platformExtensions)
{
var dependencies = new List<PlatformDependency>();
var runtimeId = GetCorrectRuntimeIdentifier();
var dependencyGraph = DependencyContext.Default.RuntimeGraph.FirstOrDefault(g => g.Runtime == runtimeId);
// List of supported runtimes, includes the default runtime and the fallbacks for this dependency context
var runtimes = new List<string> { dependencyGraph?.Runtime }.AddRangeToList<string>(dependencyGraph?.Fallbacks);
foreach (var runtimeLibrary in pluginDependencyContext.RuntimeLibraries)
{
var assets = runtimeLibrary.NativeLibraryGroups.GetDefaultAssets();
foreach (var runtime in runtimes)
{
var runtimeSpecificGroup = runtimeLibrary.NativeLibraryGroups.FirstOrDefault(g => g.Runtime == runtime);
if (runtimeSpecificGroup != null)
{
assets = runtimeSpecificGroup.AssetPaths;
break;
}
}
foreach (var asset in assets.Where(a => platformExtensions.Contains(Path.GetExtension(a)))) // Only load assemblies and not debug files
{
SemanticVersion semVer;
if (!SemanticVersion.TryParse(runtimeLibrary.Version, out semVer))
// Take first 3 digits
semVer = SemanticVersion.Parse(String.Join('.',runtimeLibrary.Version.Split(".").Take(3).ToArray()));
dependencies.Add(new PlatformDependency
{
DependencyNameWithoutExtension = Path.GetFileNameWithoutExtension(asset),
SemVer = semVer,
DependencyPath = asset
});
}
}
return dependencies;
}
private static IEnumerable<PluginResourceDependency> GetResourceDependencies(DependencyContext pluginDependencyContext)
{
var dependencies = new List<PluginResourceDependency>();
foreach (var runtimeLibrary in pluginDependencyContext.RuntimeLibraries
.Where(l => l.ResourceAssemblies != null && l.ResourceAssemblies.Any()))
{
dependencies.AddRange(runtimeLibrary.ResourceAssemblies
.Where(r => !String.IsNullOrEmpty(Path.GetDirectoryName(Path.GetDirectoryName(r.Path))))
.Select(r =>
new PluginResourceDependency
{
Path = Path.Combine(runtimeLibrary.Name.ToLowerInvariant(),
runtimeLibrary.Version,
r.Path)
}));
}
return dependencies;
}
private static DependencyContext GetDependencyContext(string fullPathToPluginAssembly)
{
var file = File.OpenRead(Path.Combine(Path.GetDirectoryName(fullPathToPluginAssembly), $"{Path.GetFileNameWithoutExtension(fullPathToPluginAssembly)}.deps.json"));
return new DependencyContextJsonReader().Read(file);
}
}
public class DefaultPluginDependencyContext : IPluginDependencyContext
{
public string FullPathToPluginAssembly { get; set; }
public IEnumerable<HostDependency> HostDependencies { get; set; }
public IEnumerable<RemoteDependency> RemoteDependencies { get; set; }
public IEnumerable<PluginDependency> PluginDependencies { get; set; }
public IEnumerable<PluginResourceDependency> PluginResourceDependencies { get; set; }
public IEnumerable<PlatformDependency> PlatformDependencies { get; set; }
public IEnumerable<string> AdditionalProbingPaths { get; set; }
internal DefaultPluginDependencyContext(string fullPathToPluginAssembly,
IEnumerable<HostDependency> hostDependencies,
IEnumerable<RemoteDependency> remoteDependencies,
IEnumerable<PluginDependency> pluginDependencies,
IEnumerable<PluginResourceDependency> pluginResourceDependencies,
IEnumerable<PlatformDependency> platformDependencies,
IEnumerable<string> additionalProbingPaths)
{
this.FullPathToPluginAssembly = fullPathToPluginAssembly.ThrowIfNull(nameof(fullPathToPluginAssembly));
this.HostDependencies = hostDependencies.ThrowIfNull(nameof(hostDependencies));
this.RemoteDependencies = remoteDependencies.ThrowIfNull(nameof(remoteDependencies));
this.PluginDependencies = pluginDependencies.ThrowIfNull(nameof(pluginDependencies));
this.PluginResourceDependencies = pluginResourceDependencies.ThrowIfNull(nameof(pluginResourceDependencies));
this.PlatformDependencies = platformDependencies.ThrowIfNull(nameof(platformDependencies));
this.AdditionalProbingPaths = additionalProbingPaths ?? Enumerable.Empty<string>();
}
public override string ToString()
{
var builder = new StringBuilder();
builder.AppendLine($"Dependency context for plugin: {this.FullPathToPluginAssembly}");
builder.AppendLine($"HostDependencies");
foreach (var p in this.HostDependencies)
builder.AppendLine($"{p.DependencyName.Name} {p.DependencyName.Version}");
builder.AppendLine($"");
builder.AppendLine($"RemoteDependencies");
foreach (var p in this.RemoteDependencies)
builder.AppendLine($"{p.DependencyName.Name} {p.DependencyName.Version}");
builder.AppendLine($"");
builder.AppendLine($"PlatformDependencies");
foreach (var p in this.PlatformDependencies)
builder.AppendLine($"{p.DependencyPath} {p.DependencyNameWithoutExtension} {p.SemVer}");
builder.AppendLine($"");
builder.AppendLine($"PluginDependencies");
foreach (var p in this.PluginDependencies)
builder.AppendLine($"{p.DependencyPath} {p.DependencyNameWithoutExtension} {p.SemVer}");
builder.AppendLine($"");
builder.AppendLine($"PluginResourceDependencies");
foreach (var p in this.PluginResourceDependencies)
builder.AppendLine($"{p.Path}");
return builder.ToString();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed && disposing)
{
this.FullPathToPluginAssembly = null;
this.HostDependencies = null;
this.RemoteDependencies = null;
this.PluginDependencies = null;
this.PluginResourceDependencies = null;
this.PlatformDependencies = null;
this.AdditionalProbingPaths = null;
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}