using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Kyoo.Controllers { /// /// An implementation of . /// This is used to load plugins and retrieve information from them. /// public class PluginManager : IPluginManager { /// /// The service provider. It allow plugin's activation. /// private readonly IServiceProvider _provider; /// /// The configuration to get the plugin's directory. /// private readonly IConfiguration _config; /// /// The logger used by this class. /// private readonly ILogger _logger; /// /// The list of plugins that are currently loaded. /// private readonly List _plugins = new(); /// /// Create a new instance. /// /// A service container to allow initialization of plugins /// The configuration instance, to get the plugin's directory path. /// The logger used by this class. public PluginManager(IServiceProvider provider, IConfiguration config, ILogger logger) { _provider = provider; _config = config; _logger = logger; } /// public T GetPlugin(string name) { return (T)_plugins?.FirstOrDefault(x => x.Name == name && x is T); } /// public ICollection GetPlugins() { return _plugins?.OfType().ToArray(); } /// public ICollection GetAllPlugins() { return _plugins; } /// /// Load a single plugin and return all IPlugin implementations contained in the Assembly. /// /// The path of the dll /// The list of dlls in hte assembly private IPlugin[] LoadPlugin(string path) { path = Path.GetFullPath(path); try { PluginDependencyLoader loader = new(path); Assembly assembly = loader.LoadFromAssemblyPath(path); return assembly.GetTypes() .Where(x => typeof(IPlugin).IsAssignableFrom(x)) .Where(x => _plugins.All(y => y.GetType() != x)) .Select(x => (IPlugin)ActivatorUtilities.CreateInstance(_provider, x)) .ToArray(); } catch (Exception ex) { _logger.LogError(ex, "Could not load the plugin at {Path}", path); return Array.Empty(); } } /// public void LoadPlugins(ICollection plugins) { string pluginFolder = _config.GetValue("plugins"); if (!Directory.Exists(pluginFolder)) Directory.CreateDirectory(pluginFolder); _logger.LogTrace("Loading new plugins..."); string[] pluginsPaths = Directory.GetFiles(pluginFolder, "*.dll", SearchOption.AllDirectories); plugins = plugins.Concat(pluginsPaths.SelectMany(LoadPlugin)) .GroupBy(x => x.Name) .Select(x => x.First()) .ToList(); ICollection available = GetProvidedTypes(plugins); _plugins.AddRange(plugins.Where(plugin => { Type missing = plugin.Requires.FirstOrDefault(x => available.All(y => !y.IsAssignableTo(x))); if (missing == null) return true; _logger.LogCritical("No {Dependency} available in Kyoo but the plugin {Plugin} requires it", missing.Name, plugin.Name); return false; })); if (!_plugins.Any()) _logger.LogInformation("No plugin enabled"); else _logger.LogInformation("Plugin enabled: {Plugins}", _plugins.Select(x => x.Name)); } /// public void ConfigureServices(IServiceCollection services) { ICollection available = GetProvidedTypes(_plugins); foreach (IPlugin plugin in _plugins) plugin.Configure(services, available); } /// public void ConfigureAspnet(IApplicationBuilder app) { foreach (IPlugin plugin in _plugins) plugin.ConfigureAspNet(app); } /// /// Get the list of types provided by the currently loaded plugins. /// /// The list of plugins that will be used as a plugin pool to get provided types. /// The list of types available. private ICollection GetProvidedTypes(ICollection plugins) { List available = plugins.SelectMany(x => x.Provides).ToList(); List conditionals = plugins .SelectMany(x => x.ConditionalProvides) .Where(x => x.Condition.Condition()) .ToList(); bool IsAvailable(ConditionalProvide conditional, bool log = false) { if (!conditional.Condition.Condition()) return false; ICollection needed = conditional.Condition.Needed .Where(y => !available.Contains(y)) .ToList(); // TODO handle circular dependencies, actually it might stack overflow. needed = needed.Where(x => !conditionals .Where(y => y.Type == x) .Any(y => IsAvailable(y))) .ToList(); if (!needed.Any()) return true; if (log && available.All(x => x != conditional.Type)) { _logger.LogWarning("The type {Type} is not available, {Dependencies} could not be met", conditional.Type.Name, needed.Select(x => x.Name)); } return false; } // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator foreach (ConditionalProvide conditional in conditionals) { if (IsAvailable(conditional, true)) available.Add(conditional.Type); } return available; } /// /// A custom to load plugin's dependency if they are on the same folder. /// private class PluginDependencyLoader : AssemblyLoadContext { /// /// The basic resolver that will be used to load dlls. /// private readonly AssemblyDependencyResolver _resolver; /// /// Create a new for the given path. /// /// The path of the plugin and it's dependencies public PluginDependencyLoader(string pluginPath) { _resolver = new AssemblyDependencyResolver(pluginPath); } /// protected override Assembly Load(AssemblyName assemblyName) { Assembly existing = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(x => { AssemblyName name = x.GetName(); return name.Name == assemblyName.Name && name.Version == assemblyName.Version; }); if (existing != null) return existing; // TODO load the assembly from the common folder if the file exists (this would allow shared libraries) string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) return LoadFromAssemblyPath(assemblyPath); return base.Load(assemblyName); } /// protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); if (libraryPath != null) return LoadUnmanagedDllFromPath(libraryPath); return base.LoadUnmanagedDll(unmanagedDllName); } } } }