up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,38 +1,38 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.Hosting;
using StellaOps.Plugin.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
namespace StellaOps.Plugin.DependencyInjection;
public static class PluginDependencyInjectionExtensions
{
public static IServiceCollection RegisterPluginRoutines(
this IServiceCollection services,
IConfiguration configuration,
PluginHostOptions options,
ILogger? logger = null)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.Hosting;
using StellaOps.Plugin.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
namespace StellaOps.Plugin.DependencyInjection;
public static class PluginDependencyInjectionExtensions
{
public static IServiceCollection RegisterPluginRoutines(
this IServiceCollection services,
IConfiguration configuration,
PluginHostOptions options,
ILogger? logger = null)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
var loadResult = PluginHost.LoadPlugins(options, logger);
foreach (var plugin in loadResult.Plugins)
@@ -44,50 +44,50 @@ public static class PluginDependencyInjectionExtensions
logger?.LogDebug(
"Registering DI routine '{RoutineType}' from plugin '{PluginAssembly}'.",
routine.GetType().FullName,
plugin.Assembly.FullName);
routine.Register(services, configuration);
}
}
if (loadResult.MissingOrderedPlugins.Count > 0)
{
logger?.LogWarning(
"Some ordered plugins were not found: {Missing}",
string.Join(", ", loadResult.MissingOrderedPlugins));
}
return services;
}
private static IEnumerable<IDependencyInjectionRoutine> CreateRoutines(System.Reflection.Assembly assembly)
{
foreach (var type in assembly.GetLoadableTypes())
{
if (type is null || type.IsAbstract || type.IsInterface)
{
continue;
}
if (!typeof(IDependencyInjectionRoutine).IsAssignableFrom(type))
{
continue;
}
object? instance;
try
{
instance = Activator.CreateInstance(type);
}
catch
{
continue;
}
if (instance is IDependencyInjectionRoutine routine)
{
yield return routine;
}
}
}
plugin.Assembly.FullName);
routine.Register(services, configuration);
}
}
if (loadResult.MissingOrderedPlugins.Count > 0)
{
logger?.LogWarning(
"Some ordered plugins were not found: {Missing}",
string.Join(", ", loadResult.MissingOrderedPlugins));
}
return services;
}
private static IEnumerable<IDependencyInjectionRoutine> CreateRoutines(System.Reflection.Assembly assembly)
{
foreach (var type in assembly.GetLoadableTypes())
{
if (type is null || type.IsAbstract || type.IsInterface)
{
continue;
}
if (!typeof(IDependencyInjectionRoutine).IsAssignableFrom(type))
{
continue;
}
object? instance;
try
{
instance = Activator.CreateInstance(type);
}
catch
{
continue;
}
if (instance is IDependencyInjectionRoutine routine)
{
yield return routine;
}
}
}
}

View File

@@ -1,169 +1,169 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.Internal;
namespace StellaOps.Plugin.DependencyInjection;
public static class PluginServiceRegistration
{
public static void RegisterAssemblyMetadata(IServiceCollection services, Assembly assembly, ILogger? logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(assembly);
foreach (var implementationType in assembly.GetLoadableTypes())
{
if (implementationType is null || !implementationType.IsClass || implementationType.IsAbstract)
{
continue;
}
var attributes = implementationType.GetCustomAttributes<ServiceBindingAttribute>(inherit: false);
if (!attributes.Any())
{
continue;
}
foreach (var attribute in attributes)
{
try
{
ApplyBinding(services, implementationType, attribute, logger);
}
catch (Exception ex)
{
logger?.LogWarning(
ex,
"Failed to register service binding for implementation '{Implementation}' declared in assembly '{Assembly}'.",
implementationType.FullName ?? implementationType.Name,
assembly.FullName ?? assembly.GetName().Name);
}
}
}
}
private static void ApplyBinding(
IServiceCollection services,
Type implementationType,
ServiceBindingAttribute attribute,
ILogger? logger)
{
var serviceType = attribute.ServiceType ?? implementationType;
if (!IsValidBinding(serviceType, implementationType))
{
logger?.LogWarning(
"Service binding metadata ignored: implementation '{Implementation}' is not assignable to service '{Service}'.",
implementationType.FullName ?? implementationType.Name,
serviceType.FullName ?? serviceType.Name);
return;
}
if (attribute.ReplaceExisting)
{
RemoveExistingDescriptors(services, serviceType);
}
AddDescriptorIfMissing(services, serviceType, implementationType, attribute.Lifetime, logger);
if (attribute.RegisterAsSelf && serviceType != implementationType)
{
AddDescriptorIfMissing(services, implementationType, implementationType, attribute.Lifetime, logger);
}
}
private static bool IsValidBinding(Type serviceType, Type implementationType)
{
if (serviceType.IsGenericTypeDefinition)
{
return implementationType.IsGenericTypeDefinition
&& implementationType.IsClass
&& implementationType.IsAssignableToGenericTypeDefinition(serviceType);
}
return serviceType.IsAssignableFrom(implementationType);
}
private static void AddDescriptorIfMissing(
IServiceCollection services,
Type serviceType,
Type implementationType,
ServiceLifetime lifetime,
ILogger? logger)
{
if (services.Any(descriptor =>
descriptor.ServiceType == serviceType &&
descriptor.ImplementationType == implementationType))
{
logger?.LogDebug(
"Skipping duplicate service binding for {ServiceType} -> {ImplementationType}.",
serviceType.FullName ?? serviceType.Name,
implementationType.FullName ?? implementationType.Name);
return;
}
ServiceDescriptor descriptor;
if (serviceType.IsGenericTypeDefinition || implementationType.IsGenericTypeDefinition)
{
descriptor = ServiceDescriptor.Describe(serviceType, implementationType, lifetime);
}
else
{
descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
}
services.Add(descriptor);
logger?.LogDebug(
"Registered service binding {ServiceType} -> {ImplementationType} with {Lifetime} lifetime.",
serviceType.FullName ?? serviceType.Name,
implementationType.FullName ?? implementationType.Name,
lifetime);
}
private static void RemoveExistingDescriptors(IServiceCollection services, Type serviceType)
{
for (var i = services.Count - 1; i >= 0; i--)
{
if (services[i].ServiceType == serviceType)
{
services.RemoveAt(i);
}
}
}
private static bool IsAssignableToGenericTypeDefinition(this Type implementationType, Type serviceTypeDefinition)
{
if (!serviceTypeDefinition.IsGenericTypeDefinition)
{
return false;
}
if (implementationType == serviceTypeDefinition)
{
return true;
}
if (implementationType.IsGenericType && implementationType.GetGenericTypeDefinition() == serviceTypeDefinition)
{
return true;
}
var interfaces = implementationType.GetInterfaces();
foreach (var iface in interfaces)
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == serviceTypeDefinition)
{
return true;
}
}
var baseType = implementationType.BaseType;
return baseType is not null && baseType.IsGenericTypeDefinition
? baseType.GetGenericTypeDefinition() == serviceTypeDefinition
: baseType is not null && baseType.IsAssignableToGenericTypeDefinition(serviceTypeDefinition);
}
}
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.Internal;
namespace StellaOps.Plugin.DependencyInjection;
public static class PluginServiceRegistration
{
public static void RegisterAssemblyMetadata(IServiceCollection services, Assembly assembly, ILogger? logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(assembly);
foreach (var implementationType in assembly.GetLoadableTypes())
{
if (implementationType is null || !implementationType.IsClass || implementationType.IsAbstract)
{
continue;
}
var attributes = implementationType.GetCustomAttributes<ServiceBindingAttribute>(inherit: false);
if (!attributes.Any())
{
continue;
}
foreach (var attribute in attributes)
{
try
{
ApplyBinding(services, implementationType, attribute, logger);
}
catch (Exception ex)
{
logger?.LogWarning(
ex,
"Failed to register service binding for implementation '{Implementation}' declared in assembly '{Assembly}'.",
implementationType.FullName ?? implementationType.Name,
assembly.FullName ?? assembly.GetName().Name);
}
}
}
}
private static void ApplyBinding(
IServiceCollection services,
Type implementationType,
ServiceBindingAttribute attribute,
ILogger? logger)
{
var serviceType = attribute.ServiceType ?? implementationType;
if (!IsValidBinding(serviceType, implementationType))
{
logger?.LogWarning(
"Service binding metadata ignored: implementation '{Implementation}' is not assignable to service '{Service}'.",
implementationType.FullName ?? implementationType.Name,
serviceType.FullName ?? serviceType.Name);
return;
}
if (attribute.ReplaceExisting)
{
RemoveExistingDescriptors(services, serviceType);
}
AddDescriptorIfMissing(services, serviceType, implementationType, attribute.Lifetime, logger);
if (attribute.RegisterAsSelf && serviceType != implementationType)
{
AddDescriptorIfMissing(services, implementationType, implementationType, attribute.Lifetime, logger);
}
}
private static bool IsValidBinding(Type serviceType, Type implementationType)
{
if (serviceType.IsGenericTypeDefinition)
{
return implementationType.IsGenericTypeDefinition
&& implementationType.IsClass
&& implementationType.IsAssignableToGenericTypeDefinition(serviceType);
}
return serviceType.IsAssignableFrom(implementationType);
}
private static void AddDescriptorIfMissing(
IServiceCollection services,
Type serviceType,
Type implementationType,
ServiceLifetime lifetime,
ILogger? logger)
{
if (services.Any(descriptor =>
descriptor.ServiceType == serviceType &&
descriptor.ImplementationType == implementationType))
{
logger?.LogDebug(
"Skipping duplicate service binding for {ServiceType} -> {ImplementationType}.",
serviceType.FullName ?? serviceType.Name,
implementationType.FullName ?? implementationType.Name);
return;
}
ServiceDescriptor descriptor;
if (serviceType.IsGenericTypeDefinition || implementationType.IsGenericTypeDefinition)
{
descriptor = ServiceDescriptor.Describe(serviceType, implementationType, lifetime);
}
else
{
descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);
}
services.Add(descriptor);
logger?.LogDebug(
"Registered service binding {ServiceType} -> {ImplementationType} with {Lifetime} lifetime.",
serviceType.FullName ?? serviceType.Name,
implementationType.FullName ?? implementationType.Name,
lifetime);
}
private static void RemoveExistingDescriptors(IServiceCollection services, Type serviceType)
{
for (var i = services.Count - 1; i >= 0; i--)
{
if (services[i].ServiceType == serviceType)
{
services.RemoveAt(i);
}
}
}
private static bool IsAssignableToGenericTypeDefinition(this Type implementationType, Type serviceTypeDefinition)
{
if (!serviceTypeDefinition.IsGenericTypeDefinition)
{
return false;
}
if (implementationType == serviceTypeDefinition)
{
return true;
}
if (implementationType.IsGenericType && implementationType.GetGenericTypeDefinition() == serviceTypeDefinition)
{
return true;
}
var interfaces = implementationType.GetInterfaces();
foreach (var iface in interfaces)
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == serviceTypeDefinition)
{
return true;
}
}
var baseType = implementationType.BaseType;
return baseType is not null && baseType.IsGenericTypeDefinition
? baseType.GetGenericTypeDefinition() == serviceTypeDefinition
: baseType is not null && baseType.IsAssignableToGenericTypeDefinition(serviceTypeDefinition);
}
}

View File

@@ -1,26 +1,26 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.DependencyInjection;
namespace StellaOps.Plugin.DependencyInjection;
public static class StellaOpsPluginRegistration
{
public static IServiceCollection RegisterStellaOpsPlugin(
this IServiceCollection services,
IConfiguration configuration)
{
// No-op today but reserved for future plugin infrastructure services.
return services;
}
}
public sealed class DependencyInjectionRoutine : IDependencyInjectionRoutine
{
public IServiceCollection Register(
IServiceCollection services,
IConfiguration configuration)
{
return services.RegisterStellaOpsPlugin(configuration);
}
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.DependencyInjection;
namespace StellaOps.Plugin.DependencyInjection;
public static class StellaOpsPluginRegistration
{
public static IServiceCollection RegisterStellaOpsPlugin(
this IServiceCollection services,
IConfiguration configuration)
{
// No-op today but reserved for future plugin infrastructure services.
return services;
}
}
public sealed class DependencyInjectionRoutine : IDependencyInjectionRoutine
{
public IServiceCollection Register(
IServiceCollection services,
IConfiguration configuration)
{
return services.RegisterStellaOpsPlugin(configuration);
}
}

View File

@@ -1,21 +1,21 @@
using System.Reflection;
namespace StellaOps.Plugin.Hosting;
public sealed class PluginAssembly
{
internal PluginAssembly(string assemblyPath, Assembly assembly, PluginLoadContext loadContext)
{
AssemblyPath = assemblyPath;
Assembly = assembly;
LoadContext = loadContext;
}
public string AssemblyPath { get; }
public Assembly Assembly { get; }
internal PluginLoadContext LoadContext { get; }
public override string ToString() => Assembly.FullName ?? AssemblyPath;
using System.Reflection;
namespace StellaOps.Plugin.Hosting;
public sealed class PluginAssembly
{
internal PluginAssembly(string assemblyPath, Assembly assembly, PluginLoadContext loadContext)
{
AssemblyPath = assemblyPath;
Assembly = assembly;
LoadContext = loadContext;
}
public string AssemblyPath { get; }
public Assembly Assembly { get; }
internal PluginLoadContext LoadContext { get; }
public override string ToString() => Assembly.FullName ?? AssemblyPath;
}

View File

@@ -1,76 +1,76 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace StellaOps.Plugin.Hosting;
public static class PluginHost
{
private static readonly object Sync = new();
private static readonly Dictionary<string, PluginAssembly> LoadedPlugins = new(StringComparer.OrdinalIgnoreCase);
public static PluginHostResult LoadPlugins(PluginHostOptions options, ILogger? logger = null)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
var baseDirectory = options.ResolveBaseDirectory();
var pluginDirectory = ResolvePluginDirectory(options, baseDirectory);
if (options.EnsureDirectoryExists && !Directory.Exists(pluginDirectory))
{
Directory.CreateDirectory(pluginDirectory);
}
if (!Directory.Exists(pluginDirectory))
{
logger?.LogWarning("Plugin directory '{PluginDirectory}' does not exist; no plugins will be loaded.", pluginDirectory);
return new PluginHostResult(pluginDirectory, Array.Empty<string>(), Array.Empty<PluginAssembly>(), Array.Empty<string>());
}
var searchPatterns = BuildSearchPatterns(options, pluginDirectory);
var discovered = DiscoverPluginFiles(pluginDirectory, searchPatterns, options.RecursiveSearch, logger);
var orderedFiles = ApplyExplicitOrdering(discovered, options.PluginOrder, out var missingOrderedNames);
var loaded = new List<PluginAssembly>(orderedFiles.Count);
lock (Sync)
{
foreach (var file in orderedFiles)
{
if (LoadedPlugins.TryGetValue(file, out var existing))
{
loaded.Add(existing);
continue;
}
try
{
var loadContext = new PluginLoadContext(file);
var assembly = loadContext.LoadFromAssemblyPath(file);
var descriptor = new PluginAssembly(file, assembly, loadContext);
LoadedPlugins[file] = descriptor;
loaded.Add(descriptor);
logger?.LogInformation("Loaded plugin assembly '{Assembly}' from '{Path}'.", assembly.FullName, file);
}
catch (Exception ex)
{
logger?.LogError(ex, "Failed to load plugin assembly from '{Path}'.", file);
}
}
}
var missingOrdered = new ReadOnlyCollection<string>(missingOrderedNames);
return new PluginHostResult(pluginDirectory, searchPatterns, new ReadOnlyCollection<PluginAssembly>(loaded), missingOrdered);
}
private static string ResolvePluginDirectory(PluginHostOptions options, string baseDirectory)
{
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace StellaOps.Plugin.Hosting;
public static class PluginHost
{
private static readonly object Sync = new();
private static readonly Dictionary<string, PluginAssembly> LoadedPlugins = new(StringComparer.OrdinalIgnoreCase);
public static PluginHostResult LoadPlugins(PluginHostOptions options, ILogger? logger = null)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
var baseDirectory = options.ResolveBaseDirectory();
var pluginDirectory = ResolvePluginDirectory(options, baseDirectory);
if (options.EnsureDirectoryExists && !Directory.Exists(pluginDirectory))
{
Directory.CreateDirectory(pluginDirectory);
}
if (!Directory.Exists(pluginDirectory))
{
logger?.LogWarning("Plugin directory '{PluginDirectory}' does not exist; no plugins will be loaded.", pluginDirectory);
return new PluginHostResult(pluginDirectory, Array.Empty<string>(), Array.Empty<PluginAssembly>(), Array.Empty<string>());
}
var searchPatterns = BuildSearchPatterns(options, pluginDirectory);
var discovered = DiscoverPluginFiles(pluginDirectory, searchPatterns, options.RecursiveSearch, logger);
var orderedFiles = ApplyExplicitOrdering(discovered, options.PluginOrder, out var missingOrderedNames);
var loaded = new List<PluginAssembly>(orderedFiles.Count);
lock (Sync)
{
foreach (var file in orderedFiles)
{
if (LoadedPlugins.TryGetValue(file, out var existing))
{
loaded.Add(existing);
continue;
}
try
{
var loadContext = new PluginLoadContext(file);
var assembly = loadContext.LoadFromAssemblyPath(file);
var descriptor = new PluginAssembly(file, assembly, loadContext);
LoadedPlugins[file] = descriptor;
loaded.Add(descriptor);
logger?.LogInformation("Loaded plugin assembly '{Assembly}' from '{Path}'.", assembly.FullName, file);
}
catch (Exception ex)
{
logger?.LogError(ex, "Failed to load plugin assembly from '{Path}'.", file);
}
}
}
var missingOrdered = new ReadOnlyCollection<string>(missingOrderedNames);
return new PluginHostResult(pluginDirectory, searchPatterns, new ReadOnlyCollection<PluginAssembly>(loaded), missingOrdered);
}
private static string ResolvePluginDirectory(PluginHostOptions options, string baseDirectory)
{
if (string.IsNullOrWhiteSpace(options.PluginsDirectory))
{
var defaultDirectory = !string.IsNullOrWhiteSpace(options.PrimaryPrefix)
@@ -78,142 +78,142 @@ public static class PluginHost
: "PluginBinaries";
return Path.Combine(baseDirectory, defaultDirectory);
}
if (Path.IsPathRooted(options.PluginsDirectory))
{
return options.PluginsDirectory;
}
return Path.Combine(baseDirectory, options.PluginsDirectory);
}
private static IReadOnlyList<string> BuildSearchPatterns(PluginHostOptions options, string pluginDirectory)
{
var patterns = new List<string>();
if (options.SearchPatterns.Count > 0)
{
patterns.AddRange(options.SearchPatterns);
}
else
{
var prefixes = new List<string>();
if (!string.IsNullOrWhiteSpace(options.PrimaryPrefix))
{
prefixes.Add(options.PrimaryPrefix);
}
else if (System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name is { } entryName)
{
prefixes.Add(entryName);
}
prefixes.AddRange(options.AdditionalPrefixes);
if (prefixes.Count == 0)
{
// Fallback to directory name
prefixes.Add(Path.GetFileName(pluginDirectory));
}
foreach (var prefix in prefixes.Where(p => !string.IsNullOrWhiteSpace(p)))
{
patterns.Add($"{prefix}.Plugin.*.dll");
}
}
return new ReadOnlyCollection<string>(patterns.Distinct(StringComparer.OrdinalIgnoreCase).ToList());
}
private static List<string> DiscoverPluginFiles(
string pluginDirectory,
IReadOnlyList<string> searchPatterns,
bool recurse,
ILogger? logger)
{
var files = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var searchOption = recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
foreach (var pattern in searchPatterns)
{
try
{
foreach (var file in Directory.EnumerateFiles(pluginDirectory, pattern, searchOption))
{
if (IsHiddenPath(file))
{
continue;
}
if (seen.Add(file))
{
files.Add(file);
}
}
}
catch (DirectoryNotFoundException)
{
// Directory could be removed between the existence check and enumeration.
logger?.LogDebug("Plugin directory '{PluginDirectory}' disappeared before enumeration.", pluginDirectory);
}
}
return files;
}
private static List<string> ApplyExplicitOrdering(
List<string> discoveredFiles,
IList<string> pluginOrder,
out List<string> missingNames)
{
if (pluginOrder.Count == 0 || discoveredFiles.Count == 0)
{
missingNames = new List<string>();
discoveredFiles.Sort(StringComparer.OrdinalIgnoreCase);
return discoveredFiles;
}
var configuredSet = new HashSet<string>(pluginOrder, StringComparer.OrdinalIgnoreCase);
var fileLookup = discoveredFiles.ToDictionary(
k => Path.GetFileNameWithoutExtension(k),
StringComparer.OrdinalIgnoreCase);
var specified = new List<string>();
foreach (var name in pluginOrder)
{
if (fileLookup.TryGetValue(name, out var file))
{
specified.Add(file);
}
}
var unspecified = discoveredFiles
.Where(f => !configuredSet.Contains(Path.GetFileNameWithoutExtension(f)))
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
.ToList();
missingNames = pluginOrder
.Where(name => !fileLookup.ContainsKey(name))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
specified.AddRange(unspecified);
return specified;
}
private static bool IsHiddenPath(string filePath)
{
var directory = Path.GetDirectoryName(filePath);
while (!string.IsNullOrEmpty(directory))
{
var name = Path.GetFileName(directory);
if (name.StartsWith(".", StringComparison.Ordinal))
{
return true;
}
directory = Path.GetDirectoryName(directory);
}
return false;
}
if (Path.IsPathRooted(options.PluginsDirectory))
{
return options.PluginsDirectory;
}
return Path.Combine(baseDirectory, options.PluginsDirectory);
}
private static IReadOnlyList<string> BuildSearchPatterns(PluginHostOptions options, string pluginDirectory)
{
var patterns = new List<string>();
if (options.SearchPatterns.Count > 0)
{
patterns.AddRange(options.SearchPatterns);
}
else
{
var prefixes = new List<string>();
if (!string.IsNullOrWhiteSpace(options.PrimaryPrefix))
{
prefixes.Add(options.PrimaryPrefix);
}
else if (System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name is { } entryName)
{
prefixes.Add(entryName);
}
prefixes.AddRange(options.AdditionalPrefixes);
if (prefixes.Count == 0)
{
// Fallback to directory name
prefixes.Add(Path.GetFileName(pluginDirectory));
}
foreach (var prefix in prefixes.Where(p => !string.IsNullOrWhiteSpace(p)))
{
patterns.Add($"{prefix}.Plugin.*.dll");
}
}
return new ReadOnlyCollection<string>(patterns.Distinct(StringComparer.OrdinalIgnoreCase).ToList());
}
private static List<string> DiscoverPluginFiles(
string pluginDirectory,
IReadOnlyList<string> searchPatterns,
bool recurse,
ILogger? logger)
{
var files = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var searchOption = recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
foreach (var pattern in searchPatterns)
{
try
{
foreach (var file in Directory.EnumerateFiles(pluginDirectory, pattern, searchOption))
{
if (IsHiddenPath(file))
{
continue;
}
if (seen.Add(file))
{
files.Add(file);
}
}
}
catch (DirectoryNotFoundException)
{
// Directory could be removed between the existence check and enumeration.
logger?.LogDebug("Plugin directory '{PluginDirectory}' disappeared before enumeration.", pluginDirectory);
}
}
return files;
}
private static List<string> ApplyExplicitOrdering(
List<string> discoveredFiles,
IList<string> pluginOrder,
out List<string> missingNames)
{
if (pluginOrder.Count == 0 || discoveredFiles.Count == 0)
{
missingNames = new List<string>();
discoveredFiles.Sort(StringComparer.OrdinalIgnoreCase);
return discoveredFiles;
}
var configuredSet = new HashSet<string>(pluginOrder, StringComparer.OrdinalIgnoreCase);
var fileLookup = discoveredFiles.ToDictionary(
k => Path.GetFileNameWithoutExtension(k),
StringComparer.OrdinalIgnoreCase);
var specified = new List<string>();
foreach (var name in pluginOrder)
{
if (fileLookup.TryGetValue(name, out var file))
{
specified.Add(file);
}
}
var unspecified = discoveredFiles
.Where(f => !configuredSet.Contains(Path.GetFileNameWithoutExtension(f)))
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
.ToList();
missingNames = pluginOrder
.Where(name => !fileLookup.ContainsKey(name))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
specified.AddRange(unspecified);
return specified;
}
private static bool IsHiddenPath(string filePath)
{
var directory = Path.GetDirectoryName(filePath);
while (!string.IsNullOrEmpty(directory))
{
var name = Path.GetFileName(directory);
if (name.StartsWith(".", StringComparison.Ordinal))
{
return true;
}
directory = Path.GetDirectoryName(directory);
}
return false;
}
}

View File

@@ -1,59 +1,59 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace StellaOps.Plugin.Hosting;
public sealed class PluginHostOptions
{
private readonly List<string> additionalPrefixes = new();
private readonly List<string> pluginOrder = new();
private readonly List<string> searchPatterns = new();
/// <summary>
/// Optional base directory used for resolving relative plugin paths. Defaults to <see cref="AppContext.BaseDirectory" />.
/// </summary>
public string? BaseDirectory { get; set; }
/// <summary>
using System;
using System.Collections.Generic;
using System.IO;
namespace StellaOps.Plugin.Hosting;
public sealed class PluginHostOptions
{
private readonly List<string> additionalPrefixes = new();
private readonly List<string> pluginOrder = new();
private readonly List<string> searchPatterns = new();
/// <summary>
/// Optional base directory used for resolving relative plugin paths. Defaults to <see cref="AppContext.BaseDirectory" />.
/// </summary>
public string? BaseDirectory { get; set; }
/// <summary>
/// Directory that contains plugin assemblies. Relative values are resolved against <see cref="BaseDirectory" />.
/// Defaults to <c>{PrimaryPrefix}.PluginBinaries</c> when a primary prefix is provided, otherwise <c>PluginBinaries</c>.
/// </summary>
public string? PluginsDirectory { get; set; }
/// <summary>
/// Primary prefix used to discover plugin assemblies. If not supplied, the entry assembly name is used.
/// </summary>
public string? PrimaryPrefix { get; set; }
/// <summary>
/// Additional prefixes that should be considered when building search patterns.
/// </summary>
public IList<string> AdditionalPrefixes => additionalPrefixes;
/// <summary>
/// Explicit plugin ordering expressed as assembly names without extension.
/// Entries that are not discovered will be reported in <see cref="PluginHostResult.MissingOrderedPlugins" />.
/// </summary>
public IList<string> PluginOrder => pluginOrder;
/// <summary>
/// Optional explicit search patterns. When empty, they are derived from prefix settings.
/// </summary>
public IList<string> SearchPatterns => searchPatterns;
/// <summary>
/// When true (default) the plugin directory will be created if it does not exist.
/// </summary>
public bool EnsureDirectoryExists { get; set; } = true;
/// <summary>
/// Controls whether sub-directories should be scanned. Defaults to true.
/// </summary>
public bool RecursiveSearch { get; set; } = true;
internal string ResolveBaseDirectory()
=> string.IsNullOrWhiteSpace(BaseDirectory)
? AppContext.BaseDirectory
: Path.GetFullPath(BaseDirectory);
/// </summary>
public string? PluginsDirectory { get; set; }
/// <summary>
/// Primary prefix used to discover plugin assemblies. If not supplied, the entry assembly name is used.
/// </summary>
public string? PrimaryPrefix { get; set; }
/// <summary>
/// Additional prefixes that should be considered when building search patterns.
/// </summary>
public IList<string> AdditionalPrefixes => additionalPrefixes;
/// <summary>
/// Explicit plugin ordering expressed as assembly names without extension.
/// Entries that are not discovered will be reported in <see cref="PluginHostResult.MissingOrderedPlugins" />.
/// </summary>
public IList<string> PluginOrder => pluginOrder;
/// <summary>
/// Optional explicit search patterns. When empty, they are derived from prefix settings.
/// </summary>
public IList<string> SearchPatterns => searchPatterns;
/// <summary>
/// When true (default) the plugin directory will be created if it does not exist.
/// </summary>
public bool EnsureDirectoryExists { get; set; } = true;
/// <summary>
/// Controls whether sub-directories should be scanned. Defaults to true.
/// </summary>
public bool RecursiveSearch { get; set; } = true;
internal string ResolveBaseDirectory()
=> string.IsNullOrWhiteSpace(BaseDirectory)
? AppContext.BaseDirectory
: Path.GetFullPath(BaseDirectory);
}

View File

@@ -1,26 +1,26 @@
using System.Collections.Generic;
namespace StellaOps.Plugin.Hosting;
public sealed class PluginHostResult
{
internal PluginHostResult(
string pluginDirectory,
IReadOnlyList<string> searchPatterns,
IReadOnlyList<PluginAssembly> plugins,
IReadOnlyList<string> missingOrderedPlugins)
{
PluginDirectory = pluginDirectory;
SearchPatterns = searchPatterns;
Plugins = plugins;
MissingOrderedPlugins = missingOrderedPlugins;
}
public string PluginDirectory { get; }
public IReadOnlyList<string> SearchPatterns { get; }
public IReadOnlyList<PluginAssembly> Plugins { get; }
public IReadOnlyList<string> MissingOrderedPlugins { get; }
using System.Collections.Generic;
namespace StellaOps.Plugin.Hosting;
public sealed class PluginHostResult
{
internal PluginHostResult(
string pluginDirectory,
IReadOnlyList<string> searchPatterns,
IReadOnlyList<PluginAssembly> plugins,
IReadOnlyList<string> missingOrderedPlugins)
{
PluginDirectory = pluginDirectory;
SearchPatterns = searchPatterns;
Plugins = plugins;
MissingOrderedPlugins = missingOrderedPlugins;
}
public string PluginDirectory { get; }
public IReadOnlyList<string> SearchPatterns { get; }
public IReadOnlyList<PluginAssembly> Plugins { get; }
public IReadOnlyList<string> MissingOrderedPlugins { get; }
}

View File

@@ -1,79 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
namespace StellaOps.Plugin.Hosting;
internal sealed class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver resolver;
private readonly IEnumerable<Assembly> hostAssemblies;
public PluginLoadContext(string pluginPath)
: base(isCollectible: false)
{
resolver = new AssemblyDependencyResolver(pluginPath);
hostAssemblies = AssemblyLoadContext.Default.Assemblies;
}
protected override Assembly? Load(AssemblyName assemblyName)
{
// Attempt to reuse assemblies that already exist in the default context when versions are compatible.
var existing = hostAssemblies.FirstOrDefault(a => string.Equals(
a.GetName().Name,
assemblyName.Name,
StringComparison.OrdinalIgnoreCase));
if (existing != null && IsCompatible(existing.GetName(), assemblyName))
{
return existing;
}
var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);
if (!string.IsNullOrEmpty(assemblyPath))
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var libraryPath = resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (!string.IsNullOrEmpty(libraryPath))
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
private static bool IsCompatible(AssemblyName hostAssembly, AssemblyName pluginAssembly)
{
if (hostAssembly.Version == pluginAssembly.Version)
{
return true;
}
if (hostAssembly.Version is null || pluginAssembly.Version is null)
{
return false;
}
if (hostAssembly.Version.Major == pluginAssembly.Version.Major &&
hostAssembly.Version.Minor >= pluginAssembly.Version.Minor)
{
return true;
}
if (hostAssembly.Version.Major >= pluginAssembly.Version.Major)
{
return true;
}
return false;
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
namespace StellaOps.Plugin.Hosting;
internal sealed class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver resolver;
private readonly IEnumerable<Assembly> hostAssemblies;
public PluginLoadContext(string pluginPath)
: base(isCollectible: false)
{
resolver = new AssemblyDependencyResolver(pluginPath);
hostAssemblies = AssemblyLoadContext.Default.Assemblies;
}
protected override Assembly? Load(AssemblyName assemblyName)
{
// Attempt to reuse assemblies that already exist in the default context when versions are compatible.
var existing = hostAssemblies.FirstOrDefault(a => string.Equals(
a.GetName().Name,
assemblyName.Name,
StringComparison.OrdinalIgnoreCase));
if (existing != null && IsCompatible(existing.GetName(), assemblyName))
{
return existing;
}
var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);
if (!string.IsNullOrEmpty(assemblyPath))
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var libraryPath = resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (!string.IsNullOrEmpty(libraryPath))
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
private static bool IsCompatible(AssemblyName hostAssembly, AssemblyName pluginAssembly)
{
if (hostAssembly.Version == pluginAssembly.Version)
{
return true;
}
if (hostAssembly.Version is null || pluginAssembly.Version is null)
{
return false;
}
if (hostAssembly.Version.Major == pluginAssembly.Version.Major &&
hostAssembly.Version.Minor >= pluginAssembly.Version.Minor)
{
return true;
}
if (hostAssembly.Version.Major >= pluginAssembly.Version.Major)
{
return true;
}
return false;
}
}

View File

@@ -1,21 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace StellaOps.Plugin.Internal;
internal static class ReflectionExtensions
{
public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(static t => t is not null)!;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace StellaOps.Plugin.Internal;
internal static class ReflectionExtensions
{
public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(static t => t is not null)!;
}
}
}

View File

@@ -1,172 +1,172 @@
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.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<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)!;
}
}
}
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.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<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)!;
}
}
}

View File

@@ -1,3 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Plugin.Tests")]
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Plugin.Tests")]