using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Microsoft.Extensions.Logging; using StellaOps.Plugin; using StellaOps.Plugin.Hosting; using StellaOps.Scanner.Core.Security; namespace StellaOps.Scanner.Analyzers.Lang.Plugin; public interface ILanguageAnalyzerPluginCatalog { IReadOnlyCollection Plugins { get; } void LoadFromDirectory(string directory, bool seal = true); IReadOnlyList CreateAnalyzers(IServiceProvider services); } public sealed class LanguageAnalyzerPluginCatalog : ILanguageAnalyzerPluginCatalog { private readonly ILogger _logger; private readonly IPluginCatalogGuard _guard; private readonly ConcurrentDictionary _assemblies = new(StringComparer.OrdinalIgnoreCase); private IReadOnlyList _plugins = Array.Empty(); public LanguageAnalyzerPluginCatalog(IPluginCatalogGuard guard, ILogger logger) { _guard = guard ?? throw new ArgumentNullException(nameof(guard)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public IReadOnlyCollection Plugins => _plugins; public void LoadFromDirectory(string directory, bool seal = true) { ArgumentException.ThrowIfNullOrWhiteSpace(directory); var fullDirectory = Path.GetFullPath(directory); var options = new PluginHostOptions { PluginsDirectory = fullDirectory, EnsureDirectoryExists = false, RecursiveSearch = false, }; options.SearchPatterns.Add("StellaOps.Scanner.Analyzers.*.dll"); var result = PluginHost.LoadPlugins(options, _logger); if (result.Plugins.Count == 0) { _logger.LogWarning("No language analyzer plug-ins discovered under '{Directory}'.", fullDirectory); } foreach (var descriptor in result.Plugins) { try { _guard.EnsureRegistrationAllowed(descriptor.AssemblyPath); _assemblies[descriptor.AssemblyPath] = descriptor.Assembly; _logger.LogInformation( "Registered language analyzer plug-in assembly '{Assembly}' from '{Path}'.", descriptor.Assembly.FullName, descriptor.AssemblyPath); } catch (Exception ex) { _logger.LogError(ex, "Failed to register language analyzer plug-in '{Path}'.", descriptor.AssemblyPath); } } RefreshPluginList(); if (seal) { _guard.Seal(); } } public IReadOnlyList CreateAnalyzers(IServiceProvider services) { ArgumentNullException.ThrowIfNull(services); if (_plugins.Count == 0) { _logger.LogWarning("No language analyzer plug-ins available; skipping language analysis."); return Array.Empty(); } var analyzers = new List(_plugins.Count); foreach (var plugin in _plugins) { if (!IsPluginAvailable(plugin, services)) { continue; } try { var analyzer = plugin.CreateAnalyzer(services); if (analyzer is null) { continue; } analyzers.Add(analyzer); } catch (Exception ex) { _logger.LogError(ex, "Language analyzer plug-in '{Plugin}' failed to create analyzer instance.", plugin.Name); } } if (analyzers.Count == 0) { _logger.LogWarning("All language analyzer plug-ins were unavailable."); return Array.Empty(); } analyzers.Sort(static (a, b) => string.CompareOrdinal(a.Id, b.Id)); return new ReadOnlyCollection(analyzers); } private void RefreshPluginList() { var assemblies = _assemblies.Values.ToArray(); var plugins = PluginLoader.LoadPlugins(assemblies); _plugins = plugins is IReadOnlyList list ? list : new ReadOnlyCollection(plugins.ToArray()); } private static bool IsPluginAvailable(ILanguageAnalyzerPlugin plugin, IServiceProvider services) { try { return plugin.IsAvailable(services); } catch { return false; } } }