//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.PlugIns;
using System.Collections.Concurrent;
using System.ComponentModel.Design;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Nito.Disposables.Internals;
///
/// The manager for plugins.
///
public class PlugInManager
{
private readonly ILogger _logger;
private readonly ServiceContainer _serviceContainer;
private readonly IDictionary _plugInPoints = new Dictionary();
private readonly IDictionary _knownPlugIns = new ConcurrentDictionary();
private readonly ConcurrentDictionary> _knownPlugInsPerInterfaceType = new();
private readonly ConcurrentDictionary _activePlugIns = new();
private object? _lastCreatedPlugIn;
///
/// Initializes a new instance of the class.
///
/// The configurations.
/// The logger factory.
/// The service provider.
/// The reference handler for references in custom plugin configurations.
public PlugInManager(ICollection? configurations, ILoggerFactory loggerFactory, IServiceProvider? serviceProvider, ReferenceHandler? customConfigReferenceHandler)
{
_ = typeof(Nito.AsyncEx.AsyncReaderWriterLock); // Ensure Nito.AsyncEx.Coordination is loaded so it will be available in proxy generation.
this._logger = loggerFactory.CreateLogger();
this._serviceContainer = new ServiceContainer(serviceProvider);
this._serviceContainer.AddService(typeof(PlugInManager), this);
this._serviceContainer.AddService(typeof(ILoggerFactory), loggerFactory);
this.CustomConfigReferenceHandler = customConfigReferenceHandler;
if (configurations is not null)
{
this.DiscoverAndRegisterPlugIns();
var loadedAssemblies = new HashSet();
foreach (var configuration in configurations)
{
this.ReadConfiguration(configuration, loadedAssemblies);
}
}
}
///
/// Occurs when a plugin got deactivated.
///
public event EventHandler? PlugInDeactivated;
///
/// Occurs when a plugin got activated.
///
public event EventHandler? PlugInActivated;
///
/// Occurs when the has been changed.
///
public event EventHandler? PlugInConfigurationChanged;
///
/// Gets the known plugin types.
///
///
/// The known plugin types.
///
public IEnumerable KnownPlugInTypes => this._knownPlugIns.Values;
///
/// Gets the reference handler for references in custom plugin configurations.
///
public ReferenceHandler? CustomConfigReferenceHandler { get; }
///
/// Discovers and registers all plugins of all loaded assemblies.
///
public void DiscoverAndRegisterPlugIns()
{
var plugIns = this.DiscoverNewPlugIns();
this.ValidateNoDuplicateGuids(plugIns);
this.RegisterPlugIns(plugIns);
}
///
/// Discovers and registers plugins of the specified assembly.
///
/// The assembly.
public void DiscoverAndRegisterPlugIns(Assembly assembly)
{
var plugIns = this.DiscoverNewPlugIns(this.DiscoverPlugIns(assembly));
this.ValidateNoDuplicateGuids(plugIns);
this.RegisterPlugIns(plugIns);
}
///
/// Discovers and register plugins of type .
///
/// The type of the plugins that should be discovered.
public void DiscoverAndRegisterPlugInsOf()
{
var plugIns = this.DiscoverAllPlugIns().Where(type => typeof(T).IsAssignableFrom(type));
this.RegisterPlugIns(plugIns);
}
///
/// Gets the known plugins of the given interface type.
///
/// The plugin interface type. Type parameter of a .
/// The known plugins of the given interface type.
public IEnumerable GetKnownPlugInsOf()
{
if (this._knownPlugInsPerInterfaceType.TryGetValue(typeof(T), out var result))
{
return result;
}
return this._knownPlugIns.Values.Where(p => typeof(T).IsAssignableFrom(p));
}
///
/// Gets the active plugins of the specified type.
///
/// The type of the plugin.
/// The active plugins of the specified type.
public IEnumerable GetActivePlugInsOf()
{
if (this._plugInPoints.TryGetValue(typeof(TPlugIn), out var point) && point is IPlugInContainer container)
{
return container.ActivePlugIns;
}
return Enumerable.Empty();
}
///
/// Deactivates the plugin of type .
///
/// The type of the plugin.
public void DeactivatePlugIn() => this.DeactivatePlugIn(typeof(TPlugIn));
///
/// Deactivates the plugin of the specified type.
///
/// The plugin.
public void DeactivatePlugIn(Type plugIn) => this.DeactivatePlugIn(plugIn.GUID);
///
/// Deactivates the plugin with the specified type id.
///
/// The plugin identifier.
public void DeactivatePlugIn(Guid plugInId)
{
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
{
this._activePlugIns.TryRemove(plugInId, out _);
this.PlugInDeactivated?.Invoke(this, new PlugInEventArgs(plugInType));
}
}
///
/// Activates the plugin of type .
///
/// The type of the plugin.
public void ActivatePlugIn() => this.ActivatePlugIn(typeof(TPlugIn));
///
/// Activates the plugin of the specified type.
///
/// The plugin.
public void ActivatePlugIn(Type plugIn) => this.ActivatePlugIn(plugIn.GUID);
///
/// Activates the plugin with the specified type id.
///
/// The plugin identifier.
public void ActivatePlugIn(Guid plugInId)
{
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
{
this._activePlugIns.TryAdd(plugInId, plugInType);
this.PlugInActivated?.Invoke(this, new PlugInEventArgs(plugInType));
}
}
///
/// Gets the plugin point which implements .
///
/// The type of the plugin.
/// The plugin point which implements , if available; Otherwise, null.
public TPlugIn? GetPlugInPoint()
where TPlugIn : class
{
if (this._plugInPoints.TryGetValue(typeof(TPlugIn), out var obj) && obj is TPlugIn plugIn)
{
return plugIn;
}
return null;
}
///
/// Gets the strategy plugin.
///
/// The type of the key.
/// The type of the strategy.
/// The strategy plugin.
public IStrategyPlugInProvider? GetStrategyProvider()
where TStrategy : class, IStrategyPlugIn
{
if (this._plugInPoints.TryGetValue(typeof(TStrategy), out var obj) && obj is IStrategyPlugInProvider plugIn)
{
return plugIn;
}
return null;
}
///
/// Gets the strategy plugin.
///
/// The type of the key.
/// The type of the strategy.
/// The key.
/// The strategy plugin of the specified key, if available; Otherwise, null.
public TStrategy? GetStrategy(TKey key)
where TStrategy : class, IStrategyPlugIn
{
return this.GetStrategyProvider()?[key];
}
///
/// Gets the strategy plugin.
///
/// The type of the strategy.
/// The key.
/// The strategy plugin of the specified key, if available; Otherwise, null.
public TStrategy? GetStrategy(string key)
where TStrategy : class, IStrategyPlugIn
{
return this.GetStrategy(key);
}
///
/// Determines whether the specified plugin type is configured as active.
///
/// Type of the plugin.
///
/// true if the specified plugin type is configured as active; otherwise, false.
///
public bool IsPlugInActive(Type plugInType)
{
return this.IsPlugInActive(plugInType.GUID);
}
///
/// Determines whether the specified plugin type is configured as active.
///
/// Identifier of the type of the plugin.
///
/// true if the specified plugin type is configured as active; otherwise, false.
///
public bool IsPlugInActive(Guid plugInTypeId)
{
return this._activePlugIns.ContainsKey(plugInTypeId);
}
///
/// Registers the plugin class for the specified plugin interface.
///
/// The type of the plugin interface.
/// The type of the plugin class.
public void RegisterPlugIn()
where TPlugInInterface : class
where TPlugInClass : class, TPlugInInterface
{
this.RegisterPlugInType(typeof(TPlugInClass));
if (typeof(TPlugInInterface).GetCustomAttribute(typeof(PlugInPointAttribute)) != null)
{
var plugIn = this._lastCreatedPlugIn as TPlugInClass ?? this.CreatePlugInInstance();
this._lastCreatedPlugIn = plugIn;
this.RegisterPlugInAtPlugInPoint(plugIn);
}
else if (this.GetCustomPlugInPointType(typeof(TPlugInInterface)) is { } customPlugInPointType)
{
if (!this._knownPlugInsPerInterfaceType.TryGetValue(customPlugInPointType, out var plugInList))
{
plugInList = new HashSet();
this._knownPlugInsPerInterfaceType.TryAdd(customPlugInPointType, plugInList);
}
plugInList.Add(typeof(TPlugInClass));
this.PlugInActivated?.Invoke(this, new PlugInEventArgs(typeof(TPlugInClass)));
}
else
{
this._logger.LogWarning("Plugin {PlugInClass} wasn't registered, because it's not an implementation of an interface which is marked with PlugInPointAttribute or CustomPlugInContainerAttribute.", typeof(TPlugInClass));
}
}
///
/// Registers the plugin instance for the specified plugin point interface.
///
/// The type of the plugin interface.
/// The instance.
/// Plugin Type {instance.GetType()} - instance.
public void RegisterPlugInAtPlugInPoint(TPlugInInterface instance)
where TPlugInInterface : class
{
if (instance.GetType().GetCustomAttribute() is null)
{
throw new ArgumentException($"Plugin Type {instance.GetType()} is missing a {typeof(GuidAttribute)}. It's required to identify the plugin in the configuration. Otherwise, the plugin would get a different Guid every time it gets compiled.", nameof(instance));
}
if (!this._plugInPoints.TryGetValue(typeof(TPlugInInterface), out var point))
{
var proxy = this.CreateProxy();
proxy.AddPlugIn(instance, true);
this._plugInPoints.Add(typeof(TPlugInInterface), proxy);
}
else if (point is IPlugInContainer proxy)
{
proxy.AddPlugIn(instance, true);
}
else
{
throw new InvalidPlugInProxyException(point.GetType(), typeof(IPlugInContainer));
}
this.RegisterPlugInType(instance.GetType());
}
///
/// Configures the plugin.
///
/// The plugin identifier.
/// The configuration.
public void ConfigurePlugIn(Guid plugInId, PlugInConfiguration configuration)
{
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
{
this.ConfigurePlugIn(plugInType, configuration);
}
}
private void ValidateNoDuplicateGuids(IEnumerable plugIns)
{
var allPlugIns = plugIns.Concat(this._knownPlugIns.Values).Distinct();
var duplicates = allPlugIns.GroupBy(t => t.GUID).Where(g => g.Count() > 1).ToList();
if (duplicates.Count == 0)
{
return;
}
var message = string.Join("; ", duplicates.Select(g =>
$"{g.Key} used by {string.Join(", ", g.Select(t => t.FullName ?? t.Name))}"));
throw new DuplicatePlugInGuidException(message);
}
private Type? GetCustomPlugInPointType(Type interfaceType)
{
if (interfaceType.GetCustomAttribute() != null)
{
return interfaceType;
}
return interfaceType.GetInterfaces().FirstOrDefault(i => i.GetCustomAttribute() != null);
}
private void RegisterPlugInType(Type plugInType)
{
var plugInTypeId = plugInType.GUID;
if (!this._knownPlugIns.ContainsKey(plugInTypeId))
{
this._knownPlugIns.Add(plugInTypeId, plugInType);
this._activePlugIns.TryAdd(plugInTypeId, plugInType); // registered plugins are by default active
this._logger.LogDebug("Added known plugin {0}, {1}", plugInTypeId, plugInType);
}
}
private IPlugInContainer CreateProxy()
where TPlugInInterface : class
{
IPlugInContainer proxy;
var strategyPlugInInterface = typeof(TPlugInInterface).GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IStrategyPlugIn<>));
if (strategyPlugInInterface != null)
{
var keyType = strategyPlugInInterface.GetGenericArguments()[0];
var providerType = typeof(StrategyPlugInProvider<,>).MakeGenericType(keyType, typeof(TPlugInInterface));
proxy = (IPlugInContainer)ActivatorUtilities.CreateInstance(this._serviceContainer, providerType);
}
else
{
var proxyGenerator = new PlugInProxyTypeGenerator();
proxy = proxyGenerator.GenerateProxy(this);
}
return proxy;
}
private TPlugInClass CreatePlugInInstance()
{
return ActivatorUtilities.CreateInstance(this._serviceContainer);
}
private void ReadConfiguration(PlugInConfiguration configuration, HashSet loadedAssemblies)
{
if (!this._knownPlugIns.ContainsKey(configuration.TypeId))
{
if (!string.IsNullOrEmpty(configuration.ExternalAssemblyName)
&& !loadedAssemblies.Contains(configuration.ExternalAssemblyName.ToLower()))
{
loadedAssemblies.Add(configuration.ExternalAssemblyName.ToLower());
try
{
var assembly = Assembly.LoadFile("plugins\\" + configuration.ExternalAssemblyName);
this.DiscoverAndRegisterPlugIns(assembly);
}
catch (Exception e)
{
this._logger.LogError(e, "Error while loading external plugin assembly {ExternalAssemblyName} for plugin {TypeId}.", configuration.ExternalAssemblyName, configuration.TypeId);
return;
}
}
else if (!string.IsNullOrEmpty(configuration.CustomPlugInSource))
{
this._logger.LogWarning("Custom plugin source found at plugin configuration: {Configuration}", configuration);
/* TODO: Implement code signing, if we really need this feature.
Assembly customPlugInAssembly = this.CompileCustomPlugInAssembly(configuration);
this.DiscoverAndRegisterPlugIns(customPlugInAssembly);*/
}
else
{
// nothing we can do
}
}
if (this._knownPlugIns.TryGetValue(configuration.TypeId, out var plugInType))
{
if (!configuration.IsActive)
{
this.DeactivatePlugIn(plugInType);
}
this.ConfigurePlugIn(plugInType, configuration);
// When the IsActive property changed, we activate/deactivate accordingly.
// Currently, property changes are only fired for IsActive, so we don't need to check it.
configuration.PropertyChanged += (sender, args) => this.OnConfigurationChanged(configuration, plugInType, args.PropertyName);
}
else
{
this._logger.LogWarning("Unknown plugin type for id {TypeId}", configuration.TypeId);
}
}
private void OnConfigurationChanged(PlugInConfiguration configuration, Type plugInType, string? propertyName)
{
if (propertyName == nameof(PlugInConfiguration.IsActive))
{
if (configuration.IsActive)
{
this.ActivatePlugIn(plugInType);
this.ConfigurePlugIn(plugInType, configuration);
}
else
{
this.DeactivatePlugIn(plugInType);
}
}
if (propertyName == nameof(PlugInConfiguration.CustomConfiguration))
{
this.ConfigurePlugIn(plugInType, configuration);
}
}
private void ConfigurePlugIn(Type plugInType, PlugInConfiguration configuration)
{
this.PlugInConfigurationChanged?.Invoke(this, new PlugInConfigurationChangedEventArgs(plugInType, configuration));
}
private IEnumerable DiscoverNewPlugIns()
{
return this.DiscoverNewPlugIns(this.DiscoverAllPlugIns());
}
private IEnumerable DiscoverNewPlugIns(IEnumerable allPlugIns)
{
var newPlugIns = allPlugIns.Where(plugIn => !this._knownPlugIns.ContainsKey(plugIn.GUID)).ToList();
return newPlugIns;
}
private IEnumerable DiscoverAllPlugIns()
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => assembly.FullName is not null)
.Where(assembly => !assembly.FullName!.StartsWith("System"))
.Where(assembly => !assembly.FullName!.StartsWith("Microsoft"))
.Where(assembly => !assembly.FullName!.StartsWith("Nito"))
.Where(assembly => !assembly.FullName!.StartsWith("Blazor"))
.SelectMany(assembly =>
{
try
{
return assembly.DefinedTypes.Where(type => type.GetCustomAttribute() != null);
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.WhereNotNull();
}
catch (Exception)
{
return Enumerable.Empty();
}
});
}
private IEnumerable DiscoverPlugIns(Assembly assembly)
{
return assembly.DefinedTypes.Where(type => type.GetCustomAttribute() != null);
}
private void RegisterPlugIns(IEnumerable plugIns)
{
foreach (var plugIn in plugIns)
{
try
{
// A plugin usually should be small, but it should be possible that one plugin can implement more than one plugin interface.
// In this case, we need to register it at every plugin point
var plugInInterfaces = plugIn.GetInterfaces().Where(t => t.IsInterface && (t.GetCustomAttribute() != null || t.GetCustomAttribute() != null));
foreach (var plugInInterface in plugInInterfaces)
{
var genericMethod = this.GetType().GetMethods().FirstOrDefault(mi => mi.IsGenericMethod && mi.Name == nameof(this.RegisterPlugIn))?.MakeGenericMethod(plugInInterface, plugIn);
genericMethod?.Invoke(this, []);
}
}
catch (Exception e)
{
this._logger.LogError(e, "Couldn't register plugin type {PlugIn}", plugIn);
this._logger.LogError("TODO: Use ServiceContainer");
}
this._lastCreatedPlugIn = null;
}
}
///
/// Exception that is thrown when two different plugin types share the same .
///
public class DuplicatePlugInGuidException : InvalidOperationException
{
///
/// Initializes a new instance of the class.
///
/// The message.
public DuplicatePlugInGuidException(string message)
: base(message)
{
}
}
///
/// Exception that occurs when the created proxy doesn't implement the expected interface .
///
///
public class InvalidPlugInProxyException : Exception
{
///
/// Initializes a new instance of the class.
///
/// The actual class type of the proxy.
/// The expected interface type of the proxy.
public InvalidPlugInProxyException(Type type, Type expectedType)
{
this.Type = type;
this.ExpectedType = expectedType;
}
///
/// Gets the actual class type of the proxy.
///
///
/// The type.
///
public Type Type { get; }
///
/// Gets the expected interface type of the proxy.
///
public Type ExpectedType { get; }
///
public override string ToString()
{
return $"Unexpected plugin proxy type {this.Type}. Expected one of {this.ExpectedType}";
}
}
}