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
}