mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 03:48:56 +00:00
Plugins - Implemented features
Core - Removed Stylet dependency for #500
This commit is contained in:
1 parent
b56966c585
commit
e812929215
139 files changed
+1635
-1106
No files matched your search
@@ -40,7 +40,7 @@ namespace Artemis.Core.Services
|
||||
IRgbService rgbService, ISurfaceService surfaceService, IProfileService profileService, IModuleService moduleService)
|
||||
{
|
||||
Kernel = kernel;
|
||||
Constants.CorePluginInfo.Kernel = kernel;
|
||||
Constants.CorePlugin.Kernel = kernel;
|
||||
|
||||
_logger = logger;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
@@ -133,8 +133,8 @@ namespace Artemis.Core.Services
|
||||
|
||||
private void UpdatePluginCache()
|
||||
{
|
||||
_modules = _pluginManagementService.GetPluginsOfType<Module>().Where(p => p.IsEnabled).ToList();
|
||||
_dataModelExpansions = _pluginManagementService.GetPluginsOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled).ToList();
|
||||
_modules = _pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled).ToList();
|
||||
_dataModelExpansions = _pluginManagementService.GetFeaturesOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled).ToList();
|
||||
}
|
||||
|
||||
private void ConfigureJsonConvert()
|
||||
|
||||
+61
-32
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Artemis.Core.DeviceProviders;
|
||||
using RGB.NET.Core;
|
||||
|
||||
namespace Artemis.Core.Services
|
||||
@@ -33,69 +34,77 @@ namespace Artemis.Core.Services
|
||||
void UnloadPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// Loads the plugin defined in the provided <see cref="PluginInfo" />
|
||||
/// Loads the plugin located in the provided <paramref name="directory" />
|
||||
/// </summary>
|
||||
/// <param name="pluginInfo">The plugin info defining the plugin to load</param>
|
||||
void LoadPlugin(DirectoryInfo pluginInfo);
|
||||
/// <param name="directory">The directory where the plugin is located</param>
|
||||
Plugin LoadPlugin(DirectoryInfo directory);
|
||||
|
||||
/// <summary>
|
||||
/// Unloads the plugin defined in the provided <see cref="PluginInfo" />
|
||||
/// Enables the provided <paramref name="plugin" />
|
||||
/// </summary>
|
||||
/// <param name="pluginInfo">The plugin info defining the plugin to unload</param>
|
||||
void UnloadPlugin(PluginInfo pluginInfo);
|
||||
/// <param name="plugin">The plugin to enable</param>
|
||||
void EnablePlugin(Plugin plugin, bool ignorePluginLock = false);
|
||||
|
||||
/// <summary>
|
||||
/// Enables the provided plugin
|
||||
/// Unloads the provided <paramref name="plugin" />
|
||||
/// </summary>
|
||||
/// <param name="pluginImplementation"></param>
|
||||
/// <param name="plugin">The plugin to unload</param>
|
||||
void UnloadPlugin(Plugin plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Disables the provided <paramref name="plugin" />
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin to disable</param>
|
||||
void DisablePlugin(Plugin plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Enables the provided plugin feature
|
||||
/// </summary>
|
||||
/// <param name="pluginFeature"></param>
|
||||
/// <param name="isAutoEnable">If true, fails if there is a lock file present</param>
|
||||
void EnablePluginImplementation(PluginImplementation pluginImplementation, bool isAutoEnable = false);
|
||||
void EnablePluginFeature(PluginFeature pluginFeature, bool isAutoEnable = false);
|
||||
|
||||
/// <summary>
|
||||
/// Disables the provided plugin
|
||||
/// Disables the provided plugin feature
|
||||
/// </summary>
|
||||
/// <param name="pluginImplementation"></param>
|
||||
void DisablePluginImplementation(PluginImplementation pluginImplementation);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the plugin info related to the plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginImplementation">The plugin you want to find the plugin info for</param>
|
||||
/// <returns>The plugins PluginInfo</returns>
|
||||
PluginInfo GetPluginInfo(PluginImplementation pluginImplementation);
|
||||
/// <param name="pluginFeature"></param>
|
||||
void DisablePluginFeature(PluginFeature pluginFeature);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin info of all loaded plugins
|
||||
/// </summary>
|
||||
/// <returns>A list containing all the plugin info</returns>
|
||||
List<PluginInfo> GetAllPluginInfo();
|
||||
List<Plugin> GetAllPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// Finds all enabled <see cref="PluginImplementation" /> instances of <typeparamref name="T" />
|
||||
/// Finds all enabled <see cref="PluginFeature" /> instances of <typeparamref name="T" />
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Either <see cref="PluginImplementation" /> or a plugin type implementing <see cref="PluginImplementation" /></typeparam>
|
||||
/// <returns>Returns a list of plugin instances of <typeparamref name="T" /></returns>
|
||||
List<T> GetPluginsOfType<T>() where T : PluginImplementation;
|
||||
/// <typeparam name="T">
|
||||
/// Either <see cref="PluginFeature" /> or a plugin type implementing
|
||||
/// <see cref="PluginFeature" />
|
||||
/// </typeparam>
|
||||
/// <returns>Returns a list of feature instances of <typeparamref name="T" /></returns>
|
||||
List<T> GetFeaturesOfType<T>() where T : PluginFeature;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin that provided the specified assembly
|
||||
/// </summary>
|
||||
/// <param name="assembly"></param>
|
||||
/// <returns></returns>
|
||||
PluginImplementation GetPluginByAssembly(Assembly assembly);
|
||||
Plugin GetPluginByAssembly(Assembly assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the plugin info of the current call stack
|
||||
/// </summary>
|
||||
/// <returns>If the current call stack contains a plugin, the plugin. Otherwise null</returns>
|
||||
Plugin? GetCallingPlugin();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin that defined the specified device
|
||||
/// </summary>
|
||||
/// <param name="device"></param>
|
||||
/// <returns></returns>
|
||||
PluginImplementation GetPluginByDevice(IRGBDevice device);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the plugin info of the current call stack
|
||||
/// </summary>
|
||||
/// <returns>If the current call stack contains a plugin, the plugin. Otherwise null</returns>
|
||||
PluginImplementation GetCallingPlugin();
|
||||
DeviceProvider GetDeviceProviderByDevice(IRGBDevice device);
|
||||
|
||||
#region Events
|
||||
|
||||
@@ -134,6 +143,26 @@ namespace Artemis.Core.Services
|
||||
/// </summary>
|
||||
event EventHandler<PluginEventArgs> PluginDisabled;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a plugin feature is being enabled
|
||||
/// </summary>
|
||||
public event EventHandler<PluginFeatureEventArgs> PluginFeatureEnabling;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a plugin feature has been enabled
|
||||
/// </summary>
|
||||
public event EventHandler<PluginFeatureEventArgs> PluginFeatureEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a plugin feature could not be enabled
|
||||
/// </summary>
|
||||
public event EventHandler<PluginFeatureEventArgs> PluginFeatureEnableFailed;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a plugin feature has been disabled
|
||||
/// </summary>
|
||||
public event EventHandler<PluginFeatureEventArgs> PluginFeatureDisabled;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -26,16 +26,129 @@ namespace Artemis.Core.Services
|
||||
_moduleRepository = moduleRepository;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_profileService = profileService;
|
||||
_pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginManagementEnabled;
|
||||
_pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled;
|
||||
|
||||
Timer activationUpdateTimer = new Timer(2000);
|
||||
activationUpdateTimer.Start();
|
||||
activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed;
|
||||
|
||||
foreach (Module module in _pluginManagementService.GetPluginsOfType<Module>())
|
||||
foreach (Module module in _pluginManagementService.GetFeaturesOfType<Module>())
|
||||
InitialiseOrApplyPriority(module);
|
||||
}
|
||||
|
||||
private async void ActivationUpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
await UpdateModuleActivation();
|
||||
}
|
||||
|
||||
private async Task ActivateModule(Module module)
|
||||
{
|
||||
try
|
||||
{
|
||||
module.Activate(false);
|
||||
|
||||
// If this is a profile module, activate the last active profile after module activation
|
||||
if (module is ProfileModule profileModule)
|
||||
await _profileService.ActivateLastProfileAnimated(profileModule);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(new ArtemisPluginFeatureException(
|
||||
module, "Failed to activate module and last profile.", e), "Failed to activate module and last profile"
|
||||
);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeactivateModule(Module module)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If this is a profile module, animate profile disable
|
||||
// module.Deactivate would do the same but without animation
|
||||
if (module.IsActivated && module is ProfileModule profileModule)
|
||||
await profileModule.ChangeActiveProfileAnimated(null, null);
|
||||
|
||||
module.Deactivate(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(new ArtemisPluginFeatureException(
|
||||
module, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile"
|
||||
);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OverrideActivate(Module module)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (module.IsActivated)
|
||||
return;
|
||||
|
||||
// If activating while it should be deactivated, its an override
|
||||
bool shouldBeActivated = module.EvaluateActivationRequirements();
|
||||
module.Activate(!shouldBeActivated);
|
||||
|
||||
// If this is a profile module, activate the last active profile after module activation
|
||||
if (module is ProfileModule profileModule)
|
||||
_profileService.ActivateLastProfile(profileModule);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(new ArtemisPluginFeatureException(
|
||||
module, "Failed to activate module and last profile.", e), "Failed to activate module and last profile"
|
||||
);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OverrideDeactivate(Module module, bool clearingOverride)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!module.IsActivated)
|
||||
return;
|
||||
|
||||
// If deactivating while it should be activated, its an override
|
||||
bool shouldBeActivated = module.EvaluateActivationRequirements();
|
||||
// No need to deactivate if it is not in an overridden state
|
||||
if (shouldBeActivated && !module.IsActivatedOverride && !clearingOverride)
|
||||
return;
|
||||
|
||||
module.Deactivate(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(new ArtemisPluginFeatureException(
|
||||
module, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile"
|
||||
);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e)
|
||||
{
|
||||
if (e.PluginFeature is Module module)
|
||||
InitialiseOrApplyPriority(module);
|
||||
}
|
||||
|
||||
private void InitialiseOrApplyPriority(Module module)
|
||||
{
|
||||
ModulePriorityCategory category = module.DefaultPriorityCategory;
|
||||
int priority = 1;
|
||||
|
||||
module.Entity = _moduleRepository.GetByModuleId(module.Id);
|
||||
if (module.Entity != null)
|
||||
{
|
||||
category = (ModulePriorityCategory) module.Entity.PriorityCategory;
|
||||
priority = module.Entity.Priority;
|
||||
}
|
||||
|
||||
UpdateModulePriority(module, category, priority);
|
||||
}
|
||||
|
||||
public Module ActiveModuleOverride { get; private set; }
|
||||
|
||||
public async Task SetActiveModuleOverride(Module overrideModule)
|
||||
@@ -46,17 +159,19 @@ namespace Artemis.Core.Services
|
||||
|
||||
if (ActiveModuleOverride == overrideModule)
|
||||
return;
|
||||
|
||||
|
||||
if (overrideModule != null)
|
||||
{
|
||||
OverrideActivate(overrideModule);
|
||||
_logger.Information($"Setting active module override to {overrideModule.DisplayName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Information("Clearing active module override");
|
||||
}
|
||||
|
||||
// Always deactivate all other modules whenever override is called
|
||||
List<Module> modules = _pluginManagementService.GetPluginsOfType<Module>().ToList();
|
||||
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList();
|
||||
foreach (Module module in modules.Where(m => m != overrideModule))
|
||||
OverrideDeactivate(module, overrideModule != null);
|
||||
|
||||
@@ -83,13 +198,8 @@ namespace Artemis.Core.Services
|
||||
// the principle is different for this service but not for the module
|
||||
bool shouldBeActivated = ActiveModuleOverride.EvaluateActivationRequirements();
|
||||
if (shouldBeActivated && ActiveModuleOverride.IsActivatedOverride)
|
||||
{
|
||||
ActiveModuleOverride.Reactivate(true, false);
|
||||
}
|
||||
else if (!shouldBeActivated && !ActiveModuleOverride.IsActivatedOverride)
|
||||
{
|
||||
ActiveModuleOverride.Reactivate(false, true);
|
||||
}
|
||||
else if (!shouldBeActivated && !ActiveModuleOverride.IsActivatedOverride) ActiveModuleOverride.Reactivate(false, true);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -97,10 +207,9 @@ namespace Artemis.Core.Services
|
||||
Stopwatch stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
List<Module> modules = _pluginManagementService.GetPluginsOfType<Module>().ToList();
|
||||
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList();
|
||||
List<Task> tasks = new List<Task>();
|
||||
foreach (Module module in modules)
|
||||
{
|
||||
lock (module)
|
||||
{
|
||||
bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled;
|
||||
@@ -109,7 +218,6 @@ namespace Artemis.Core.Services
|
||||
else if (!shouldBeActivated && module.IsActivated)
|
||||
tasks.Add(DeactivateModule(module));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
@@ -128,7 +236,12 @@ namespace Artemis.Core.Services
|
||||
if (module.PriorityCategory == category && module.Priority == priority)
|
||||
return;
|
||||
|
||||
List<Module> modules = _pluginManagementService.GetPluginsOfType<Module>().Where(m => m.PriorityCategory == category).OrderBy(m => m.Priority).ToList();
|
||||
List<Module> modules = _pluginManagementService
|
||||
.GetFeaturesOfType<Module>()
|
||||
.Where(m => m.PriorityCategory == category)
|
||||
.OrderBy(m => m.Priority)
|
||||
.ToList();
|
||||
|
||||
if (modules.Contains(module))
|
||||
modules.Remove(module);
|
||||
|
||||
@@ -149,110 +262,5 @@ namespace Artemis.Core.Services
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void ActivationUpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
await UpdateModuleActivation();
|
||||
}
|
||||
|
||||
private async Task ActivateModule(Module module)
|
||||
{
|
||||
try
|
||||
{
|
||||
module.Activate(false);
|
||||
|
||||
// If this is a profile module, activate the last active profile after module activation
|
||||
if (module is ProfileModule profileModule)
|
||||
await _profileService.ActivateLastProfileAnimated(profileModule);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(new ArtemisPluginException(module.PluginInfo, "Failed to activate module and last profile.", e), "Failed to activate module and last profile");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeactivateModule(Module module)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If this is a profile module, animate profile disable
|
||||
// module.Deactivate would do the same but without animation
|
||||
if (module.IsActivated && module is ProfileModule profileModule)
|
||||
await profileModule.ChangeActiveProfileAnimated(null, null);
|
||||
|
||||
module.Deactivate(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(new ArtemisPluginException(module.PluginInfo, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OverrideActivate(Module module)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (module.IsActivated)
|
||||
return;
|
||||
|
||||
// If activating while it should be deactivated, its an override
|
||||
bool shouldBeActivated = module.EvaluateActivationRequirements();
|
||||
module.Activate(!shouldBeActivated);
|
||||
|
||||
// If this is a profile module, activate the last active profile after module activation
|
||||
if (module is ProfileModule profileModule)
|
||||
_profileService.ActivateLastProfile(profileModule);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(new ArtemisPluginException(module.PluginInfo, "Failed to activate module and last profile.", e), "Failed to activate module and last profile");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OverrideDeactivate(Module module, bool clearingOverride)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!module.IsActivated)
|
||||
return;
|
||||
|
||||
// If deactivating while it should be activated, its an override
|
||||
bool shouldBeActivated = module.EvaluateActivationRequirements();
|
||||
// No need to deactivate if it is not in an overridden state
|
||||
if (shouldBeActivated && !module.IsActivatedOverride && !clearingOverride)
|
||||
return;
|
||||
|
||||
module.Deactivate(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(new ArtemisPluginException(module.PluginInfo, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void PluginManagementServiceOnPluginManagementEnabled(object sender, PluginEventArgs e)
|
||||
{
|
||||
if (e.PluginInfo.Plugin is Module module)
|
||||
InitialiseOrApplyPriority(module);
|
||||
}
|
||||
|
||||
private void InitialiseOrApplyPriority(Module module)
|
||||
{
|
||||
ModulePriorityCategory category = module.DefaultPriorityCategory;
|
||||
int priority = 1;
|
||||
|
||||
module.Entity = _moduleRepository.GetByPluginGuid(module.PluginInfo.Guid);
|
||||
if (module.Entity != null)
|
||||
{
|
||||
category = (ModulePriorityCategory) module.Entity.PriorityCategory;
|
||||
priority = module.Entity.Priority;
|
||||
}
|
||||
|
||||
UpdateModulePriority(module, category, priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
+244
-150
@@ -41,6 +41,21 @@ namespace Artemis.Core.Services
|
||||
Directory.CreateDirectory(Constants.DataFolder + "plugins");
|
||||
}
|
||||
|
||||
private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive)
|
||||
{
|
||||
DirectoryInfo pluginDirectory = new DirectoryInfo(Path.Combine(Constants.DataFolder, "plugins", Path.GetFileNameWithoutExtension(zipFileInfo.Name)));
|
||||
bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
|
||||
|
||||
// Remove the old directory if it exists
|
||||
if (Directory.Exists(pluginDirectory.FullName))
|
||||
pluginDirectory.DeleteRecursively();
|
||||
Directory.CreateDirectory(pluginDirectory.FullName);
|
||||
|
||||
zipArchive.ExtractToDirectory(pluginDirectory.FullName, true);
|
||||
if (createLockFile)
|
||||
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
|
||||
}
|
||||
|
||||
public bool LoadingPlugins { get; private set; }
|
||||
|
||||
#region Built in plugins
|
||||
@@ -72,7 +87,9 @@ namespace Artemis.Core.Services
|
||||
// Find the matching plugin in the plugin folder
|
||||
DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == Path.GetFileNameWithoutExtension(zipFile.Name));
|
||||
if (match == null)
|
||||
{
|
||||
CopyBuiltInPlugin(zipFile, archive);
|
||||
}
|
||||
else
|
||||
{
|
||||
string metadataFile = Path.Combine(match.FullName, "plugin.json");
|
||||
@@ -105,6 +122,48 @@ namespace Artemis.Core.Services
|
||||
|
||||
#endregion
|
||||
|
||||
public List<Plugin> GetAllPlugins()
|
||||
{
|
||||
return new List<Plugin>(_plugins);
|
||||
}
|
||||
|
||||
public List<T> GetFeaturesOfType<T>() where T : PluginFeature
|
||||
{
|
||||
return _plugins.Where(p => p.IsEnabled).SelectMany(p => p.Features.Where(i => i.IsEnabled && i is T)).Cast<T>().ToList();
|
||||
}
|
||||
|
||||
public Plugin? GetPluginByAssembly(Assembly assembly)
|
||||
{
|
||||
return _plugins.FirstOrDefault(p => p.Assembly == assembly);
|
||||
}
|
||||
|
||||
// TODO: move to a more appropriate service
|
||||
public DeviceProvider GetDeviceProviderByDevice(IRGBDevice rgbDevice)
|
||||
{
|
||||
return GetFeaturesOfType<DeviceProvider>().First(d => d.RgbDeviceProvider.Devices != null && d.RgbDeviceProvider.Devices.Contains(rgbDevice));
|
||||
}
|
||||
|
||||
public Plugin? GetCallingPlugin()
|
||||
{
|
||||
StackTrace stackTrace = new StackTrace(); // get call stack
|
||||
StackFrame[] stackFrames = stackTrace.GetFrames(); // get method calls (frames)
|
||||
|
||||
foreach (StackFrame stackFrame in stackFrames)
|
||||
{
|
||||
Assembly assembly = stackFrame.GetMethod().DeclaringType.Assembly;
|
||||
Plugin plugin = GetPluginByAssembly(assembly);
|
||||
if (plugin != null)
|
||||
return plugin;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
UnloadPlugins();
|
||||
}
|
||||
|
||||
#region Plugins
|
||||
|
||||
public void LoadPlugins(bool ignorePluginLock)
|
||||
@@ -125,7 +184,8 @@ namespace Artemis.Core.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadPlugin(subDirectory);
|
||||
Plugin plugin = LoadPlugin(subDirectory);
|
||||
EnablePlugin(plugin, ignorePluginLock);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -133,22 +193,6 @@ namespace Artemis.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
// Activate plugins after they are all loaded
|
||||
foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled))
|
||||
{
|
||||
foreach (PluginImplementation pluginImplementation in plugin.Implementations.Where(i => i.Entity.IsEnabled))
|
||||
{
|
||||
try
|
||||
{
|
||||
EnablePluginImplementation(pluginImplementation, !ignorePluginLock);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored, logged in EnablePlugin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoadingPlugins = false;
|
||||
}
|
||||
}
|
||||
@@ -165,29 +209,32 @@ namespace Artemis.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadPlugin(DirectoryInfo pluginDirectory)
|
||||
public Plugin LoadPlugin(DirectoryInfo directory)
|
||||
{
|
||||
lock (_plugins)
|
||||
{
|
||||
_logger.Debug("Loading plugin from {directory}", pluginDirectory.FullName);
|
||||
_logger.Debug("Loading plugin from {directory}", directory.FullName);
|
||||
|
||||
// Load the metadata
|
||||
string metadataFile = Path.Combine(pluginDirectory.FullName, "plugin.json");
|
||||
string metadataFile = Path.Combine(directory.FullName, "plugin.json");
|
||||
if (!File.Exists(metadataFile))
|
||||
_logger.Warning(new ArtemisPluginException("Couldn't find the plugins metadata file at " + metadataFile), "Plugin exception");
|
||||
|
||||
// PluginInfo contains the ID which we need to move on
|
||||
PluginInfo pluginInfo = JsonConvert.DeserializeObject<PluginInfo>(File.ReadAllText(metadataFile));
|
||||
|
||||
if (pluginInfo.Guid == Constants.CorePluginInfo.Guid)
|
||||
throw new ArtemisPluginException($"Plugin cannot use reserved GUID {pluginInfo.Guid}");
|
||||
|
||||
// Ensure the plugin is not already loaded
|
||||
if (_plugins.Any(p => p.Guid == pluginInfo.Guid))
|
||||
throw new ArtemisCoreException("Cannot load a plugin that is already loaded");
|
||||
|
||||
Plugin plugin = new Plugin(pluginInfo, pluginDirectory);
|
||||
Plugin plugin = new Plugin(pluginInfo, directory);
|
||||
OnPluginLoading(new PluginEventArgs(plugin));
|
||||
|
||||
// Load the entity and fall back on creating a new one
|
||||
plugin.Entity = _pluginRepository.GetPluginByGuid(pluginInfo.Guid) ?? new PluginEntity { Id = plugin.Guid, IsEnabled = true };
|
||||
plugin.Entity = _pluginRepository.GetPluginByGuid(pluginInfo.Guid) ?? new PluginEntity {Id = plugin.Guid, IsEnabled = true};
|
||||
|
||||
// Locate the main assembly entry
|
||||
string? mainFile = plugin.ResolveRelativePath(plugin.Info.Main);
|
||||
@@ -210,55 +257,94 @@ namespace Artemis.Core.Services
|
||||
throw new ArtemisPluginException(plugin, "Failed to load the plugins assembly", e);
|
||||
}
|
||||
|
||||
// Get the Plugin implementation from the main assembly and if there is only one, instantiate it
|
||||
List<Type> implementationTypes;
|
||||
try
|
||||
{
|
||||
implementationTypes = plugin.Assembly.GetTypes().Where(t => typeof(PluginImplementation).IsAssignableFrom(t)).ToList();
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
throw new ArtemisPluginException(plugin, "Failed to initialize the plugin assembly", new AggregateException(e.LoaderExceptions));
|
||||
}
|
||||
|
||||
// Create the Ninject child kernel and load the module
|
||||
plugin.Kernel = new ChildKernel(_kernel);
|
||||
plugin.Kernel.Load(new PluginModule(pluginInfo));
|
||||
|
||||
if (!implementationTypes.Any())
|
||||
_logger.Warning("Plugin {plugin} contains no implementations", plugin);
|
||||
|
||||
// Create instances of each implementation and add them to the plugin
|
||||
// Construction should be simple and not contain any logic so failure at this point means the entire plugin fails
|
||||
foreach (Type implementationType in implementationTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
PluginImplementation instance = (PluginImplementation)plugin.Kernel.Get(implementationType);
|
||||
plugin.AddImplementation(instance);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArtemisPluginException(plugin, "Failed to load instantiate implementation", e);
|
||||
}
|
||||
}
|
||||
List<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList();
|
||||
if (bootstrappers.Count > 1)
|
||||
_logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}");
|
||||
if (bootstrappers.Any())
|
||||
plugin.Bootstrapper = (IPluginBootstrapper?) Activator.CreateInstance(bootstrappers.First());
|
||||
|
||||
_plugins.Add(plugin);
|
||||
|
||||
OnPluginLoaded(new PluginEventArgs(plugin));
|
||||
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
|
||||
public void EnablePlugin(Plugin plugin, bool ignorePluginLock)
|
||||
{
|
||||
if (plugin.Assembly == null)
|
||||
throw new ArtemisPluginException(plugin, "Cannot enable a plugin that hasn't successfully been loaded");
|
||||
|
||||
// Create the Ninject child kernel and load the module
|
||||
plugin.Kernel = new ChildKernel(_kernel);
|
||||
plugin.Kernel.Bind<Plugin>().ToConstant(plugin);
|
||||
plugin.Kernel.Load(new PluginModule(plugin.Info));
|
||||
|
||||
plugin.SetEnabled(true);
|
||||
|
||||
// Get the Plugin feature from the main assembly and if there is only one, instantiate it
|
||||
List<Type> featureTypes;
|
||||
try
|
||||
{
|
||||
featureTypes = plugin.Assembly.GetTypes().Where(t => typeof(PluginFeature).IsAssignableFrom(t)).ToList();
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
throw new ArtemisPluginException(plugin, "Failed to initialize the plugin assembly", new AggregateException(e.LoaderExceptions));
|
||||
}
|
||||
|
||||
if (!featureTypes.Any())
|
||||
_logger.Warning("Plugin {plugin} contains no features", plugin);
|
||||
|
||||
// Create instances of each feature and add them to the plugin
|
||||
// Construction should be simple and not contain any logic so failure at this point means the entire plugin fails
|
||||
foreach (Type featureType in featureTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Include Plugin as a parameter for the PluginSettingsProvider
|
||||
IParameter[] parameters = {new Parameter("Plugin", plugin, false)};
|
||||
PluginFeature instance = (PluginFeature) plugin.Kernel.Get(featureType, parameters);
|
||||
plugin.AddFeature(instance);
|
||||
|
||||
// Load the enabled state and if not found, default to true
|
||||
instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ??
|
||||
new PluginFeatureEntity {IsEnabled = true, Type = featureType.FullName};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArtemisPluginException(plugin, "Failed to load instantiate feature", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Activate plugins after they are all loaded
|
||||
foreach (PluginFeature pluginFeature in plugin.Features.Where(i => i.Entity.IsEnabled))
|
||||
{
|
||||
try
|
||||
{
|
||||
EnablePluginFeature(pluginFeature, !ignorePluginLock);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored, logged in EnablePluginFeature
|
||||
}
|
||||
}
|
||||
|
||||
OnPluginEnabled(new PluginEventArgs(plugin));
|
||||
}
|
||||
|
||||
public void UnloadPlugin(Plugin plugin)
|
||||
{
|
||||
lock (_plugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
plugin.SetEnabled(false);
|
||||
DisablePlugin(plugin);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception e)
|
||||
{
|
||||
// TODO: Log these
|
||||
_logger.Warning(new ArtemisPluginException(plugin, "Exception during DisablePlugin call for UnloadPlugin", e), "Failed to unload plugin");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -267,38 +353,59 @@ namespace Artemis.Core.Services
|
||||
|
||||
plugin.Dispose();
|
||||
_plugins.Remove(plugin);
|
||||
|
||||
OnPluginUnloaded(new PluginEventArgs(plugin));
|
||||
}
|
||||
}
|
||||
|
||||
public void DisablePlugin(Plugin plugin)
|
||||
{
|
||||
if (!plugin.IsEnabled)
|
||||
return;
|
||||
|
||||
while (plugin.Features.Any())
|
||||
{
|
||||
PluginFeature feature = plugin.Features[0];
|
||||
plugin.RemoveFeature(feature);
|
||||
OnPluginFeatureDisabled(new PluginFeatureEventArgs(feature));
|
||||
}
|
||||
|
||||
plugin.SetEnabled(false);
|
||||
|
||||
plugin.Kernel.Dispose();
|
||||
plugin.Kernel = null;
|
||||
|
||||
OnPluginDisabled(new PluginEventArgs(plugin));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Features
|
||||
|
||||
#region Implementations
|
||||
|
||||
public void EnablePluginImplementation(PluginImplementation pluginImplementation, bool isAutoEnable = false)
|
||||
public void EnablePluginFeature(PluginFeature pluginFeature, bool isAutoEnable = false)
|
||||
{
|
||||
_logger.Debug("Enabling plugin implementation {implementation} - {plugin}", pluginImplementation, pluginImplementation.Plugin);
|
||||
_logger.Debug("Enabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin);
|
||||
|
||||
lock (_plugins)
|
||||
{
|
||||
OnPluginFeatureEnabling(new PluginFeatureEventArgs(pluginFeature));
|
||||
try
|
||||
{
|
||||
// A device provider may be queued for disable on next restart, this undoes that
|
||||
if (pluginImplementation is DeviceProvider && pluginImplementation.IsEnabled && !pluginImplementation.PluginInfo.IsEnabled)
|
||||
if (pluginFeature is DeviceProvider && pluginFeature.IsEnabled && !pluginFeature.Entity.IsEnabled)
|
||||
{
|
||||
pluginImplementation.PluginInfo.IsEnabled = true;
|
||||
pluginImplementation.PluginInfo.ApplyToEntity();
|
||||
_pluginRepository.SavePlugin(pluginImplementation.PluginInfo.PluginEntity);
|
||||
pluginFeature.Entity.IsEnabled = true;
|
||||
SavePlugin(pluginFeature.Plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
pluginImplementation.SetEnabled(true, isAutoEnable);
|
||||
pluginFeature.SetEnabled(true, isAutoEnable);
|
||||
pluginFeature.Entity.IsEnabled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warning(new ArtemisPluginException(pluginImplementation.PluginInfo, "Exception during SetEnabled(true)", e), "Failed to enable plugin");
|
||||
_logger.Warning(
|
||||
new ArtemisPluginException(pluginFeature.Plugin, $"Exception during SetEnabled(true) on {pluginFeature}", e),
|
||||
"Failed to enable plugin"
|
||||
);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
@@ -306,108 +413,83 @@ namespace Artemis.Core.Services
|
||||
// On an auto-enable, ensure PluginInfo.Enabled is true even if enable failed, that way a failure on auto-enable does
|
||||
// not affect the user's settings
|
||||
if (isAutoEnable)
|
||||
pluginImplementation.PluginInfo.IsEnabled = true;
|
||||
pluginFeature.Entity.IsEnabled = true;
|
||||
|
||||
pluginImplementation.PluginInfo.ApplyToEntity();
|
||||
_pluginRepository.SavePlugin(pluginImplementation.PluginInfo.PluginEntity);
|
||||
SavePlugin(pluginFeature.Plugin);
|
||||
|
||||
if (pluginImplementation.PluginInfo.IsEnabled)
|
||||
_logger.Debug("Successfully enabled plugin {pluginInfo}", pluginImplementation.PluginInfo);
|
||||
if (pluginFeature.IsEnabled)
|
||||
{
|
||||
_logger.Debug("Successfully enabled plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin);
|
||||
OnPluginFeatureEnabled(new PluginFeatureEventArgs(pluginFeature));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnPluginImplementationEnabled(new PluginImplementationEventArgs(pluginImplementation));
|
||||
}
|
||||
|
||||
public void DisablePluginImplementation(PluginImplementation pluginImplementation)
|
||||
public void DisablePluginFeature(PluginFeature pluginFeature)
|
||||
{
|
||||
lock (_plugins)
|
||||
{
|
||||
_logger.Debug("Disabling plugin {pluginInfo}", pluginImplementation.PluginInfo);
|
||||
|
||||
// Device providers cannot be disabled at runtime simply queue a disable for next restart
|
||||
if (pluginImplementation is DeviceProvider)
|
||||
try
|
||||
{
|
||||
// Don't call SetEnabled(false) but simply update enabled state and save it
|
||||
pluginImplementation.PluginInfo.IsEnabled = false;
|
||||
pluginImplementation.PluginInfo.ApplyToEntity();
|
||||
_pluginRepository.SavePlugin(pluginImplementation.PluginInfo.PluginEntity);
|
||||
return;
|
||||
_logger.Debug("Disabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin);
|
||||
|
||||
// Device providers cannot be disabled at runtime simply queue a disable for next restart
|
||||
if (pluginFeature is DeviceProvider)
|
||||
{
|
||||
// Don't call SetEnabled(false) but simply update enabled state and save it
|
||||
pluginFeature.Entity.IsEnabled = false;
|
||||
SavePlugin(pluginFeature.Plugin);
|
||||
return;
|
||||
}
|
||||
|
||||
pluginFeature.SetEnabled(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
pluginFeature.Entity.IsEnabled = false;
|
||||
SavePlugin(pluginFeature.Plugin);
|
||||
|
||||
pluginImplementation.SetEnabled(false);
|
||||
pluginImplementation.PluginInfo.ApplyToEntity();
|
||||
_pluginRepository.SavePlugin(pluginImplementation.PluginInfo.PluginEntity);
|
||||
|
||||
_logger.Debug("Successfully disabled plugin {pluginInfo}", pluginImplementation.PluginInfo);
|
||||
if (!pluginFeature.IsEnabled)
|
||||
{
|
||||
_logger.Debug("Successfully disabled plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin);
|
||||
OnPluginFeatureDisabled(new PluginFeatureEventArgs(pluginFeature));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnPluginDisabled(new PluginEventArgs(pluginImplementation.PluginInfo));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storage
|
||||
|
||||
public PluginInfo GetPluginInfo(PluginImplementation pluginImplementation)
|
||||
private void SavePlugin(Plugin plugin)
|
||||
{
|
||||
return _plugins.FirstOrDefault(p => p.Plugin == pluginImplementation);
|
||||
}
|
||||
|
||||
public List<PluginInfo> GetAllPluginInfo()
|
||||
{
|
||||
return new List<PluginInfo>(_plugins);
|
||||
}
|
||||
|
||||
public List<T> GetPluginsOfType<T>() where T : PluginImplementation
|
||||
{
|
||||
return _plugins.Where(p => p.IsEnabled && p.Plugin is T).Select(p => (T) p.Plugin).ToList();
|
||||
}
|
||||
|
||||
public PluginImplementation GetPluginByAssembly(Assembly assembly)
|
||||
{
|
||||
return _plugins.FirstOrDefault(p => p.Assembly == assembly)?.Plugin;
|
||||
}
|
||||
|
||||
public PluginImplementation GetPluginByDevice(IRGBDevice rgbDevice)
|
||||
{
|
||||
return GetPluginsOfType<DeviceProvider>().First(d => d.RgbDeviceProvider.Devices != null && d.RgbDeviceProvider.Devices.Contains(rgbDevice));
|
||||
}
|
||||
|
||||
public PluginImplementation GetCallingPlugin()
|
||||
{
|
||||
StackTrace stackTrace = new StackTrace(); // get call stack
|
||||
StackFrame[] stackFrames = stackTrace.GetFrames(); // get method calls (frames)
|
||||
|
||||
foreach (StackFrame stackFrame in stackFrames)
|
||||
foreach (PluginFeature pluginFeature in plugin.Features)
|
||||
{
|
||||
Assembly assembly = stackFrame.GetMethod().DeclaringType.Assembly;
|
||||
PluginImplementation pluginImplementation = GetPluginByAssembly(assembly);
|
||||
if (pluginImplementation != null)
|
||||
return pluginImplementation;
|
||||
if (plugin.Entity.Features.All(i => i.Type != pluginFeature.GetType().FullName))
|
||||
plugin.Entity.Features.Add(pluginFeature.Entity);
|
||||
}
|
||||
|
||||
return null;
|
||||
_pluginRepository.SavePlugin(plugin.Entity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
private PluginFeatureEntity GetOrCreateFeatureEntity(PluginFeature feature)
|
||||
{
|
||||
UnloadPlugins();
|
||||
return feature.Plugin.Entity.Features.FirstOrDefault(i => i.Type == feature.GetType().FullName) ??
|
||||
new PluginFeatureEntity {IsEnabled = true, Type = feature.GetType().FullName};
|
||||
}
|
||||
|
||||
private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive)
|
||||
{
|
||||
DirectoryInfo pluginDirectory = new DirectoryInfo(Path.Combine(Constants.DataFolder, "plugins", Path.GetFileNameWithoutExtension(zipFileInfo.Name)));
|
||||
bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
|
||||
|
||||
// Remove the old directory if it exists
|
||||
if (Directory.Exists(pluginDirectory.FullName))
|
||||
pluginDirectory.DeleteRecursively();
|
||||
Directory.CreateDirectory(pluginDirectory.FullName);
|
||||
|
||||
zipArchive.ExtractToDirectory(pluginDirectory.FullName, true);
|
||||
if (createLockFile)
|
||||
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
@@ -419,8 +501,10 @@ namespace Artemis.Core.Services
|
||||
public event EventHandler<PluginEventArgs> PluginEnabled;
|
||||
public event EventHandler<PluginEventArgs> PluginDisabled;
|
||||
|
||||
public event EventHandler<PluginImplementationEventArgs> PluginImplementationEnabled;
|
||||
public event EventHandler<PluginImplementationEventArgs> PluginImplementationDisabled;
|
||||
public event EventHandler<PluginFeatureEventArgs> PluginFeatureEnabling;
|
||||
public event EventHandler<PluginFeatureEventArgs> PluginFeatureEnabled;
|
||||
public event EventHandler<PluginFeatureEventArgs> PluginFeatureDisabled;
|
||||
public event EventHandler<PluginFeatureEventArgs> PluginFeatureEnableFailed;
|
||||
|
||||
protected virtual void OnCopyingBuildInPlugins()
|
||||
{
|
||||
@@ -457,14 +541,24 @@ namespace Artemis.Core.Services
|
||||
PluginDisabled?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnPluginImplementationDisabled(PluginImplementationEventArgs e)
|
||||
protected virtual void OnPluginFeatureEnabling(PluginFeatureEventArgs e)
|
||||
{
|
||||
PluginImplementationDisabled?.Invoke(this, e);
|
||||
PluginFeatureEnabling?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnPluginImplementationEnabled(PluginImplementationEventArgs e)
|
||||
protected virtual void OnPluginFeatureEnabled(PluginFeatureEventArgs e)
|
||||
{
|
||||
PluginImplementationEnabled?.Invoke(this, e);
|
||||
PluginFeatureEnabled?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnPluginFeatureDisabled(PluginFeatureEventArgs e)
|
||||
{
|
||||
PluginFeatureDisabled?.Invoke(this, e);
|
||||
}
|
||||
|
||||
protected virtual void OnPluginFeatureEnableFailed(PluginFeatureEventArgs e)
|
||||
{
|
||||
PluginFeatureEnableFailed?.Invoke(this, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -12,14 +12,14 @@ namespace Artemis.Core.Services
|
||||
RegisterBuiltInConditionOperators();
|
||||
}
|
||||
|
||||
public ConditionOperatorRegistration RegisterConditionOperator(PluginInfo pluginInfo, BaseConditionOperator conditionOperator)
|
||||
public ConditionOperatorRegistration RegisterConditionOperator(Plugin plugin, BaseConditionOperator conditionOperator)
|
||||
{
|
||||
if (pluginInfo == null)
|
||||
throw new ArgumentNullException(nameof(pluginInfo));
|
||||
if (plugin == null)
|
||||
throw new ArgumentNullException(nameof(plugin));
|
||||
if (conditionOperator == null)
|
||||
throw new ArgumentNullException(nameof(conditionOperator));
|
||||
|
||||
conditionOperator.PluginInfo = pluginInfo;
|
||||
conditionOperator.Plugin = plugin;
|
||||
return ConditionOperatorStore.Add(conditionOperator);
|
||||
}
|
||||
|
||||
@@ -43,30 +43,30 @@ namespace Artemis.Core.Services
|
||||
private void RegisterBuiltInConditionOperators()
|
||||
{
|
||||
// General usage for any type
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new EqualsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new NotEqualConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new EqualsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new NotEqualConditionOperator());
|
||||
|
||||
// Numeric operators
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new NumberEqualsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new NumberNotEqualConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new LessThanConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new LessThanOrEqualConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanOrEqualConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new NumberEqualsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new NumberNotEqualConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new LessThanConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new GreaterThanConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new LessThanOrEqualConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new GreaterThanOrEqualConditionOperator());
|
||||
|
||||
// String operators
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new StringEqualsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new StringNotEqualConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new StringContainsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new StringNotContainsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new StringStartsWithConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new StringEndsWithConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new StringMatchesRegexConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new StringEqualsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new StringNotEqualConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new StringContainsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new StringNotContainsConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new StringStartsWithConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new StringEndsWithConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new StringMatchesRegexConditionOperator());
|
||||
|
||||
// Null checks, at the bottom
|
||||
// TODO: Implement a priority mechanism
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new NullConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePluginInfo, new NotNullConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new NullConditionOperator());
|
||||
RegisterConditionOperator(Constants.CorePlugin, new NotNullConditionOperator());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,14 @@ namespace Artemis.Core.Services
|
||||
RegisterBuiltInModifiers();
|
||||
}
|
||||
|
||||
public DataBindingModifierTypeRegistration RegisterModifierType(PluginInfo pluginInfo, BaseDataBindingModifierType dataBindingModifierType)
|
||||
public DataBindingModifierTypeRegistration RegisterModifierType(Plugin plugin, BaseDataBindingModifierType dataBindingModifierType)
|
||||
{
|
||||
if (pluginInfo == null)
|
||||
throw new ArgumentNullException(nameof(pluginInfo));
|
||||
if (plugin == null)
|
||||
throw new ArgumentNullException(nameof(plugin));
|
||||
if (dataBindingModifierType == null)
|
||||
throw new ArgumentNullException(nameof(dataBindingModifierType));
|
||||
|
||||
dataBindingModifierType.PluginInfo = pluginInfo;
|
||||
dataBindingModifierType.Plugin = plugin;
|
||||
return DataBindingModifierTypeStore.Add(dataBindingModifierType);
|
||||
}
|
||||
|
||||
@@ -43,41 +43,41 @@ namespace Artemis.Core.Services
|
||||
private void RegisterBuiltInModifiers()
|
||||
{
|
||||
// Numbers - General
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SumModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SubtractModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new MultiplicationModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new DivideModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new PercentageOfModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SumModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SubtractModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new MultiplicationModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new DivideModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new PercentageOfModifierType());
|
||||
|
||||
// Numbers - Advanced
|
||||
RegisterModifierType(Constants.CorePluginInfo, new MaxModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new MinModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new ModuloModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new AbsoluteModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new PowerModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SquareRootModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new MaxModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new MinModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new ModuloModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new AbsoluteModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new PowerModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SquareRootModifierType());
|
||||
|
||||
// Numbers - Rounding
|
||||
RegisterModifierType(Constants.CorePluginInfo, new FloorModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new RoundModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new CeilingModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new FloorModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new RoundModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new CeilingModifierType());
|
||||
|
||||
// Numbers - Trigonometric
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SineModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new CosineModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new TangentModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new CotangentModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SecantModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new CosecantModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SineModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new CosineModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new TangentModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new CotangentModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SecantModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new CosecantModifierType());
|
||||
|
||||
// Colors
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SKColorSumModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SKColorSaturateModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SKColorDesaturateModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SKColorBrightenModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SKColorDarkenModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SKColorRotateHueModifierType());
|
||||
RegisterModifierType(Constants.CorePluginInfo, new SKColorInvertModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SKColorSumModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SKColorSaturateModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SKColorDesaturateModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SKColorBrightenModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SKColorDarkenModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SKColorRotateHueModifierType());
|
||||
RegisterModifierType(Constants.CorePlugin, new SKColorInvertModifierType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,16 @@ namespace Artemis.Core.Services
|
||||
public DataModelService(IPluginManagementService pluginManagementService)
|
||||
{
|
||||
// Add data models of already loaded plugins
|
||||
foreach (Module module in pluginManagementService.GetPluginsOfType<Module>().Where(p => p.IsEnabled))
|
||||
foreach (Module module in pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled))
|
||||
AddModuleDataModel(module);
|
||||
foreach (BaseDataModelExpansion dataModelExpansion in pluginManagementService.GetPluginsOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled))
|
||||
foreach (BaseDataModelExpansion dataModelExpansion in pluginManagementService.GetFeaturesOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled))
|
||||
AddDataModelExpansionDataModel(dataModelExpansion);
|
||||
|
||||
// Add data models of new plugins when they get enabled
|
||||
pluginManagementService.PluginEnabled += PluginServiceOnPluginEnabled;
|
||||
pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled;
|
||||
}
|
||||
|
||||
|
||||
public DataModelRegistration RegisterDataModel(DataModel dataModel)
|
||||
{
|
||||
if (dataModel == null)
|
||||
@@ -44,21 +45,16 @@ namespace Artemis.Core.Services
|
||||
return (T) DataModelStore.GetAll().FirstOrDefault(d => d.DataModel is T)?.DataModel;
|
||||
}
|
||||
|
||||
public DataModel GetPluginDataModel(PluginImplementation pluginImplementation)
|
||||
public DataModel? GetPluginDataModel(PluginFeature pluginFeature)
|
||||
{
|
||||
return DataModelStore.Get(pluginImplementation.PluginInfo.Guid)?.DataModel;
|
||||
return DataModelStore.Get(pluginFeature.Id)?.DataModel;
|
||||
}
|
||||
|
||||
public DataModel GetPluginDataModel(Guid pluginGuid)
|
||||
|
||||
private void OnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e)
|
||||
{
|
||||
return DataModelStore.Get(pluginGuid)?.DataModel;
|
||||
}
|
||||
|
||||
private void PluginServiceOnPluginEnabled(object sender, PluginEventArgs e)
|
||||
{
|
||||
if (e.PluginInfo.Plugin is Module module)
|
||||
if (e.PluginFeature is Module module)
|
||||
AddModuleDataModel(module);
|
||||
else if (e.PluginInfo.Plugin is BaseDataModelExpansion dataModelExpansion)
|
||||
else if (e.PluginFeature is BaseDataModelExpansion dataModelExpansion)
|
||||
AddDataModelExpansionDataModel(dataModelExpansion);
|
||||
}
|
||||
|
||||
@@ -68,7 +64,7 @@ namespace Artemis.Core.Services
|
||||
return;
|
||||
|
||||
if (module.InternalDataModel.DataModelDescription == null)
|
||||
throw new ArtemisPluginException(module.PluginInfo, "Module overrides GetDataModelDescription but returned null");
|
||||
throw new ArtemisPluginFeatureException(module, "Module overrides GetDataModelDescription but returned null");
|
||||
|
||||
module.InternalDataModel.IsExpansion = module.InternalExpandsMainDataModel;
|
||||
RegisterDataModel(module.InternalDataModel);
|
||||
@@ -77,7 +73,7 @@ namespace Artemis.Core.Services
|
||||
private void AddDataModelExpansionDataModel(BaseDataModelExpansion dataModelExpansion)
|
||||
{
|
||||
if (dataModelExpansion.InternalDataModel.DataModelDescription == null)
|
||||
throw new ArtemisPluginException(dataModelExpansion.PluginInfo, "Data model expansion overrides GetDataModelDescription but returned null");
|
||||
throw new ArtemisPluginFeatureException(dataModelExpansion, "Data model expansion overrides GetDataModelDescription but returned null");
|
||||
|
||||
dataModelExpansion.InternalDataModel.IsExpansion = true;
|
||||
RegisterDataModel(dataModelExpansion.InternalDataModel);
|
||||
|
||||
@@ -12,9 +12,9 @@ namespace Artemis.Core.Services
|
||||
/// <summary>
|
||||
/// Registers a new condition operator for use in layer conditions
|
||||
/// </summary>
|
||||
/// <param name="pluginInfo">The PluginInfo of the plugin this condition operator belongs to</param>
|
||||
/// <param name="plugin">The plugin this condition operator belongs to</param>
|
||||
/// <param name="conditionOperator">The condition operator to register</param>
|
||||
ConditionOperatorRegistration RegisterConditionOperator([NotNull] PluginInfo pluginInfo, [NotNull] BaseConditionOperator conditionOperator);
|
||||
ConditionOperatorRegistration RegisterConditionOperator([NotNull] Plugin plugin, [NotNull] BaseConditionOperator conditionOperator);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a condition operator so it is no longer available for use in layer conditions
|
||||
|
||||
@@ -12,9 +12,9 @@ namespace Artemis.Core.Services
|
||||
/// <summary>
|
||||
/// Registers a new modifier type for use in data bindings
|
||||
/// </summary>
|
||||
/// <param name="pluginInfo">The PluginInfo of the plugin this modifier type belongs to</param>
|
||||
/// <param name="plugin">The plugin this modifier type belongs to</param>
|
||||
/// <param name="dataBindingModifierType">The modifier type to register</param>
|
||||
DataBindingModifierTypeRegistration RegisterModifierType([NotNull] PluginInfo pluginInfo, [NotNull] BaseDataBindingModifierType dataBindingModifierType);
|
||||
DataBindingModifierTypeRegistration RegisterModifierType([NotNull] Plugin plugin, [NotNull] BaseDataBindingModifierType dataBindingModifierType);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a modifier type so it is no longer available for use in data bindings
|
||||
|
||||
@@ -34,13 +34,7 @@ namespace Artemis.Core.Services
|
||||
/// <summary>
|
||||
/// If found, returns the data model of the provided plugin
|
||||
/// </summary>
|
||||
/// <param name="pluginImplementation">The plugin to find the data model of</param>
|
||||
DataModel GetPluginDataModel(PluginImplementation pluginImplementation);
|
||||
|
||||
/// <summary>
|
||||
/// If found, returns the data model of the provided plugin GUID
|
||||
/// </summary>
|
||||
/// <param name="pluginGuid">The GUID of the plugin to find the data model of</param>
|
||||
DataModel GetPluginDataModel(Guid pluginGuid);
|
||||
/// <param name="pluginFeature">The plugin to find the data model of</param>
|
||||
DataModel? GetPluginDataModel(PluginFeature pluginFeature);
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,11 @@ namespace Artemis.Core.Services
|
||||
{
|
||||
PluginSetting<LayerBrushReference> defaultReference = _settingsService.GetSetting("ProfileEditor.DefaultLayerBrushDescriptor", new LayerBrushReference
|
||||
{
|
||||
BrushPluginGuid = Guid.Parse("92a9d6ba-6f7a-4937-94d5-c1d715b4141a"),
|
||||
LayerBrushProviderId = "Artemis.Plugins.LayerBrushes.Color.ColorBrushProvider-92a9d6ba",
|
||||
BrushType = "ColorBrush"
|
||||
});
|
||||
|
||||
return LayerBrushStore.Get(defaultReference.Value.BrushPluginGuid, defaultReference.Value.BrushType)?.LayerBrushDescriptor;
|
||||
return LayerBrushStore.Get(defaultReference.Value.LayerBrushProviderId, defaultReference.Value.BrushType)?.LayerBrushDescriptor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Artemis.Core.Services
|
||||
|
||||
internal SettingsService(IPluginRepository pluginRepository)
|
||||
{
|
||||
_pluginSettings = new PluginSettings(Constants.CorePluginInfo, pluginRepository);
|
||||
_pluginSettings = new PluginSettings(Constants.CorePlugin, pluginRepository);
|
||||
}
|
||||
|
||||
public PluginSetting<T> GetSetting<T>(string name, T defaultValue = default)
|
||||
|
||||
@@ -38,13 +38,13 @@ namespace Artemis.Core.Services
|
||||
|
||||
public List<ProfileDescriptor> GetProfileDescriptors(ProfileModule module)
|
||||
{
|
||||
List<ProfileEntity> profileEntities = _profileRepository.GetByPluginGuid(module.PluginInfo.Guid);
|
||||
List<ProfileEntity> profileEntities = _profileRepository.GetByModuleId(module.Id);
|
||||
return profileEntities.Select(e => new ProfileDescriptor(module, e)).ToList();
|
||||
}
|
||||
|
||||
public ProfileDescriptor CreateProfileDescriptor(ProfileModule module, string name)
|
||||
{
|
||||
ProfileEntity profileEntity = new ProfileEntity {Id = Guid.NewGuid(), Name = name, PluginGuid = module.PluginInfo.Guid};
|
||||
ProfileEntity profileEntity = new ProfileEntity {Id = Guid.NewGuid(), Name = name, ModuleId = module.Id};
|
||||
_profileRepository.Add(profileEntity);
|
||||
|
||||
return new ProfileDescriptor(module, profileEntity);
|
||||
@@ -258,7 +258,7 @@ namespace Artemis.Core.Services
|
||||
|
||||
public ProfileDescriptor GetLastActiveProfile(ProfileModule module)
|
||||
{
|
||||
List<ProfileEntity> moduleProfiles = _profileRepository.GetByPluginGuid(module.PluginInfo.Guid);
|
||||
List<ProfileEntity> moduleProfiles = _profileRepository.GetByModuleId(module.Id);
|
||||
if (!moduleProfiles.Any())
|
||||
return CreateProfileDescriptor(module, "Default");
|
||||
|
||||
@@ -271,7 +271,7 @@ namespace Artemis.Core.Services
|
||||
if (module.ActiveProfile == null)
|
||||
return;
|
||||
|
||||
List<ProfileEntity> profileEntities = _profileRepository.GetByPluginGuid(module.PluginInfo.Guid);
|
||||
List<ProfileEntity> profileEntities = _profileRepository.GetByModuleId(module.Id);
|
||||
foreach (ProfileEntity profileEntity in profileEntities)
|
||||
{
|
||||
profileEntity.IsActive = module.ActiveProfile.EntityId == profileEntity.Id;
|
||||
@@ -285,7 +285,7 @@ namespace Artemis.Core.Services
|
||||
/// <param name="surface"></param>
|
||||
private void ActiveProfilesPopulateLeds(ArtemisSurface surface)
|
||||
{
|
||||
List<ProfileModule> profileModules = _pluginManagementService.GetPluginsOfType<ProfileModule>();
|
||||
List<ProfileModule> profileModules = _pluginManagementService.GetFeaturesOfType<ProfileModule>();
|
||||
foreach (ProfileModule profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList())
|
||||
profileModule.ActiveProfile.PopulateLeds(surface);
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ namespace Artemis.Core.Services
|
||||
// Add all current devices
|
||||
foreach (IRGBDevice rgbDevice in _rgbService.LoadedDevices)
|
||||
{
|
||||
PluginImplementation pluginImplementation = _pluginManagementService.GetPluginByDevice(rgbDevice);
|
||||
configuration.Devices.Add(new ArtemisDevice(rgbDevice, pluginImplementation, configuration));
|
||||
PluginFeature pluginFeature = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
|
||||
configuration.Devices.Add(new ArtemisDevice(rgbDevice, pluginFeature, configuration));
|
||||
}
|
||||
|
||||
lock (_surfaceConfigurations)
|
||||
@@ -136,8 +136,8 @@ namespace Artemis.Core.Services
|
||||
IRGBDevice device = _rgbService.Surface.Devices.FirstOrDefault(d => d.GetDeviceIdentifier() == position.DeviceIdentifier);
|
||||
if (device != null)
|
||||
{
|
||||
PluginImplementation pluginImplementation = _pluginManagementService.GetPluginByDevice(device);
|
||||
surfaceConfiguration.Devices.Add(new ArtemisDevice(device, pluginImplementation, surfaceConfiguration, position));
|
||||
PluginFeature pluginFeature = _pluginManagementService.GetDeviceProviderByDevice(device);
|
||||
surfaceConfiguration.Devices.Add(new ArtemisDevice(device, pluginFeature, surfaceConfiguration, position));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,8 +178,8 @@ namespace Artemis.Core.Services
|
||||
DeviceEntity existingDeviceConfig = surface.SurfaceEntity.DeviceEntities.FirstOrDefault(d => d.DeviceIdentifier == deviceIdentifier);
|
||||
if (existingDeviceConfig != null)
|
||||
{
|
||||
PluginImplementation pluginImplementation = _pluginManagementService.GetPluginByDevice(rgbDevice);
|
||||
device = new ArtemisDevice(rgbDevice, pluginImplementation, surface, existingDeviceConfig);
|
||||
PluginFeature pluginFeature = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
|
||||
device = new ArtemisDevice(rgbDevice, pluginFeature, surface, existingDeviceConfig);
|
||||
}
|
||||
// Fall back on creating a new device
|
||||
else
|
||||
@@ -189,8 +189,8 @@ namespace Artemis.Core.Services
|
||||
rgbDevice.DeviceInfo,
|
||||
deviceIdentifier
|
||||
);
|
||||
PluginImplementation pluginImplementation = _pluginManagementService.GetPluginByDevice(rgbDevice);
|
||||
device = new ArtemisDevice(rgbDevice, pluginImplementation, surface);
|
||||
PluginFeature pluginFeature = _pluginManagementService.GetDeviceProviderByDevice(rgbDevice);
|
||||
device = new ArtemisDevice(rgbDevice, pluginFeature, surface);
|
||||
}
|
||||
|
||||
surface.Devices.Add(device);
|
||||
|
||||
Reference in new issue
Block a user