diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs
index fd2dbe169..e2f3a850d 100644
--- a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs
+++ b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs
@@ -7,32 +7,24 @@ namespace Artemis.Core
///
public class ArtemisPluginPrerequisiteException : Exception
{
- internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite)
+ internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject)
{
- Plugin = plugin;
- PluginPrerequisite = pluginPrerequisite;
+ Subject = subject;
}
- internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite, string message) : base(message)
+ internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message) : base(message)
{
- Plugin = plugin;
- PluginPrerequisite = pluginPrerequisite;
+ Subject = subject;
}
- internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite, string message, Exception inner) : base(message, inner)
+ internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message, Exception inner) : base(message, inner)
{
- Plugin = plugin;
- PluginPrerequisite = pluginPrerequisite;
+ Subject = subject;
}
///
- /// Gets the plugin the error is related to
+ /// Gets the subject the error is related to
///
- public Plugin Plugin { get; }
-
- ///
- /// Gets the plugin prerequisite the error is related to
- ///
- public PluginPrerequisite? PluginPrerequisite { get; }
+ public IPrerequisitesSubject Subject { get; }
}
}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs
index e5e00841c..ddd090bfd 100644
--- a/src/Artemis.Core/Plugins/Plugin.cs
+++ b/src/Artemis.Core/Plugins/Plugin.cs
@@ -25,6 +25,7 @@ namespace Artemis.Core
Info = info;
Directory = directory;
Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true};
+ Info.Plugin = this;
_features = new List();
}
diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
index 9e8be12cf..6806db862 100644
--- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
+++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
@@ -6,6 +6,7 @@ using Artemis.Core.DeviceProviders;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.Modules;
+using Artemis.Storage.Entities.Plugins;
using Humanizer;
using Newtonsoft.Json;
@@ -22,10 +23,11 @@ namespace Artemis.Core
private PluginFeature? _instance;
private string _name = null!;
- internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureAttribute? attribute)
+ internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute)
{
Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType));
+ Entity = pluginFeatureEntity;
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
@@ -48,7 +50,7 @@ namespace Artemis.Core
else
Icon = "Plugin";
}
-
+
internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance)
{
if (instance == null) throw new ArgumentNullException(nameof(instance));
@@ -121,6 +123,11 @@ namespace Artemis.Core
[JsonProperty]
public bool AlwaysEnabled { get; }
+ ///
+ /// Gets a boolean indicating whether the feature is enabled in persistent storage
+ ///
+ public bool EnabledInStorage => Entity.IsEnabled;
+
///
/// Gets the feature this info is associated with
///
@@ -136,6 +143,8 @@ namespace Artemis.Core
///
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
+ internal PluginFeatureEntity Entity { get; }
+
///
public override string ToString()
{
diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs
new file mode 100644
index 000000000..a4daf9532
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs
@@ -0,0 +1,79 @@
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Humanizer;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents a plugin prerequisite action that downloads a file
+ ///
+ public class DownloadFileAction : PluginPrerequisiteAction
+ {
+ ///
+ /// Creates a new instance of a copy folder action
+ ///
+ /// The name of the action
+ /// The source URL to download
+ /// The target file to save as (will be created if needed)
+ public DownloadFileAction(string name, string source, string target) : base(name)
+ {
+ Source = source ?? throw new ArgumentNullException(nameof(source));
+ Target = target ?? throw new ArgumentNullException(nameof(target));
+
+ ShowProgressBar = true;
+ }
+
+ ///
+ /// Gets the source URL to download
+ ///
+ public string Source { get; }
+
+ ///
+ /// Gets the target file to save as (will be created if needed)
+ ///
+ public string Target { get; }
+
+ ///
+ public override async Task Execute(CancellationToken cancellationToken)
+ {
+ using HttpClient client = new();
+ await using FileStream destinationStream = File.Create(Target);
+
+ void ProgressOnProgressReported(object? sender, EventArgs e)
+ {
+ if (Progress.ProgressPerSecond != 0)
+ Status = $"Downloading {Target} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
+ else
+ Status = $"Downloading {Target}";
+ }
+
+ Progress.ProgressReported += ProgressOnProgressReported;
+
+ // Get the http headers first to examine the content length
+ using HttpResponseMessage response = await client.GetAsync(Target, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken);
+ long? contentLength = response.Content.Headers.ContentLength;
+
+ // Ignore progress reporting when no progress reporter was
+ // passed or when the content length is unknown
+ if (!contentLength.HasValue)
+ {
+ ProgressIndeterminate = true;
+ await download.CopyToAsync(destinationStream, Progress, cancellationToken);
+ }
+ else
+ {
+ ProgressIndeterminate = false;
+ await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken);
+ }
+
+ Progress.ProgressReported -= ProgressOnProgressReported;
+
+ Progress.Report((1, 1));
+ Status = "Finished downloading";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs
index fe839e4bc..7b3e6dea1 100644
--- a/src/Artemis.Core/Services/PluginManagementService.cs
+++ b/src/Artemis.Core/Services/PluginManagementService.cs
@@ -340,7 +340,12 @@ namespace Artemis.Core.Services
}
foreach (Type featureType in featureTypes)
- plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
+ {
+ // Load the enabled state and if not found, default to true
+ PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ??
+ new PluginFeatureEntity { IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName! };
+ plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
+ }
if (!featureTypes.Any())
_logger.Warning("Plugin {plugin} contains no features", plugin);
@@ -382,7 +387,7 @@ namespace Artemis.Core.Services
}
if (!plugin.Info.ArePrerequisitesMet())
- throw new ArtemisPluginPrerequisiteException(plugin, null, "Cannot enable a plugin whose prerequisites aren't all met");
+ throw new ArtemisPluginPrerequisiteException(plugin.Info, "Cannot enable a plugin whose prerequisites aren't all met");
// Create the Ninject child kernel and load the module
plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin));
@@ -406,10 +411,7 @@ namespace Artemis.Core.Services
featureInfo.Instance = instance;
instance.Info = featureInfo;
instance.Plugin = plugin;
-
- // Load the enabled state and if not found, default to true
- instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureInfo.FeatureType.FullName) ??
- new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureInfo.FeatureType.FullName!};
+ instance.Entity = featureInfo.Entity;
}
catch (Exception e)
{
@@ -418,17 +420,8 @@ namespace Artemis.Core.Services
}
// Activate features after they are all loaded
- foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.Instance.Entity.IsEnabled || f.AlwaysEnabled)))
- {
- try
- {
- EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock);
- }
- catch (Exception)
- {
- // ignored, logged in EnablePluginFeature
- }
- }
+ foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.EnabledInStorage || f.AlwaysEnabled)))
+ EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock);
if (saveState)
{
@@ -585,7 +578,10 @@ namespace Artemis.Core.Services
if (pluginFeature.Plugin.Info.RequiresAdmin && !_isElevated)
{
if (!saveState)
+ {
+ OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature));
throw new ArtemisCoreException("Cannot enable a feature that requires elevation without saving it's state.");
+ }
pluginFeature.Entity.IsEnabled = true;
pluginFeature.Plugin.Entity.IsEnabled = true;
@@ -596,6 +592,12 @@ namespace Artemis.Core.Services
return;
}
+ if (!pluginFeature.Info.ArePrerequisitesMet())
+ {
+ OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature));
+ throw new ArtemisPluginPrerequisiteException(pluginFeature.Info, "Cannot enable a plugin feature whose prerequisites aren't all met");
+ }
+
try
{
pluginFeature.SetEnabled(true, isAutoEnable);
@@ -608,7 +610,6 @@ namespace Artemis.Core.Services
new ArtemisPluginException(pluginFeature.Plugin, $"Exception during SetEnabled(true) on {pluginFeature}", e),
"Failed to enable plugin"
);
- throw;
}
finally
{
diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogView.xaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogView.xaml
index 5484241ea..d4c933acc 100644
--- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogView.xaml
+++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogView.xaml
@@ -24,7 +24,7 @@
- Plugin prerequisites
+ Plugin/feature prerequisites
- In order for this plugin to function certain prerequisites must be met.
+ In order for this plugin/feature to function certain prerequisites must be met.
On the left side you can see all prerequisites and whether they are currently met or not.
Clicking install prerequisites will automatically set everything up for you.
diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs
index 1f955f691..6b95d93c5 100644
--- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs
+++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs
@@ -4,7 +4,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
-using Artemis.UI.Exceptions;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services;
using MaterialDesignThemes.Wpf;
@@ -15,35 +14,25 @@ namespace Artemis.UI.Screens.Plugins
public class PluginPrerequisitesInstallDialogViewModel : DialogViewModelBase
{
private readonly IDialogService _dialogService;
+ private readonly List _subjects;
private PluginPrerequisiteViewModel _activePrerequisite;
private bool _canInstall;
private bool _isFinished;
private CancellationTokenSource _tokenSource;
- public PluginPrerequisitesInstallDialogViewModel(IPrerequisitesSubject subject, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService)
+ public PluginPrerequisitesInstallDialogViewModel(List subjects, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService)
{
+ _subjects = subjects;
_dialogService = dialogService;
- // Constructor overloading doesn't work very well with Kernel.Get :(
- if (subject is PluginInfo plugin)
- {
- PluginInfo = plugin;
- Prerequisites = new BindableCollection(plugin.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false)));
- }
- else if (subject is PluginFeatureInfo feature)
- {
- FeatureInfo = feature;
- Prerequisites = new BindableCollection(feature.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false)));
- }
- else
- throw new ArtemisUIException($"Expected plugin or feature to be passed to {nameof(PluginPrerequisitesInstallDialogViewModel)}");
+
+ Prerequisites = new BindableCollection();
+ foreach (IPrerequisitesSubject prerequisitesSubject in subjects)
+ Prerequisites.AddRange(prerequisitesSubject.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false)));
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
pluginPrerequisiteViewModel.ConductWith(this);
}
-
- public PluginInfo PluginInfo { get; }
- public PluginFeatureInfo FeatureInfo { get; }
public BindableCollection Prerequisites { get; }
public PluginPrerequisiteViewModel ActivePrerequisite
@@ -64,9 +53,6 @@ namespace Artemis.UI.Screens.Plugins
set => SetAndNotify(ref _isFinished, value);
}
- public bool IsSubjectPlugin => PluginInfo != null;
- public bool IsSubjectFeature => FeatureInfo != null;
-
#region Overrides of DialogViewModelBase
///
@@ -114,7 +100,7 @@ namespace Artemis.UI.Screens.Plugins
"Confirm",
""
);
- await _dialogService.ShowDialog(new Dictionary {{"subject", PluginInfo}});
+ await Show(_dialogService, _subjects);
}
catch (OperationCanceledException)
{
@@ -133,16 +119,18 @@ namespace Artemis.UI.Screens.Plugins
Session?.Close(true);
}
+ public static Task