using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using Artemis.Core.DeviceProviders; using Artemis.Storage.Entities.Plugins; using DryIoc; using McMaster.NETCore.Plugins; namespace Artemis.Core; /// /// Represents a plugin /// public class Plugin : CorePropertyChanged, IDisposable { private readonly List _features; private readonly bool _loadedFromStorage; private readonly List _profilers; private bool _isEnabled; internal Plugin(PluginInfo info, DirectoryInfo directory, PluginEntity? pluginEntity) { Info = info; Directory = directory; Entity = pluginEntity ?? new PluginEntity {Id = Guid}; Info.Plugin = this; _loadedFromStorage = pluginEntity != null; _features = new List(); _profilers = new List(); Features = new ReadOnlyCollection(_features); Profilers = new ReadOnlyCollection(_profilers); } /// /// Gets the plugin GUID /// public Guid Guid => Info.Guid; /// /// Gets the plugin info related to this plugin /// public PluginInfo Info { get; } /// /// The plugins root directory /// public DirectoryInfo Directory { get; } /// /// Gets or sets a configuration dialog for this plugin that is accessible in the UI under Settings > Plugins /// public IPluginConfigurationDialog? ConfigurationDialog { get; set; } /// /// Indicates whether the user enabled the plugin or not /// public bool IsEnabled { get => _isEnabled; private set => SetAndNotify(ref _isEnabled, value); } /// /// Gets a read-only collection of all features this plugin provides /// public ReadOnlyCollection Features { get; } /// /// Gets a read-only collection of profiles running on the plugin /// public ReadOnlyCollection Profilers { get; } /// /// The assembly the plugin code lives in /// public Assembly? Assembly { get; internal set; } /// /// Gets the plugin bootstrapper /// public PluginBootstrapper? Bootstrapper { get; internal set; } /// /// Gets the IOC container of the plugin, only use this for advanced IOC operations, otherwise see and /// public IContainer? Container { get; internal set; } /// /// The PluginLoader backing this plugin /// internal PluginLoader? PluginLoader { get; set; } /// /// The entity representing the plugin /// internal PluginEntity Entity { get; set; } /// /// Populated when plugin settings are first loaded /// internal PluginSettings? Settings { get; set; } /// /// Resolves the relative path provided in the parameter to an absolute path /// /// The path to resolve /// An absolute path pointing to the provided relative path [return: NotNullIfNotNull("path")] public string? ResolveRelativePath(string? path) { return path == null ? null : Path.Combine(Directory.FullName, path); } /// /// Looks up the instance of the feature of type /// /// The type of feature to find /// If found, the instance of the feature public T? GetFeature() where T : PluginFeature { return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T; } /// /// Looks up the feature info the feature of type /// /// The type of feature to find /// Feature info of the feature public PluginFeatureInfo GetFeatureInfo() where T : PluginFeature { // This should be a safe assumption because any type of PluginFeature is registered and added return _features.First(i => i.FeatureType == typeof(T)); } /// /// Gets a profiler with the provided , if it does not yet exist it will be created. /// /// The name of the profiler /// A new or existing profiler with the provided public Profiler GetProfiler(string name) { Profiler? profiler = _profilers.FirstOrDefault(p => p.Name == name); if (profiler != null) return profiler; profiler = new Profiler(this, name); _profilers.Add(profiler); return profiler; } /// /// Removes a profiler from the plugin /// /// The profiler to remove public void RemoveProfiler(Profiler profiler) { _profilers.Remove(profiler); } /// /// Gets an instance of the specified service using the plugins dependency injection container. /// /// Arguments to supply to the service. /// The service to resolve. /// An instance of the service. /// public T Resolve(params object?[] arguments) { if (Container == null) throw new ArtemisPluginException("Cannot use Resolve before the plugin finished loading"); return Container.Resolve(args: arguments); } /// /// Gets an instance of the specified service using the plugins dependency injection container. /// /// The type of service to resolve. /// Arguments to supply to the service. /// An instance of the service. /// public object Resolve(Type type, params object?[] arguments) { if (Container == null) throw new ArtemisPluginException("Cannot use Resolve before the plugin finished loading"); return Container.Resolve(type, args: arguments); } /// /// Registers service of type implemented by type. /// /// The scope in which the service should live, if you are not sure leave it on singleton. /// The service to register. /// The implementation of the service to register. public void Register(PluginServiceScope scope = PluginServiceScope.Singleton) where TImplementation : TService { IReuse reuse = scope switch { PluginServiceScope.Transient => Reuse.Transient, PluginServiceScope.Singleton => Reuse.Singleton, PluginServiceScope.Scoped => Reuse.Scoped, _ => throw new ArgumentOutOfRangeException(nameof(scope), scope, null) }; Container.Register(reuse); } /// /// Registers implementation type with itself as service type. /// /// The scope in which the service should live, if you are not sure leave it on singleton. /// The implementation of the service to register. public void Register(PluginServiceScope scope = PluginServiceScope.Singleton) { IReuse reuse = scope switch { PluginServiceScope.Transient => Reuse.Transient, PluginServiceScope.Singleton => Reuse.Singleton, PluginServiceScope.Scoped => Reuse.Scoped, _ => throw new ArgumentOutOfRangeException(nameof(scope), scope, null) }; Container.Register(reuse); } /// public override string ToString() { return Info.ToString(); } /// /// Occurs when the plugin is enabled /// public event EventHandler? Enabled; /// /// Occurs when the plugin is disabled /// public event EventHandler? Disabled; /// /// Occurs when an feature is loaded and added to the plugin /// public event EventHandler? FeatureAdded; /// /// Occurs when an feature is disabled and removed from the plugin /// public event EventHandler? FeatureRemoved; /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// /// to release both managed and unmanaged resources; /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { if (disposing) { foreach (PluginFeatureInfo feature in Features) feature.Instance?.Dispose(); SetEnabled(false); Container?.Dispose(); PluginLoader?.Dispose(); GC.Collect(); GC.WaitForPendingFinalizers(); _features.Clear(); } } /// /// Invokes the Enabled event /// protected virtual void OnEnabled() { Enabled?.Invoke(this, EventArgs.Empty); } /// /// Invokes the Disabled event /// protected virtual void OnDisabled() { Disabled?.Invoke(this, EventArgs.Empty); } /// /// Invokes the FeatureAdded event /// protected virtual void OnFeatureAdded(PluginFeatureInfoEventArgs e) { FeatureAdded?.Invoke(this, e); } /// /// Invokes the FeatureRemoved event /// protected virtual void OnFeatureRemoved(PluginFeatureInfoEventArgs e) { FeatureRemoved?.Invoke(this, e); } internal void ApplyToEntity() { Entity.Id = Guid; Entity.IsEnabled = IsEnabled; } internal void AddFeature(PluginFeatureInfo featureInfo) { if (featureInfo.Plugin != this) throw new ArtemisCoreException("Feature is not associated with this plugin"); _features.Add(featureInfo); OnFeatureAdded(new PluginFeatureInfoEventArgs(featureInfo)); } internal void RemoveFeature(PluginFeatureInfo featureInfo) { if (featureInfo.Instance != null && featureInfo.Instance.IsEnabled) throw new ArtemisCoreException("Cannot remove an enabled feature from a plugin"); _features.Remove(featureInfo); featureInfo.Instance?.Dispose(); OnFeatureRemoved(new PluginFeatureInfoEventArgs(featureInfo)); } internal void SetEnabled(bool enable) { if (IsEnabled == enable) return; if (!enable && Features.Any(e => e.Instance != null && e.Instance.IsEnabled)) throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features"); IsEnabled = enable; if (enable) { Bootstrapper?.OnPluginEnabled(this); OnEnabled(); } else { Bootstrapper?.OnPluginDisabled(this); OnDisabled(); } } internal bool HasEnabledFeatures() { return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled); } internal void AutoEnableIfNew() { if (_loadedFromStorage) return; // Enabled is preset to true if the plugin meets the following criteria // - Requires no admin rights // - No always-enabled device providers // - Either has no prerequisites or they are all met Entity.IsEnabled = !Info.RequiresAdmin && Features.All(f => !f.AlwaysEnabled || !f.FeatureType.IsAssignableTo(typeof(DeviceProvider))) && Info.ArePrerequisitesMet(); if (!Entity.IsEnabled) return; // Also auto-enable any non-device provider feature foreach (PluginFeatureInfo pluginFeatureInfo in Features) pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } /// /// Represents a scope in which a plugin service is injected by the IOC container. /// public enum PluginServiceScope { /// /// Services in this scope are never reused, a new instance is injected each time. /// Transient, /// /// Services in this scope are reused for as long as the plugin lives, the same instance is injected each time. /// Singleton, /// /// Services in this scope are reused within a container scope, this is an advanced setting you shouldn't need. /// To learn more see the DryIoc docs. /// Scoped }