From 77be79dde5f5e39aa8c5872beedbd6c213ffdc8d Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 2 May 2021 23:00:48 +0200 Subject: [PATCH] Prerequisites - Finalized UI logic surrounding install/remove --- .../ArtemisPluginPrerequisiteException.cs | 24 ++-- src/Artemis.Core/Plugins/Plugin.cs | 1 + src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 13 +- .../PrerequisiteAction/DownloadFileAction.cs | 79 ++++++++++++ .../Services/PluginManagementService.cs | 37 +++--- .../PluginPrerequisitesInstallDialogView.xaml | 4 +- ...uginPrerequisitesInstallDialogViewModel.cs | 40 ++---- ...luginPrerequisitesUninstallDialogView.xaml | 8 +- ...inPrerequisitesUninstallDialogViewModel.cs | 71 +++++----- .../Tabs/Plugins/PluginFeatureViewModel.cs | 52 +++----- .../Tabs/Plugins/PluginSettingsViewModel.cs | 122 +++++++++--------- 11 files changed, 260 insertions(+), 191 deletions(-) create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs 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 Show(IDialogService dialogService, List subjects) + { + return dialogService.ShowDialog(new Dictionary {{"subjects", subjects}}); + } + #region Overrides of Screen /// protected override void OnInitialActivate() { CanInstall = false; - if (PluginInfo != null) - Task.Run(() => CanInstall = !PluginInfo.ArePrerequisitesMet()); - else - Task.Run(() => CanInstall = !FeatureInfo.ArePrerequisitesMet()); + Task.Run(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet())); base.OnInitialActivate(); } diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogView.xaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogView.xaml index cc6abcd40..7e77a7fc9 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogView.xaml +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogView.xaml @@ -24,7 +24,7 @@ - Plugin prerequisites + Plugin/feature prerequisites - This plugin installed certain prerequisites in order to function. - In this screen you can chose to remove these, this will mean the plugin will no longer work until you reinstall the prerequisites. + This plugin/feature installed certain prerequisites in order to function. + In this screen you can chose to remove these, this will mean the plugin/feature will no longer work until you reinstall the prerequisites. + Content="{Binding CancelLabel}" />