415 lines
22 KiB
C#
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);
|
|
}
|
|
}
|
|
} |