173 lines
5.1 KiB
C#
173 lines
5.1 KiB
C#
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<Assembly> _assemblies = new();
|
|
private readonly HashSet<string> _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.Feedser.*.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<IConnectorPlugin> GetConnectorPlugins() => PluginLoader.LoadPlugins<IConnectorPlugin>(_assemblies);
|
|
|
|
public IReadOnlyList<IExporterPlugin> GetExporterPlugins() => PluginLoader.LoadPlugins<IExporterPlugin>(_assemblies);
|
|
|
|
public IReadOnlyList<IConnectorPlugin> GetAvailableConnectorPlugins(IServiceProvider services)
|
|
=> FilterAvailable(GetConnectorPlugins(), services);
|
|
|
|
public IReadOnlyList<IExporterPlugin> GetAvailableExporterPlugins(IServiceProvider services)
|
|
=> FilterAvailable(GetExporterPlugins(), services);
|
|
|
|
private static IReadOnlyList<TPlugin> FilterAvailable<TPlugin>(IEnumerable<TPlugin> plugins, IServiceProvider services)
|
|
where TPlugin : IAvailabilityPlugin
|
|
{
|
|
var list = new List<TPlugin>();
|
|
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<TPlugin> LoadPlugins<TPlugin>(IEnumerable<Assembly> assemblies)
|
|
where TPlugin : class
|
|
{
|
|
if (assemblies == null) throw new ArgumentNullException(nameof(assemblies));
|
|
|
|
var plugins = new List<TPlugin>();
|
|
var seen = new HashSet<string>(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<Type> SafeGetTypes(Assembly assembly)
|
|
{
|
|
try
|
|
{
|
|
return assembly.GetTypes();
|
|
}
|
|
catch (ReflectionTypeLoadException ex)
|
|
{
|
|
return ex.Types.Where(t => t is not null)!;
|
|
}
|
|
}
|
|
}
|
|
|