From de5b8e445856c7a5c52a009f9052f0341d79c5a1 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 21 Jan 2021 19:25:46 +0100 Subject: [PATCH] Plugins - Added RequiresAdmin boolean Utilities - Simplified shutdown method signature Utilities - Added restart method with option to elevate Core - Moved actual shutdown/restart logic to UI --- src/Artemis.Core/Constants.cs | 2 +- src/Artemis.Core/Events/RestartEventArgs.cs | 26 ++++++++++++ src/Artemis.Core/Plugins/PluginInfo.cs | 14 ++++++- src/Artemis.Core/Services/CoreService.cs | 20 ++++++--- .../Services/Interfaces/ICoreService.cs | 5 +++ .../Interfaces/IPluginManagementService.cs | 2 +- .../Services/PluginManagementService.cs | 36 +++++++++++----- src/Artemis.Core/Utilities/Utilities.cs | 42 ++++++++++--------- src/Artemis.UI/Bootstrapper.cs | 29 +++++++++++++ .../Screens/Settings/Debug/DebugView.xaml | 2 + .../Screens/Settings/Debug/DebugViewModel.cs | 10 +++++ src/Artemis.UI/Screens/TrayViewModel.cs | 2 +- 12 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 src/Artemis.Core/Events/RestartEventArgs.cs diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 408ce0d50..effc348c2 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -108,6 +108,6 @@ namespace Artemis.Core typeof(float), typeof(double), typeof(decimal) - }; + }; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/RestartEventArgs.cs b/src/Artemis.Core/Events/RestartEventArgs.cs new file mode 100644 index 000000000..6df873143 --- /dev/null +++ b/src/Artemis.Core/Events/RestartEventArgs.cs @@ -0,0 +1,26 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Provides data about application restart events + /// + public class RestartEventArgs : EventArgs + { + internal RestartEventArgs(bool elevate, TimeSpan delay) + { + Elevate = elevate; + Delay = delay; + } + + /// + /// Gets a boolean indicating whether the application should be restarted with elevated permissions + /// + public bool Elevate { get; } + + /// + /// Gets the delay before killing process and restarting + /// + public TimeSpan Delay { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index 831fd4015..fcbeacd07 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -17,6 +17,7 @@ namespace Artemis.Core private string _main = null!; private string _name = null!; private Plugin _plugin = null!; + private bool _requiresAdmin; private Version _version = null!; internal PluginInfo() @@ -86,7 +87,8 @@ namespace Artemis.Core } /// - /// Gets or sets a boolean indicating whether this plugin should automatically enable all its features when it is first loaded + /// Gets or sets a boolean indicating whether this plugin should automatically enable all its features when it is first + /// loaded /// [DefaultValue(true)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] @@ -96,6 +98,16 @@ namespace Artemis.Core set => SetAndNotify(ref _autoEnableFeatures, value); } + /// + /// Gets a boolean indicating whether this plugin requires elevated admin privileges + /// + [JsonProperty] + public bool RequiresAdmin + { + get => _requiresAdmin; + internal set => SetAndNotify(ref _requiresAdmin, value); + } + /// /// Gets the plugin this info is associated with /// diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 55674dd4c..4177b475a 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -78,6 +78,7 @@ namespace Artemis.Core.Services public TimeSpan FrameTime { get; private set; } public bool ModuleRenderingDisabled { get; set; } public List? StartupArguments { get; set; } + public bool IsElevated { get; set; } public void Dispose() { @@ -93,18 +94,27 @@ namespace Artemis.Core.Services throw new ArtemisCoreException("Cannot initialize the core as it is already initialized."); AssemblyInformationalVersionAttribute? versionAttribute = typeof(CoreService).Assembly.GetCustomAttribute(); - _logger.Information("Initializing Artemis Core version {version}, build {buildNumber} branch {branch}.", versionAttribute?.InformationalVersion, Constants.BuildInfo.BuildNumber, - Constants.BuildInfo.SourceBranch); - // This should prevent a certain someone from removing HidSharp as an unused dependency as well - _logger.Information("Forcing plugins to use HidSharp {hidSharpVersion}", Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version); + _logger.Information( + "Initializing Artemis Core version {version}, build {buildNumber} branch {branch}.", + versionAttribute?.InformationalVersion, + Constants.BuildInfo.BuildNumber, + Constants.BuildInfo.SourceBranch + ); + _logger.Information("Startup arguments: {args}", StartupArguments); + _logger.Information("Elevated permissions: {perms}", IsElevated); ApplyLoggingLevel(); + // Don't remove even if it looks useless + // Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well + Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version; + _logger.Debug("Forcing plugins to use HidSharp {hidSharpVersion}", hidSharpVersion); + DeserializationLogger.Initialize(Kernel); // Initialize the services _pluginManagementService.CopyBuiltInPlugins(); - _pluginManagementService.LoadPlugins(StartupArguments != null && StartupArguments.Contains("--ignore-plugin-lock")); + _pluginManagementService.LoadPlugins(StartupArguments != null && StartupArguments.Contains("--ignore-plugin-lock"), IsElevated); ArtemisSurface surfaceConfig = _surfaceService.ActiveSurface; _logger.Information("Initialized with active surface entity {surfaceConfig}-{guid}", surfaceConfig.Name, surfaceConfig.EntityId); diff --git a/src/Artemis.Core/Services/Interfaces/ICoreService.cs b/src/Artemis.Core/Services/Interfaces/ICoreService.cs index cb658c2c3..6e13fba3b 100644 --- a/src/Artemis.Core/Services/Interfaces/ICoreService.cs +++ b/src/Artemis.Core/Services/Interfaces/ICoreService.cs @@ -28,6 +28,11 @@ namespace Artemis.Core.Services /// List? StartupArguments { get; set; } + /// + /// Gets a boolean indicating whether Artemis is running in an elevated environment (admin permissions) + /// + bool IsElevated { get; set; } + /// /// Initializes the core, only call once /// diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index 8dec2854b..170f87771 100644 --- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs @@ -26,7 +26,7 @@ namespace Artemis.Core.Services /// /// Loads all installed plugins. If plugins already loaded this will reload them all /// - void LoadPlugins(bool ignorePluginLock); + void LoadPlugins(bool ignorePluginLock, bool isElevated); /// /// Unloads all installed plugins. diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index cdf0f41df..bf18f1dde 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -172,7 +172,7 @@ namespace Artemis.Core.Services #region Plugins - public void LoadPlugins(bool ignorePluginLock) + public void LoadPlugins(bool ignorePluginLock, bool isElevated) { if (LoadingPlugins) throw new ArtemisCoreException("Cannot load plugins while a previous load hasn't been completed yet."); @@ -188,9 +188,7 @@ namespace Artemis.Core.Services { try { - Plugin plugin = LoadPlugin(subDirectory); - if (plugin.Entity.IsEnabled) - EnablePlugin(plugin, false, ignorePluginLock); + LoadPlugin(subDirectory); } catch (Exception e) { @@ -198,6 +196,25 @@ namespace Artemis.Core.Services } } + lock (_plugins) + { + _logger.Debug("Loaded {count} plugin(s)", _plugins.Count); + + bool mustElevate = !isElevated && _plugins.Any(p => p.Entity.IsEnabled && p.Info.RequiresAdmin); + if (mustElevate) + { + _logger.Information("Restarting because one or more plugins requires elevation"); + // No need for a delay this early on, nothing that needs graceful shutdown is happening yet + Utilities.Restart(true, TimeSpan.Zero); + return; + } + + foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) + EnablePlugin(plugin, false, ignorePluginLock); + + _logger.Debug("Enabled {count} plugin(s)", _plugins.Where(p => p.IsEnabled).Sum(p => p.Features.Count(f => f.IsEnabled))); + } + LoadingPlugins = false; } @@ -217,7 +234,7 @@ namespace Artemis.Core.Services public Plugin LoadPlugin(DirectoryInfo directory) { - _logger.Debug("Loading plugin from {directory}", directory.FullName); + _logger.Verbose("Loading plugin from {directory}", directory.FullName); // Load the metadata string metadataFile = Path.Combine(directory.FullName, "plugin.json"); @@ -411,8 +428,7 @@ namespace Artemis.Core.Services public void EnablePluginFeature(PluginFeature pluginFeature, bool saveState, bool isAutoEnable) { - _logger.Debug("Enabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); - + _logger.Verbose("Enabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); OnPluginFeatureEnabling(new PluginFeatureEventArgs(pluginFeature)); try @@ -443,7 +459,7 @@ namespace Artemis.Core.Services if (pluginFeature.IsEnabled) { - _logger.Debug("Successfully enabled plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); + _logger.Verbose("Successfully enabled plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); OnPluginFeatureEnabled(new PluginFeatureEventArgs(pluginFeature)); } else @@ -457,7 +473,7 @@ namespace Artemis.Core.Services { try { - _logger.Debug("Disabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); + _logger.Verbose("Disabling plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); pluginFeature.SetEnabled(false); } finally @@ -470,7 +486,7 @@ namespace Artemis.Core.Services if (!pluginFeature.IsEnabled) { - _logger.Debug("Successfully disabled plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); + _logger.Verbose("Successfully disabled plugin feature {feature} - {plugin}", pluginFeature, pluginFeature.Plugin); OnPluginFeatureDisabled(new PluginFeatureEventArgs(pluginFeature)); } } diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index 39cfc9339..47861c970 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; @@ -27,31 +28,22 @@ namespace Artemis.Core /// gracefully shut down /// /// - /// The delay in seconds after which to kill the application (ignored when a debugger is attached) - /// Whether or not to restart the application after shutdown (ignored when a debugger is attached) - public static void Shutdown(int delay, bool restart) + public static void Shutdown() { - // Always kill the process after the delay has passed, with all the plugins a graceful shutdown cannot be guaranteed - string arguments = "-Command \"& {Start-Sleep -s " + delay + "; (Get-Process 'Artemis.UI').kill()}"; - // If restart is required, start the executable again after the process was killed - if (restart) - arguments = "-Command \"& {Start-Sleep -s " + delay + "; (Get-Process 'Artemis.UI').kill(); Start-Process -FilePath '" + Process.GetCurrentProcess().MainModule!.FileName + "'}\""; - - ProcessStartInfo info = new() - { - Arguments = arguments, - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = "PowerShell.exe" - }; - - if (!Debugger.IsAttached) - Process.Start(info); - // Request a graceful shutdown, whatever UI we're running can pick this up OnShutdownRequested(); } + /// + /// Restarts the application + /// + /// Whether the application should be restarted with elevated permissions + /// Delay in seconds before killing process and restarting + public static void Restart(bool elevate, TimeSpan delay) + { + OnRestartRequested(new RestartEventArgs(elevate, delay)); + } + /// /// Opens the provided URL in the default web browser /// @@ -105,11 +97,21 @@ namespace Artemis.Core /// public static event EventHandler? ShutdownRequested; + /// + /// Occurs when the core has requested an application restart + /// + public static event EventHandler? RestartRequested; + private static void OnShutdownRequested() { ShutdownRequested?.Invoke(null, EventArgs.Empty); } #endregion + + private static void OnRestartRequested(RestartEventArgs e) + { + RestartRequested?.Invoke(null, e); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index 51af5da78..400ded1a6 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Security.Principal; using System.Threading.Tasks; using System.Windows; using System.Windows.Markup; using System.Windows.Threading; +using Artemis.Core; using Artemis.Core.Ninject; using Artemis.Core.Services; using Artemis.UI.Ninject; @@ -37,6 +40,7 @@ namespace Artemis.UI protected override void Launch() { Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; + Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; Core.Utilities.PrepareFirstLaunch(); ILogger logger = Kernel.Get(); @@ -77,6 +81,7 @@ namespace Artemis.UI } _core.StartupArguments = StartupArguments; + _core.IsElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); _core.Initialize(); } catch (Exception e) @@ -124,6 +129,30 @@ namespace Artemis.UI private void UtilitiesOnShutdownRequested(object sender, EventArgs e) { + // Use PowerShell to kill the process after 2 sec just in case + ProcessStartInfo info = new() + { + Arguments = "-Command \"& {Start-Sleep -s 2; (Get-Process 'Artemis.UI').kill()}", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + + Execute.OnUIThread(() => Application.Current.Shutdown()); + } + + private void UtilitiesOnRestartRequested(object sender, RestartEventArgs e) + { + ProcessStartInfo info = new() + { + Arguments = + $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; (Get-Process 'Artemis.UI').kill(); Start-Process -FilePath '{Constants.ExecutablePath}' {(e.Elevate ? "-Verb RunAs" : "")}}}\"", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); Execute.OnUIThread(() => Application.Current.Shutdown()); } diff --git a/src/Artemis.UI/Screens/Settings/Debug/DebugView.xaml b/src/Artemis.UI/Screens/Settings/Debug/DebugView.xaml index fbe220eb2..46eb707f5 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/DebugView.xaml +++ b/src/Artemis.UI/Screens/Settings/Debug/DebugView.xaml @@ -40,6 +40,8 @@