using StellaOps.Plugin.Hosting; using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading; using System.Linq; using System.Threading.Tasks; namespace StellaOps.Plugin; public interface IAvailabilityPlugin { string Name { get; } bool IsAvailable(IServiceProvider services); } public interface IFeedConnector { string SourceName { get; } Task FetchAsync(IServiceProvider services, CancellationToken cancellationToken); Task ParseAsync(IServiceProvider services, CancellationToken cancellationToken); Task MapAsync(IServiceProvider services, CancellationToken cancellationToken); } public interface IFeedExporter { string Name { get; } Task ExportAsync(IServiceProvider services, CancellationToken cancellationToken); } public interface IConnectorPlugin : IAvailabilityPlugin { IFeedConnector Create(IServiceProvider services); } public interface IExporterPlugin : IAvailabilityPlugin { IFeedExporter Create(IServiceProvider services); } public sealed class PluginCatalog { private readonly List _assemblies = new(); private readonly HashSet _assemblyLocations = new(StringComparer.OrdinalIgnoreCase); public PluginCatalog AddAssembly(Assembly assembly) { if (assembly == null) throw new ArgumentNullException(nameof(assembly)); if (_assemblies.Contains(assembly)) { return this; } _assemblies.Add(assembly); if (!string.IsNullOrWhiteSpace(assembly.Location)) { _assemblyLocations.Add(Path.GetFullPath(assembly.Location)); } return this; } public PluginCatalog AddFromDirectory(string directory, string searchPattern = "StellaOps.Concelier.*.dll") { if (string.IsNullOrWhiteSpace(directory)) throw new ArgumentException("Directory is required", nameof(directory)); var fullDirectory = Path.GetFullPath(directory); var options = new PluginHostOptions { PluginsDirectory = fullDirectory, EnsureDirectoryExists = false, RecursiveSearch = false, }; options.SearchPatterns.Add(searchPattern); var result = PluginHost.LoadPlugins(options); foreach (var plugin in result.Plugins) { AddAssembly(plugin.Assembly); } return this; } public IReadOnlyList GetConnectorPlugins() => PluginLoader.LoadPlugins(_assemblies); public IReadOnlyList GetExporterPlugins() => PluginLoader.LoadPlugins(_assemblies); public IReadOnlyList GetAvailableConnectorPlugins(IServiceProvider services) => FilterAvailable(GetConnectorPlugins(), services); public IReadOnlyList GetAvailableExporterPlugins(IServiceProvider services) => FilterAvailable(GetExporterPlugins(), services); private static IReadOnlyList FilterAvailable(IEnumerable plugins, IServiceProvider services) where TPlugin : IAvailabilityPlugin { var list = new List(); foreach (var plugin in plugins) { try { if (plugin.IsAvailable(services)) { list.Add(plugin); } } catch { // Treat exceptions as plugin not available. } } return list; } } public static class PluginLoader { public static IReadOnlyList LoadPlugins(IEnumerable assemblies) where TPlugin : class { if (assemblies == null) throw new ArgumentNullException(nameof(assemblies)); var plugins = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var assembly in assemblies) { foreach (var candidate in SafeGetTypes(assembly)) { if (candidate.IsAbstract || candidate.IsInterface) { continue; } if (!typeof(TPlugin).IsAssignableFrom(candidate)) { continue; } if (Activator.CreateInstance(candidate) is not TPlugin plugin) { continue; } var key = candidate.FullName ?? candidate.Name; if (key is null || !seen.Add(key)) { continue; } plugins.Add(plugin); } } return plugins; } private static IEnumerable SafeGetTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where(t => t is not null)!; } } }