// // 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}"; } } }