Restructure solution layout by module
This commit is contained in:
219
src/__Libraries/StellaOps.Plugin/Hosting/PluginHost.cs
Normal file
219
src/__Libraries/StellaOps.Plugin/Hosting/PluginHost.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
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)
|
||||
? $"{options.PrimaryPrefix}.PluginBinaries"
|
||||
: "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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user