Files
git.stella-ops.org/src/Plugin/StellaOps.Plugin.Host/Discovery/EmbeddedPluginDiscovery.cs
2026-02-01 21:37:40 +02:00

155 lines
5.2 KiB
C#

using Microsoft.Extensions.Logging;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Attributes;
using StellaOps.Plugin.Abstractions.Manifest;
using System.Reflection;
namespace StellaOps.Plugin.Host.Discovery;
/// <summary>
/// Discovers plugins embedded in loaded assemblies.
/// </summary>
public sealed class EmbeddedPluginDiscovery : IPluginDiscovery
{
private readonly ILogger<EmbeddedPluginDiscovery> _logger;
private readonly IReadOnlyList<Assembly> _assemblies;
/// <summary>
/// Creates a new embedded plugin discovery instance.
/// </summary>
/// <param name="logger">Logger instance.</param>
/// <param name="assemblies">Optional list of assemblies to scan. If null, scans all loaded assemblies.</param>
public EmbeddedPluginDiscovery(
ILogger<EmbeddedPluginDiscovery> logger,
IEnumerable<Assembly>? assemblies = null)
{
_logger = logger;
_assemblies = assemblies?.ToList() ?? [];
}
/// <inheritdoc />
public Task<IReadOnlyList<PluginManifest>> DiscoverAsync(
IEnumerable<string> searchPaths,
CancellationToken ct)
{
// searchPaths are ignored for embedded discovery
// We only scan the provided assemblies or the application assemblies
var manifests = new List<PluginManifest>();
var assembliesToScan = _assemblies.Count > 0
? _assemblies
: GetApplicationAssemblies();
foreach (var assembly in assembliesToScan)
{
ct.ThrowIfCancellationRequested();
try
{
var pluginTypes = assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && typeof(IPlugin).IsAssignableFrom(t))
.ToList();
foreach (var pluginType in pluginTypes)
{
try
{
var manifest = CreateManifestFromType(pluginType);
if (manifest != null)
{
manifests.Add(manifest);
_logger.LogDebug(
"Discovered embedded plugin {PluginId} in assembly {Assembly}",
manifest.Info.Id, assembly.GetName().Name);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Failed to create manifest for type {Type} in assembly {Assembly}",
pluginType.FullName, assembly.GetName().Name);
}
}
}
catch (ReflectionTypeLoadException ex)
{
_logger.LogWarning(ex, "Failed to load types from assembly {Assembly}",
assembly.GetName().Name);
}
}
return Task.FromResult<IReadOnlyList<PluginManifest>>(manifests);
}
/// <inheritdoc />
public Task<PluginManifest> DiscoverSingleAsync(PluginSource source, CancellationToken ct)
{
if (source.Type != PluginSourceType.Embedded)
throw new ArgumentException($"Unsupported source type: {source.Type}", nameof(source));
// Location should be the fully qualified type name
var typeName = source.Location;
foreach (var assembly in GetApplicationAssemblies())
{
var type = assembly.GetType(typeName);
if (type != null && typeof(IPlugin).IsAssignableFrom(type))
{
var manifest = CreateManifestFromType(type);
if (manifest != null)
return Task.FromResult(manifest);
}
}
throw new InvalidOperationException($"Embedded plugin type not found: {typeName}");
}
private static IReadOnlyList<Assembly> GetApplicationAssemblies()
{
var entryAssembly = Assembly.GetEntryAssembly();
if (entryAssembly == null)
return AppDomain.CurrentDomain.GetAssemblies();
var referencedNames = entryAssembly.GetReferencedAssemblies();
var assemblies = new List<Assembly> { entryAssembly };
foreach (var name in referencedNames)
{
try
{
assemblies.Add(Assembly.Load(name));
}
catch
{
// Skip assemblies that can't be loaded
}
}
return assemblies;
}
private static PluginManifest? CreateManifestFromType(Type pluginType)
{
var attribute = pluginType.GetCustomAttribute<PluginAttribute>();
if (attribute == null)
return null;
return new PluginManifest
{
Info = new PluginInfo(
Id: attribute.Id,
Name: attribute.Name ?? pluginType.Name,
Version: attribute.Version ?? "1.0.0",
Vendor: attribute.Vendor ?? "Unknown",
Description: attribute.Description),
EntryPoint = pluginType.FullName!,
AssemblyPath = pluginType.Assembly.Location,
Capabilities = [],
Dependencies = [],
Permissions = [],
Tags = []
};
}
}