diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs new file mode 100644 index 000000000..fd2dbe169 --- /dev/null +++ b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs @@ -0,0 +1,38 @@ +using System; + +namespace Artemis.Core +{ + /// + /// An exception thrown when a plugin prerequisite-related error occurs + /// + public class ArtemisPluginPrerequisiteException : Exception + { + internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite) + { + Plugin = plugin; + PluginPrerequisite = pluginPrerequisite; + } + + internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite, string message) : base(message) + { + Plugin = plugin; + PluginPrerequisite = pluginPrerequisite; + } + + internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite, string message, Exception inner) : base(message, inner) + { + Plugin = plugin; + PluginPrerequisite = pluginPrerequisite; + } + + /// + /// Gets the plugin the error is related to + /// + public Plugin Plugin { get; } + + /// + /// Gets the plugin prerequisite the error is related to + /// + public PluginPrerequisite? PluginPrerequisite { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index 03403cf51..a8dbaf7ad 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -125,6 +125,14 @@ namespace Artemis.Core return Info.ToString(); } + /// + /// Determines whether the prerequisites of this plugin are met + /// + public bool ArePrerequisitesMet() + { + return Prerequisites.All(p => p.IsMet()); + } + /// /// Occurs when the plugin is enabled /// diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs index ea272786b..5267b4d33 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -123,7 +123,7 @@ namespace Artemis.Core /// Called to determine whether the prerequisite is met /// /// if the prerequisite is met; otherwise - public abstract Task IsMet(); + public abstract bool IsMet(); /// /// Called before installation starts diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs index 344d4412d..90d9a787e 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs @@ -10,6 +10,8 @@ namespace Artemis.Core public abstract class PluginPrerequisiteAction : CorePropertyChanged { private bool _progressIndeterminate; + private bool _showProgressBar; + private bool _showSubProgressBar; private string? _status; private bool _subProgressIndeterminate; @@ -56,6 +58,24 @@ namespace Artemis.Core set => SetAndNotify(ref _subProgressIndeterminate, value); } + /// + /// Gets or sets a boolean indicating whether the progress bar should be shown + /// + public bool ShowProgressBar + { + get => _showProgressBar; + set => SetAndNotify(ref _showProgressBar, value); + } + + /// + /// Gets or sets a boolean indicating whether the sub progress bar should be shown + /// + public bool ShowSubProgressBar + { + get => _showSubProgressBar; + set => SetAndNotify(ref _showSubProgressBar, value); + } + /// /// Gets or sets the progress of the action (0 to 100) /// diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs index 7a18f5f08..6602567e0 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs @@ -21,6 +21,9 @@ namespace Artemis.Core { Source = source; Target = target; + + ShowProgressBar = true; + ShowSubProgressBar = true; } /// diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs new file mode 100644 index 000000000..e6324bf8f --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that deletes a file + /// + public class DeleteFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target folder to delete recursively + public DeleteFileAction(string name, string target) : base(name) + { + Target = target; + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + ShowProgressBar = true; + Status = $"Removing {Target}"; + + await Task.Run(() => + { + if (File.Exists(Target)) + File.Delete(Target); + }, cancellationToken); + + ShowProgressBar = false; + Status = $"Removed {Target}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs new file mode 100644 index 000000000..62b4dfc41 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that recursively deletes a folder + /// + public class DeleteFolderAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target folder to delete recursively + public DeleteFolderAction(string name, string target) : base(name) + { + if (Enum.GetValues().Select(Environment.GetFolderPath).Contains(target)) + throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose."); + + Target = target; + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + ShowProgressBar = true; + Status = $"Removing {Target}"; + + await Task.Run(() => + { + if (Directory.Exists(Target)) + Directory.Delete(Target, true); + }, cancellationToken); + + ShowProgressBar = false; + Status = $"Removed {Target}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs new file mode 100644 index 000000000..92c7d0294 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that copies a folder + /// + public class WriteBytesToFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target file to write to (will be created if needed) + /// The contents to write + public WriteBytesToFileAction(string name, string target, byte[] content) : base(name) + { + Target = target; + ByteContent = content ?? throw new ArgumentNullException(nameof(content)); + } + + /// + /// Gets or sets the target file + /// + public string Target { get; } + + /// + /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to + /// the file will be deleted and recreated + /// + public bool Append { get; set; } = false; + + /// + /// Gets the bytes that will be written + /// + public byte[] ByteContent { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string outputDir = Path.GetDirectoryName(Target)!; + Utilities.CreateAccessibleDirectory(outputDir); + + ShowProgressBar = true; + Status = $"Writing to {Path.GetFileName(Target)}..."; + + if (!Append && File.Exists(Target)) + File.Delete(Target); + + await using Stream fileStream = File.OpenWrite(Target); + await using MemoryStream sourceStream = new(ByteContent); + await sourceStream.CopyToAsync(sourceStream.Length, fileStream, Progress, cancellationToken); + + ShowProgressBar = false; + Status = $"Finished writing to {Path.GetFileName(Target)}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs similarity index 55% rename from src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteToFileAction.cs rename to src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs index 0dedc3f28..e4e6a9875 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteToFileAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -8,7 +9,7 @@ namespace Artemis.Core /// /// Represents a plugin prerequisite action that copies a folder /// - public class WriteToFileAction : PluginPrerequisiteAction + public class WriteStringToFileAction : PluginPrerequisiteAction { /// /// Creates a new instance of a copy folder action @@ -16,22 +17,12 @@ namespace Artemis.Core /// The name of the action /// The target file to write to (will be created if needed) /// The contents to write - public WriteToFileAction(string name, string target, string content) : base(name) - { - Target = target ?? throw new ArgumentNullException(nameof(target)); - Content = content ?? throw new ArgumentNullException(nameof(content)); - } - - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The target file to write to (will be created if needed) - /// The contents to write - public WriteToFileAction(string name, string target, byte[] content) : base(name) + public WriteStringToFileAction(string name, string target, string content) : base(name) { Target = target; - ByteContent = content ?? throw new ArgumentNullException(nameof(content)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + + ProgressIndeterminate = true; } /// @@ -40,30 +31,31 @@ namespace Artemis.Core public string Target { get; } /// - /// Gets the contents that will be written + /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to + /// the file will be deleted and recreated /// - public string? Content { get; } + public bool Append { get; set; } = false; /// - /// Gets the bytes that will be written + /// Gets the string that will be written /// - public byte[]? ByteContent { get; } - + public string Content { get; } + /// public override async Task Execute(CancellationToken cancellationToken) { string outputDir = Path.GetDirectoryName(Target)!; Utilities.CreateAccessibleDirectory(outputDir); - ProgressIndeterminate = true; + ShowProgressBar = true; Status = $"Writing to {Path.GetFileName(Target)}..."; - if (Content != null) + if (Append) + await File.AppendAllTextAsync(Target, Content, cancellationToken); + else await File.WriteAllTextAsync(Target, Content, cancellationToken); - else if (ByteContent != null) - await File.WriteAllBytesAsync(Target, ByteContent, cancellationToken); - ProgressIndeterminate = false; + ShowProgressBar = false; Status = $"Finished writing to {Path.GetFileName(Target)}"; } } diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 9fcd8829a..a46c1e3fb 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -241,7 +241,16 @@ namespace Artemis.Core.Services } foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) - EnablePlugin(plugin, false, ignorePluginLock); + { + try + { + EnablePlugin(plugin, false, ignorePluginLock); + } + catch (ArtemisPluginPrerequisiteException) + { + _logger.Warning("Skipped enabling plugin {plugin} because not all prerequisites are met", plugin); + } + } _logger.Debug("Enabled {count} plugin(s)", _plugins.Count(p => p.IsEnabled)); // ReSharper restore InconsistentlySynchronizedField @@ -372,6 +381,9 @@ namespace Artemis.Core.Services return; } + if (!plugin.ArePrerequisitesMet()) + throw new ArtemisPluginPrerequisiteException(plugin, null, "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)); OnPluginEnabling(new PluginEventArgs(plugin)); diff --git a/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml b/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml index 9fced07e6..a65e58422 100644 --- a/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml +++ b/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml @@ -1,14 +1,18 @@ - + d:DataContext="{d:DesignInstance {x:Type dialogs:ConfirmDialogViewModel}}"> + + + + Content="{Binding CancelText}" + Visibility="{Binding CancelText, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}" /> - - + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs index f025488c1..e3aa2f065 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs @@ -10,23 +10,24 @@ using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.Plugins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; -using MaterialDesignThemes.Wpf; using Ninject; using Stylet; -using Constants = Artemis.Core.Constants; namespace Artemis.UI.Screens.Settings.Tabs.Plugins { public class PluginSettingsViewModel : Conductor.Collection.AllActive { + private readonly ICoreService _coreService; private readonly IDialogService _dialogService; + private readonly IMessageService _messageService; private readonly IPluginManagementService _pluginManagementService; private readonly ISettingsVmFactory _settingsVmFactory; - private readonly ICoreService _coreService; - private readonly IMessageService _messageService; private readonly IWindowManager _windowManager; private bool _enabling; private Plugin _plugin; + private bool _isSettingsPopupOpen; + private bool _canInstallPrerequisites; + private bool _canRemovePrerequisites; public PluginSettingsViewModel(Plugin plugin, ISettingsVmFactory settingsVmFactory, @@ -70,6 +71,28 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins set => Task.Run(() => UpdateEnabled(value)); } + public bool IsSettingsPopupOpen + { + get => _isSettingsPopupOpen; + set + { + if (!SetAndNotify(ref _isSettingsPopupOpen, value)) return; + CheckPrerequisites(); + } + } + + public bool CanInstallPrerequisites + { + get => _canInstallPrerequisites; + set => SetAndNotify(ref _canInstallPrerequisites, value); + } + + public bool CanRemovePrerequisites + { + get => _canRemovePrerequisites; + set => SetAndNotify(ref _canRemovePrerequisites, value); + } + public void OpenSettings() { PluginConfigurationDialog configurationViewModel = (PluginConfigurationDialog) Plugin.ConfigurationDialog; @@ -88,6 +111,49 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } + public void OpenPluginDirectory() + { + try + { + Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Plugin.Directory.FullName); + } + catch (Exception e) + { + _dialogService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e); + } + } + + public async Task Reload() + { + bool wasEnabled = IsEnabled; + + _pluginManagementService.UnloadPlugin(Plugin); + Items.Clear(); + + Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory); + foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features) + Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false)); + + if (wasEnabled) + await UpdateEnabled(true); + } + + public async Task InstallPrerequisites() + { + if (Plugin.Prerequisites.Any()) + await ShowPrerequisitesDialog(false); + } + + public async Task RemovePrerequisites() + { + if (Plugin.Prerequisites.Any(p => p.UninstallActions.Any())) + { + await ShowPrerequisitesDialog(true); + NotifyOfPropertyChange(nameof(IsEnabled)); + NotifyOfPropertyChange(nameof(CanOpenSettings)); + } + } + public async Task RemoveSettings() { bool confirmed = await _dialogService.ShowConfirmDialog("Clear plugin settings", "Are you sure you want to clear the settings of this plugin?"); @@ -109,7 +175,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins public async Task Remove() { - bool confirmed = await _dialogService.ShowConfirmDialog("Delete plugin", "Are you sure you want to delete this plugin?"); + bool confirmed = await _dialogService.ShowConfirmDialog("Remove plugin", "Are you sure you want to remove this plugin?"); if (!confirmed) return; @@ -170,10 +236,10 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } // Check if all prerequisites are met async - if (!await ArePrerequisitesMetAsync()) + if (!Plugin.ArePrerequisitesMet()) { - await _dialogService.ShowDialog(new Dictionary {{"pluginOrFeature", Plugin}}); - if (!await ArePrerequisitesMetAsync()) + await ShowPrerequisitesDialog(false); + if (!Plugin.ArePrerequisitesMet()) { CancelEnable(); return; @@ -194,9 +260,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } else - { _pluginManagementService.DisablePlugin(Plugin, true); - } NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(CanOpenSettings)); @@ -209,19 +273,17 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins NotifyOfPropertyChange(nameof(CanOpenSettings)); } - private async Task ArePrerequisitesMetAsync() + private void CheckPrerequisites() { - bool needsPrerequisites = false; - foreach (PluginPrerequisite pluginPrerequisite in Plugin.Prerequisites) - { - if (await pluginPrerequisite.IsMet()) - continue; + CanInstallPrerequisites = Plugin.Prerequisites.Any(); + CanRemovePrerequisites = Plugin.Prerequisites.Any(p => p.UninstallActions.Any()); + } - needsPrerequisites = true; - break; - } - - return !needsPrerequisites; + private async Task ShowPrerequisitesDialog(bool uninstall) + { + if (uninstall) + return await _dialogService.ShowDialog(new Dictionary { { "pluginOrFeature", Plugin } }); + return await _dialogService.ShowDialog(new Dictionary { { "pluginOrFeature", Plugin } }); } } } \ No newline at end of file