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 FromPluginLoadContext(IPluginLoadContext pluginLoadContext); } public class DefaultPluginDependencyContextProvider : IPluginDependencyContextProvider { private readonly IPlatformAbstraction platformAbstraction; private readonly IRuntimePlatformContext runtimePlatformContext; public DefaultPluginDependencyContextProvider(Func platformAbstractionFactory, Func runtimePlatformContextFactory) { this.platformAbstraction = platformAbstractionFactory.ThrowIfNull(nameof(platformAbstractionFactory))(); this.runtimePlatformContext = runtimePlatformContextFactory.ThrowIfNull(nameof(runtimePlatformContextFactory))(); } public Task FromPluginLoadContext(IPluginLoadContext pluginLoadContext) { var hostDependencies = new List(); var remoteDependencies = new List(); // 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(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 hostDependencies, IEnumerable downgradableHostTypes, IEnumerable 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 hostDependencies, IEnumerable downgradableHostTypes, IEnumerable 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 GetPluginDependencies(DependencyContext pluginDependencyContext) { var dependencies = new List(); 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 { dependencyGraph?.Runtime }.AddRangeToList(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 GetPluginReferenceDependencies(DependencyContext pluginDependencyContext) { var dependencies = new List(); 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 GetPlatformDependencies(DependencyContext pluginDependencyContext, IEnumerable platformExtensions) { var dependencies = new List(); 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 { dependencyGraph?.Runtime }.AddRangeToList(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 GetResourceDependencies(DependencyContext pluginDependencyContext) { var dependencies = new List(); 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 HostDependencies { get; set; } public IEnumerable RemoteDependencies { get; set; } public IEnumerable PluginDependencies { get; set; } public IEnumerable PluginResourceDependencies { get; set; } public IEnumerable PlatformDependencies { get; set; } public IEnumerable AdditionalProbingPaths { get; set; } internal DefaultPluginDependencyContext(string fullPathToPluginAssembly, IEnumerable hostDependencies, IEnumerable remoteDependencies, IEnumerable pluginDependencies, IEnumerable pluginResourceDependencies, IEnumerable platformDependencies, IEnumerable 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(); } 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); } } }