diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
index f99fab4de..00c64717e 100644
--- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings
+++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings
@@ -53,6 +53,8 @@
True
True
True
+ True
+ True
True
True
True
diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs
new file mode 100644
index 000000000..e2f3a850d
--- /dev/null
+++ b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Artemis.Core
+{
+ ///
+ /// An exception thrown when a plugin prerequisite-related error occurs
+ ///
+ public class ArtemisPluginPrerequisiteException : Exception
+ {
+ internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject)
+ {
+ Subject = subject;
+ }
+
+ internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message) : base(message)
+ {
+ Subject = subject;
+ }
+
+ internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message, Exception inner) : base(message, inner)
+ {
+ Subject = subject;
+ }
+
+ ///
+ /// Gets the subject the error is related to
+ ///
+ public IPrerequisitesSubject Subject { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Extensions/StreamExtensions.cs b/src/Artemis.Core/Extensions/StreamExtensions.cs
new file mode 100644
index 000000000..f4d5b9a5f
--- /dev/null
+++ b/src/Artemis.Core/Extensions/StreamExtensions.cs
@@ -0,0 +1,133 @@
+// Based on: https://www.codeproject.com/Tips/5274597/An-Improved-Stream-CopyToAsync-that-Reports-Progre
+// The MIT License
+//
+// Copyright (c) 2020 honey the codewitch
+//
+// Permission is hereby granted, free of charge,
+// to any person obtaining a copy of this software and
+// associated documentation files (the "Software"), to
+// deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify,
+// merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom
+// the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice
+// shall be included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Artemis.Core
+{
+ internal static class StreamExtensions
+ {
+ private const int DefaultBufferSize = 81920;
+
+ ///
+ /// Copys a stream to another stream
+ ///
+ /// The source to copy from
+ /// The length of the source stream, if known - used for progress reporting
+ /// The destination to copy to
+ /// The size of the copy block buffer
+ /// An implementation for reporting progress
+ /// A cancellation token
+ /// A task representing the operation
+ public static async Task CopyToAsync(
+ this Stream source,
+ long sourceLength,
+ Stream destination,
+ int bufferSize,
+ IProgress<(long, long)> progress,
+ CancellationToken cancellationToken)
+ {
+ if (0 == bufferSize)
+ bufferSize = DefaultBufferSize;
+ byte[]? buffer = new byte[bufferSize];
+ if (0 > sourceLength && source.CanSeek)
+ sourceLength = source.Length - source.Position;
+ long totalBytesCopied = 0L;
+ if (null != progress)
+ progress.Report((totalBytesCopied, sourceLength));
+ int bytesRead = -1;
+ while (0 != bytesRead && !cancellationToken.IsCancellationRequested)
+ {
+ bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
+ if (0 == bytesRead || cancellationToken.IsCancellationRequested)
+ break;
+ await destination.WriteAsync(buffer, 0, buffer.Length, cancellationToken);
+ totalBytesCopied += bytesRead;
+ progress?.Report((totalBytesCopied, sourceLength));
+ }
+
+ if (0 < totalBytesCopied)
+ progress?.Report((totalBytesCopied, sourceLength));
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ ///
+ /// Copys a stream to another stream
+ ///
+ /// The source to copy from
+ /// The length of the source stream, if known - used for progress reporting
+ /// The destination to copy to
+ /// An implementation for reporting progress
+ /// A cancellation token
+ /// A task representing the operation
+ public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
+ {
+ return CopyToAsync(source, sourceLength, destination, 0, progress, cancellationToken);
+ }
+
+ ///
+ /// Copys a stream to another stream
+ ///
+ /// The source to copy from
+ /// The destination to copy to
+ /// An implementation for reporting progress
+ /// A cancellation token
+ /// A task representing the operation
+ public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
+ {
+ return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken);
+ }
+
+ ///
+ /// Copys a stream to another stream
+ ///
+ /// The source to copy from
+ /// The length of the source stream, if known - used for progress reporting
+ /// The destination to copy to
+ /// An implementation for reporting progress
+ /// A task representing the operation
+ public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress)
+ {
+ return CopyToAsync(source, sourceLength, destination, 0, progress, default);
+ }
+
+ ///
+ /// Copys a stream to another stream
+ ///
+ /// The source to copy from
+ /// The destination to copy to
+ /// An implementation for reporting progress
+ /// A task representing the operation
+ public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress)
+ {
+ return CopyToAsync(source, 0L, destination, 0, progress, default);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs b/src/Artemis.Core/Plugins/IPluginBootstrapper.cs
deleted file mode 100644
index af4b607d7..000000000
--- a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace Artemis.Core
-{
- ///
- /// An optional entry point for your plugin
- ///
- public interface IPluginBootstrapper
- {
- ///
- /// Called when the plugin is activated
- ///
- /// The plugin instance of your plugin
- void Enable(Plugin plugin);
-
- ///
- /// Called when the plugin is deactivated or when Artemis shuts down
- ///
- /// The plugin instance of your plugin
- void Disable(Plugin plugin);
- }
-}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs
index da4e3f7aa..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();
}
@@ -71,7 +72,7 @@ namespace Artemis.Core
///
/// Gets the plugin bootstrapper
///
- public IPluginBootstrapper? Bootstrapper { get; internal set; }
+ public PluginBootstrapper? Bootstrapper { get; internal set; }
///
/// The Ninject kernel of the plugin
@@ -113,6 +114,17 @@ namespace Artemis.Core
{
return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T;
}
+
+ ///
+ /// Looks up the feature info the feature of type
+ ///
+ /// The type of feature to find
+ /// Feature info of the feature
+ public PluginFeatureInfo GetFeatureInfo() where T : PluginFeature
+ {
+ // This should be a safe assumption because any type of PluginFeature is registered and added
+ return _features.First(i => i.FeatureType == typeof(T));
+ }
///
public override string ToString()
@@ -235,12 +247,12 @@ namespace Artemis.Core
if (enable)
{
- Bootstrapper?.Enable(this);
+ Bootstrapper?.OnPluginEnabled(this);
OnEnabled();
}
else
{
- Bootstrapper?.Disable(this);
+ Bootstrapper?.OnPluginDisabled(this);
OnDisabled();
}
}
diff --git a/src/Artemis.Core/Plugins/PluginBootstrapper.cs b/src/Artemis.Core/Plugins/PluginBootstrapper.cs
new file mode 100644
index 000000000..0d41ad36d
--- /dev/null
+++ b/src/Artemis.Core/Plugins/PluginBootstrapper.cs
@@ -0,0 +1,100 @@
+namespace Artemis.Core
+{
+ ///
+ /// An optional entry point for your plugin
+ ///
+ public abstract class PluginBootstrapper
+ {
+ private Plugin? _plugin;
+
+ ///
+ /// Called when the plugin is loaded
+ ///
+ ///
+ public virtual void OnPluginLoaded(Plugin plugin)
+ {
+ }
+
+ ///
+ /// Called when the plugin is activated
+ ///
+ /// The plugin instance of your plugin
+ public virtual void OnPluginEnabled(Plugin plugin)
+ {
+ }
+
+ ///
+ /// Called when the plugin is deactivated or when Artemis shuts down
+ ///
+ /// The plugin instance of your plugin
+ public virtual void OnPluginDisabled(Plugin plugin)
+ {
+ }
+
+ ///
+ /// Adds the provided prerequisite to the plugin.
+ ///
+ /// The prerequisite to add
+ public void AddPluginPrerequisite(PluginPrerequisite prerequisite)
+ {
+ // TODO: We can keep track of them and add them after load, same goes for the others
+ if (_plugin == null)
+ throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded");
+
+ if (!_plugin.Info.Prerequisites.Contains(prerequisite))
+ _plugin.Info.Prerequisites.Add(prerequisite);
+ }
+
+ ///
+ /// Removes the provided prerequisite from the plugin.
+ ///
+ /// The prerequisite to remove
+ ///
+ /// is successfully removed; otherwise . This method also returns
+ /// if the prerequisite was not found.
+ ///
+ public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite)
+ {
+ if (_plugin == null)
+ throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded");
+
+ return _plugin.Info.Prerequisites.Remove(prerequisite);
+ }
+
+ ///
+ /// Adds the provided prerequisite to the feature of type .
+ ///
+ /// The prerequisite to add
+ public void AddFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature
+ {
+ if (_plugin == null)
+ throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
+
+ PluginFeatureInfo info = _plugin.GetFeatureInfo();
+ if (!info.Prerequisites.Contains(prerequisite))
+ info.Prerequisites.Add(prerequisite);
+ }
+
+ ///
+ /// Removes the provided prerequisite from the feature of type .
+ ///
+ /// The prerequisite to remove
+ ///
+ /// is successfully removed; otherwise . This method also returns
+ /// if the prerequisite was not found.
+ ///
+ public bool RemoveFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature
+ {
+ if (_plugin == null)
+ throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
+
+ return _plugin.GetFeatureInfo().Prerequisites.Remove(prerequisite);
+ }
+
+ internal void InternalOnPluginLoaded(Plugin plugin)
+ {
+ _plugin = plugin;
+ OnPluginLoaded(plugin);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs
index 8b9943a0a..aa661c951 100644
--- a/src/Artemis.Core/Plugins/PluginFeature.cs
+++ b/src/Artemis.Core/Plugins/PluginFeature.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
@@ -15,7 +16,7 @@ namespace Artemis.Core
private readonly Stopwatch _updateStopwatch = new();
private bool _isEnabled;
private Exception? _loadException;
-
+
///
/// Gets the plugin feature info related to this feature
///
diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
index 47f34cbaa..6806db862 100644
--- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
+++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs
@@ -1,9 +1,12 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using Artemis.Core.DataModelExpansions;
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;
@@ -13,23 +16,24 @@ namespace Artemis.Core
/// Represents basic info about a plugin feature and contains a reference to the instance of said feature
///
[JsonObject(MemberSerialization.OptIn)]
- public class PluginFeatureInfo : CorePropertyChanged
+ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
{
private string? _description;
private string? _icon;
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;
Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
-
+
if (Icon != null) return;
if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType))
Icon = "TableAdd";
@@ -46,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));
@@ -119,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
///
@@ -128,6 +137,14 @@ namespace Artemis.Core
internal set => SetAndNotify(ref _instance, value);
}
+ ///
+ public List Prerequisites { get; } = new();
+
+ ///
+ public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
+
+ internal PluginFeatureEntity Entity { get; }
+
///
public override string ToString()
{
diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs
index fcbeacd07..de8a77e7f 100644
--- a/src/Artemis.Core/Plugins/PluginInfo.cs
+++ b/src/Artemis.Core/Plugins/PluginInfo.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
+using System.Linq;
using Newtonsoft.Json;
namespace Artemis.Core
@@ -8,7 +10,7 @@ namespace Artemis.Core
/// Represents basic info about a plugin and contains a reference to the instance of said plugin
///
[JsonObject(MemberSerialization.OptIn)]
- public class PluginInfo : CorePropertyChanged
+ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
{
private bool _autoEnableFeatures = true;
private string? _description;
@@ -117,6 +119,12 @@ namespace Artemis.Core
internal set => SetAndNotify(ref _plugin, value);
}
+ ///
+ public List Prerequisites { get; } = new();
+
+ ///
+ public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
+
///
public override string ToString()
{
diff --git a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs
new file mode 100644
index 000000000..5401455ed
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents a type that has prerequisites
+ ///
+ public interface IPrerequisitesSubject
+ {
+ ///
+ /// Gets a list of prerequisites for this plugin
+ ///
+ List Prerequisites { get; }
+
+ ///
+ /// Determines whether the prerequisites of this plugin are met
+ ///
+ bool ArePrerequisitesMet();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs
new file mode 100644
index 000000000..a762d7e7c
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents a prerequisite for a or
+ ///
+ public abstract class PluginPrerequisite : CorePropertyChanged
+ {
+ private PluginPrerequisiteAction? _currentAction;
+
+ ///
+ /// Gets the name of the prerequisite
+ ///
+ public abstract string Name { get; }
+
+ ///
+ /// Gets the description of the prerequisite
+ ///
+ public abstract string Description { get; }
+
+ ///
+ /// Gets a boolean indicating whether installing or uninstalling this prerequisite requires admin privileges
+ ///
+ public abstract bool RequiresElevation { get; }
+
+ ///
+ /// Gets a list of actions to execute when is called
+ ///
+ public abstract List InstallActions { get; }
+
+ ///
+ /// Gets a list of actions to execute when is called
+ ///
+ public abstract List UninstallActions { get; }
+
+ ///
+ /// Gets or sets the action currently being executed
+ ///
+ public PluginPrerequisiteAction? CurrentAction
+ {
+ get => _currentAction;
+ private set => SetAndNotify(ref _currentAction, value);
+ }
+
+ ///
+ /// Execute all install actions
+ ///
+ public async Task Install(CancellationToken cancellationToken)
+ {
+ try
+ {
+ OnInstallStarting();
+ foreach (PluginPrerequisiteAction installAction in InstallActions)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ CurrentAction = installAction;
+ await installAction.Execute(cancellationToken);
+ }
+ }
+ finally
+ {
+ CurrentAction = null;
+ OnInstallFinished();
+ }
+ }
+
+ ///
+ /// Execute all uninstall actions
+ ///
+ public async Task Uninstall(CancellationToken cancellationToken)
+ {
+ try
+ {
+ OnUninstallStarting();
+ foreach (PluginPrerequisiteAction uninstallAction in UninstallActions)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ CurrentAction = uninstallAction;
+ await uninstallAction.Execute(cancellationToken);
+ }
+ }
+ finally
+ {
+ CurrentAction = null;
+ OnUninstallFinished();
+ }
+ }
+
+ ///
+ /// Called to determine whether the prerequisite is met
+ ///
+ /// if the prerequisite is met; otherwise
+ public abstract bool IsMet();
+
+ ///
+ /// Called before installation starts
+ ///
+ protected virtual void OnInstallStarting()
+ {
+ }
+
+ ///
+ /// Called after installation finishes
+ ///
+ protected virtual void OnInstallFinished()
+ {
+ }
+
+ ///
+ /// Called before uninstall starts
+ ///
+ protected virtual void OnUninstallStarting()
+ {
+ }
+
+ ///
+ /// Called after uninstall finished
+ ///
+ protected virtual void OnUninstallFinished()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs
new file mode 100644
index 000000000..90d9a787e
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents an action that must be taken to install or uninstall a plugin prerequisite
+ ///
+ public abstract class PluginPrerequisiteAction : CorePropertyChanged
+ {
+ private bool _progressIndeterminate;
+ private bool _showProgressBar;
+ private bool _showSubProgressBar;
+ private string? _status;
+ private bool _subProgressIndeterminate;
+
+ ///
+ /// The base constructor for all plugin prerequisite actions
+ ///
+ /// The name of the action
+ protected PluginPrerequisiteAction(string name)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ }
+
+ #region Implementation of IPluginPrerequisiteAction
+
+ ///
+ /// Gets the name of the action
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets or sets the status of the action
+ ///
+ public string? Status
+ {
+ get => _status;
+ set => SetAndNotify(ref _status, value);
+ }
+
+ ///
+ /// Gets or sets a boolean indicating whether the progress is indeterminate or not
+ ///
+ public bool ProgressIndeterminate
+ {
+ get => _progressIndeterminate;
+ set => SetAndNotify(ref _progressIndeterminate, value);
+ }
+
+ ///
+ /// Gets or sets a boolean indicating whether the progress is indeterminate or not
+ ///
+ public bool SubProgressIndeterminate
+ {
+ get => _subProgressIndeterminate;
+ 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)
+ ///
+ public PrerequisiteActionProgress Progress { get; } = new();
+
+ ///
+ /// Gets or sets the sub progress of the action
+ ///
+ public PrerequisiteActionProgress SubProgress { get; } = new();
+
+ ///
+ /// Called when the action must execute
+ ///
+ public abstract Task Execute(CancellationToken cancellationToken);
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs
new file mode 100644
index 000000000..6602567e0
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs
@@ -0,0 +1,81 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Humanizer;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents a plugin prerequisite action that copies a folder
+ ///
+ public class CopyFolderAction : PluginPrerequisiteAction
+ {
+ ///
+ /// Creates a new instance of a copy folder action
+ ///
+ /// The name of the action
+ /// The source folder to copy
+ /// The target folder to copy to (will be created if needed)
+ public CopyFolderAction(string name, string source, string target) : base(name)
+ {
+ Source = source;
+ Target = target;
+
+ ShowProgressBar = true;
+ ShowSubProgressBar = true;
+ }
+
+ ///
+ /// Gets the source directory
+ ///
+ public string Source { get; }
+
+ ///
+ /// Gets or sets the target directory
+ ///
+ public string Target { get; }
+
+ ///
+ public override async Task Execute(CancellationToken cancellationToken)
+ {
+ DirectoryInfo source = new(Source);
+ DirectoryInfo target = new(Target);
+
+ if (!source.Exists)
+ throw new ArtemisCoreException($"The source directory at '{source}' was not found.");
+
+ int filesCopied = 0;
+ FileInfo[] files = source.GetFiles("*", SearchOption.AllDirectories);
+
+ foreach (FileInfo fileInfo in files)
+ {
+ string outputPath = fileInfo.FullName.Replace(source.FullName, target.FullName);
+ string outputDir = Path.GetDirectoryName(outputPath)!;
+ Utilities.CreateAccessibleDirectory(outputDir);
+
+ void SubProgressOnProgressReported(object? sender, EventArgs e)
+ {
+ if (SubProgress.ProgressPerSecond != 0)
+ Status = $"Copying {fileInfo.Name} - {SubProgress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
+ else
+ Status = $"Copying {fileInfo.Name}";
+ }
+
+ Progress.Report((filesCopied, files.Length));
+ SubProgress.ProgressReported += SubProgressOnProgressReported;
+
+ await using FileStream sourceStream = fileInfo.OpenRead();
+ await using FileStream destinationStream = File.Create(outputPath);
+
+ await sourceStream.CopyToAsync(fileInfo.Length, destinationStream, SubProgress, cancellationToken);
+
+ filesCopied++;
+ SubProgress.ProgressReported -= SubProgressOnProgressReported;
+ }
+
+ Progress.Report((filesCopied, files.Length));
+ Status = $"Finished copying {filesCopied} file(s)";
+ }
+ }
+}
\ No newline at end of file
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/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/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/WriteStringToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs
new file mode 100644
index 000000000..e4e6a9875
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.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 WriteStringToFileAction : 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 WriteStringToFileAction(string name, string target, string content) : base(name)
+ {
+ Target = target;
+ Content = content ?? throw new ArgumentNullException(nameof(content));
+
+ ProgressIndeterminate = true;
+ }
+
+ ///
+ /// 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 string that will be written
+ ///
+ public string Content { 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)
+ await File.AppendAllTextAsync(Target, Content, cancellationToken);
+ else
+ await File.WriteAllTextAsync(Target, Content, cancellationToken);
+
+ ShowProgressBar = false;
+ Status = $"Finished writing to {Path.GetFileName(Target)}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs
new file mode 100644
index 000000000..96b48bde3
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs
@@ -0,0 +1,91 @@
+using System;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents progress on a plugin prerequisite action
+ ///
+ public class PrerequisiteActionProgress : CorePropertyChanged, IProgress<(long, long)>
+ {
+ private long _current;
+ private DateTime _lastReport;
+ private double _percentage;
+ private double _progressPerSecond;
+ private long _total;
+ private long _lastReportValue;
+
+ ///
+ /// The current amount
+ ///
+ public long Current
+ {
+ get => _current;
+ set => SetAndNotify(ref _current, value);
+ }
+
+ ///
+ /// The total amount
+ ///
+ public long Total
+ {
+ get => _total;
+ set => SetAndNotify(ref _total, value);
+ }
+
+ ///
+ /// The percentage
+ ///
+ public double Percentage
+ {
+ get => _percentage;
+ set => SetAndNotify(ref _percentage, value);
+ }
+
+ ///
+ /// Gets or sets the progress per second
+ ///
+ public double ProgressPerSecond
+ {
+ get => _progressPerSecond;
+ set => SetAndNotify(ref _progressPerSecond, value);
+ }
+
+ #region Implementation of IProgress
+
+ ///
+ public void Report((long, long) value)
+ {
+ (long newCurrent, long newTotal) = value;
+
+ TimeSpan timePassed = DateTime.Now - _lastReport;
+ if (timePassed >= TimeSpan.FromSeconds(1))
+ {
+ ProgressPerSecond = Math.Max(0, Math.Round(1.0 / timePassed.TotalSeconds * (newCurrent - _lastReportValue), 2));
+ _lastReportValue = newCurrent;
+ _lastReport = DateTime.Now;
+ }
+
+ Current = newCurrent;
+ Total = newTotal;
+ Percentage = Math.Round((double) Current / Total * 100.0, 2);
+
+ OnProgressReported();
+ }
+
+ #endregion
+
+ #region Events
+
+ ///
+ /// Occurs when progress has been reported
+ ///
+ public event EventHandler? ProgressReported;
+
+ protected virtual void OnProgressReported()
+ {
+ ProgressReported?.Invoke(this, EventArgs.Empty);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs
index 4f833282d..7b3e6dea1 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
@@ -331,16 +340,24 @@ 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);
- List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList();
+ List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).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());
+ {
+ plugin.Bootstrapper = (PluginBootstrapper?) Activator.CreateInstance(bootstrappers.First());
+ plugin.Bootstrapper?.InternalOnPluginLoaded(plugin);
+ }
lock (_plugins)
{
@@ -369,6 +386,9 @@ namespace Artemis.Core.Services
return;
}
+ if (!plugin.Info.ArePrerequisitesMet())
+ 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));
OnPluginEnabling(new PluginEventArgs(plugin));
@@ -391,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)
{
@@ -403,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)
{
@@ -570,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;
@@ -581,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);
@@ -593,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.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}" />