From b332614d003a1ff2887e131ea8d32f700ba9e258 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 25 May 2021 14:16:59 +0200 Subject: [PATCH 1/7] Prerequisites - Removed RequiresElevation Prerequisites - Added PowerShell actions --- .../Prerequisites/PluginPrerequisite.cs | 5 -- .../PrerequisiteAction/ExecuteFileAction.cs | 4 +- .../RunInlinePowerShellAction.cs | 70 +++++++++++++++++++ .../PrerequisiteAction/RunPowerShellAction.cs | 53 ++++++++++++++ .../Plugins/PluginPrerequisiteViewModel.cs | 19 +---- 5 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs index 57c4850a7..3adc6fac1 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -22,11 +22,6 @@ namespace Artemis.Core /// public abstract string Description { get; } - /// - /// [NYI] 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 /// diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs index fcfc4db19..e98f0f354 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs @@ -18,7 +18,7 @@ namespace Artemis.Core /// The target file to execute /// A set of command-line arguments to use when starting the application /// A boolean indicating whether the action should wait for the process to exit - /// A boolean indicating whether the file should run with administrator privileges (does not require ) + /// A boolean indicating whether the file should run with administrator privileges public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name) { FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); @@ -72,7 +72,7 @@ namespace Artemis.Core } } - private static Task RunProcessAsync(string fileName, string? arguments, bool elevate) + internal static Task RunProcessAsync(string fileName, string? arguments, bool elevate) { TaskCompletionSource tcs = new(); diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs new file mode 100644 index 000000000..08f6e63ca --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs @@ -0,0 +1,70 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action runs inline powershell + /// + public class RunInlinePowerShellAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The inline code to run + /// A boolean indicating whether the file should run with administrator privileges + public RunInlinePowerShellAction(string name, string code, bool elevate = false) : base(name) + { + Code = code; + Elevate = elevate; + ProgressIndeterminate = true; + } + + /// + /// Gets the inline code to run + /// + public string Code { get; } + + /// + /// Gets a boolean indicating whether the file should run with administrator privileges + /// + public bool Elevate { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string file = Path.GetTempFileName().Replace(".tmp", ".ps1"); + try + { + string code = + @"try + { + " + Code + @" + Start-Sleep 1 + } + catch + { + Write-Error $_.Exception.ToString() + pause + }"; + + await File.WriteAllTextAsync(file, code, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + Status = "Running PowerShell script and waiting for exit.."; + ShowProgressBar = true; + ProgressIndeterminate = true; + + int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-File {file}", Elevate); + + Status = $"PowerShell exited with code {result}"; + } + finally + { + File.Delete(file); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs new file mode 100644 index 000000000..8255328ee --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs @@ -0,0 +1,53 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that runs a PowerShell script + /// Note: To run an inline script instead, use + /// + public class RunPowerShellAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The full path of the script to run + /// A boolean indicating whether the file should run with administrator privileges + public RunPowerShellAction(string name, string scriptPath, bool elevate = false) : base(name) + { + ScriptPath = scriptPath; + Elevate = elevate; + ProgressIndeterminate = true; + } + + /// + /// Gets the inline full path of the script to run + /// + public string ScriptPath { get; } + + /// + /// Gets a boolean indicating whether the file should run with administrator privileges + /// + public bool Elevate { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + if (!ScriptPath.EndsWith(".ps1")) + throw new ArtemisPluginException($"Script at path {ScriptPath} must have the .ps1 extension or PowerShell will refuse to run it"); + if (!File.Exists(ScriptPath)) + throw new ArtemisCoreException($"Script not found at path {ScriptPath}"); + + Status = "Running PowerShell script and waiting for exit.."; + ShowProgressBar = true; + ProgressIndeterminate = true; + + int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-File {ScriptPath}", Elevate); + + Status = $"PowerShell exited with code {result}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs index 841f79385..453ec9bac 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs @@ -13,18 +13,13 @@ namespace Artemis.UI.Screens.Plugins public class PluginPrerequisiteViewModel : Conductor.Collection.OneActive { private readonly bool _uninstall; - private readonly ICoreService _coreService; - private readonly IDialogService _dialogService; private bool _installing; private bool _uninstalling; private bool _isMet; - public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall, ICoreService coreService, IDialogService dialogService) + public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall) { _uninstall = uninstall; - _coreService = coreService; - _dialogService = dialogService; - PluginPrerequisite = pluginPrerequisite; } @@ -65,12 +60,6 @@ namespace Artemis.UI.Screens.Plugins if (Busy) return; - if (PluginPrerequisite.RequiresElevation && !_coreService.IsElevated) - { - await _dialogService.ShowConfirmDialog("Install plugin prerequisite", "This plugin prerequisite admin rights to install (restart & elevate NYI)"); - return; - } - Installing = true; try { @@ -88,12 +77,6 @@ namespace Artemis.UI.Screens.Plugins if (Busy) return; - if (PluginPrerequisite.RequiresElevation && !_coreService.IsElevated) - { - await _dialogService.ShowConfirmDialog("Install plugin prerequisite", "This plugin prerequisite admin rights to install (restart & elevate NYI)"); - return; - } - Uninstalling = true; try { From a6f52ce4a03f954273f7b2e2930db1d59e005fc6 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 25 May 2021 23:15:40 +0200 Subject: [PATCH 2/7] Core - Refactored (unused) queued actions system --- .../Interfaces/IPluginManagementService.cs | 13 ++-- .../Services/PluginManagementService.cs | 63 +++++++++++++------ .../Entities/General/QueuedActionEntity.cs | 19 ++++++ .../Entities/Plugins/PluginSettingEntity.cs | 17 ----- .../Interfaces/IPluginRepository.cs | 8 +-- .../Interfaces/IQueuedActionRepository.cs | 13 ++++ .../Repositories/ModuleRepository.cs | 3 +- .../Repositories/PluginRepository.cs | 20 ------ .../Repositories/QueuedActionRepository.cs | 46 ++++++++++++++ 9 files changed, 134 insertions(+), 68 deletions(-) create mode 100644 src/Artemis.Storage/Entities/General/QueuedActionEntity.cs create mode 100644 src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs create mode 100644 src/Artemis.Storage/Repositories/QueuedActionRepository.cs diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index d0719bd38..ddd61d6cd 100644 --- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs @@ -135,11 +135,16 @@ namespace Artemis.Core.Services DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); /// - /// Queues an action for the provided plugin for the next time Artemis starts, before plugins are loaded + /// Queues the provided plugin to be deleted the next time Artemis starts, before plugins are loaded /// - /// The plugin to queue the action for - /// The action to take - void QueuePluginAction(Plugin plugin, PluginManagementAction pluginAction); + /// The plugin to delete + void QueuePluginDeletion(Plugin plugin); + + /// + /// Removes the provided plugin for the deletion queue it was added to via + /// + /// The plugin to dequeue + void DequeuePluginDeletion(Plugin plugin); /// /// Occurs when built-in plugins are being loaded diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 5655b8112..c9c698be7 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reflection; using Artemis.Core.DeviceProviders; using Artemis.Core.Ninject; +using Artemis.Storage.Entities.General; using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Repositories.Interfaces; using McMaster.NETCore.Plugins; @@ -26,17 +27,19 @@ namespace Artemis.Core.Services private readonly IKernel _kernel; private readonly ILogger _logger; private readonly IPluginRepository _pluginRepository; + private readonly IQueuedActionRepository _queuedActionRepository; private readonly List _plugins; private bool _isElevated; - public PluginManagementService(IKernel kernel, ILogger logger, IPluginRepository pluginRepository) + public PluginManagementService(IKernel kernel, ILogger logger, IPluginRepository pluginRepository, IQueuedActionRepository queuedActionRepository) { _kernel = kernel; _logger = logger; _pluginRepository = pluginRepository; + _queuedActionRepository = queuedActionRepository; _plugins = new List(); - ProcessQueuedActions(); + ProcessPluginDeletionQueue(); } private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) @@ -664,27 +667,51 @@ namespace Artemis.Core.Services #region Queued actions - public void QueuePluginAction(Plugin plugin, PluginManagementAction pluginAction) + public void QueuePluginDeletion(Plugin plugin) { - List existing = _pluginRepository.GetQueuedActions(plugin.Guid); - if (existing.Any(e => pluginAction == PluginManagementAction.Delete && e is PluginQueuedDeleteEntity)) - return; - - if (pluginAction == PluginManagementAction.Delete) - _pluginRepository.AddQueuedAction(new PluginQueuedDeleteEntity {PluginGuid = plugin.Guid, Directory = plugin.Directory.FullName}); + _queuedActionRepository.Add(new QueuedActionEntity + { + Type = "DeletePlugin", + CreatedAt = DateTimeOffset.Now, + Parameters = new Dictionary() + { + {"pluginGuid", plugin.Guid.ToString()}, + {"plugin", plugin.ToString()}, + {"directory", plugin.Directory.FullName} + } + }); } - private void ProcessQueuedActions() + public void DequeuePluginDeletion(Plugin plugin) { - foreach (PluginQueuedActionEntity pluginQueuedActionEntity in _pluginRepository.GetQueuedActions()) - { - if (pluginQueuedActionEntity is PluginQueuedDeleteEntity deleteAction) - { - if (Directory.Exists(deleteAction.Directory)) - Directory.Delete(deleteAction.Directory, true); - } + QueuedActionEntity? queuedActionEntity = _queuedActionRepository.GetByType("DeletePlugin").FirstOrDefault(q => q.Parameters["pluginGuid"].Equals(plugin.Guid.ToString())); + if (queuedActionEntity != null) + _queuedActionRepository.Remove(queuedActionEntity); + } - _pluginRepository.RemoveQueuedAction(pluginQueuedActionEntity); + private void ProcessPluginDeletionQueue() + { + foreach (QueuedActionEntity queuedActionEntity in _queuedActionRepository.GetByType("DeletePlugin")) + { + string? directory = queuedActionEntity.Parameters["directory"].ToString(); + try + { + if (Directory.Exists(directory)) + { + _logger.Information("Queued plugin deletion - deleting folder - {plugin}", queuedActionEntity.Parameters["plugin"]); + Directory.Delete(directory!, true); + } + else + { + _logger.Information("Queued plugin deletion - folder already deleted - {plugin}", queuedActionEntity.Parameters["plugin"]); + } + + _queuedActionRepository.Remove(queuedActionEntity); + } + catch (Exception e) + { + _logger.Warning(e, "Queued plugin deletion failed - {plugin}", queuedActionEntity.Parameters["plugin"]); + } } } diff --git a/src/Artemis.Storage/Entities/General/QueuedActionEntity.cs b/src/Artemis.Storage/Entities/General/QueuedActionEntity.cs new file mode 100644 index 000000000..62eeee417 --- /dev/null +++ b/src/Artemis.Storage/Entities/General/QueuedActionEntity.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.General +{ + public class QueuedActionEntity + { + public QueuedActionEntity() + { + Parameters = new Dictionary(); + } + + public Guid Id { get; set; } + public string Type { get; set; } + public DateTimeOffset CreatedAt { get; set; } + + public Dictionary Parameters { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs b/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs index 08a17f922..ae4827426 100644 --- a/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs +++ b/src/Artemis.Storage/Entities/Plugins/PluginSettingEntity.cs @@ -13,21 +13,4 @@ namespace Artemis.Storage.Entities.Plugins public string Name { get; set; } public string Value { get; set; } } - - /// - /// Represents a queued action for a plugin - /// - public abstract class PluginQueuedActionEntity - { - public Guid Id { get; set; } - public Guid PluginGuid { get; set; } - } - - /// - /// Represents a queued delete action for a plugin - /// - public class PluginQueuedDeleteEntity : PluginQueuedActionEntity - { - public string Directory { get; set; } - } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs index 602eb2572..8b559b255 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IPluginRepository.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Artemis.Storage.Entities.Plugins; namespace Artemis.Storage.Repositories.Interfaces @@ -9,16 +8,11 @@ namespace Artemis.Storage.Repositories.Interfaces void AddPlugin(PluginEntity pluginEntity); PluginEntity GetPluginByGuid(Guid pluginGuid); void SavePlugin(PluginEntity pluginEntity); - + void AddSetting(PluginSettingEntity pluginSettingEntity); PluginSettingEntity GetSettingByGuid(Guid pluginGuid); PluginSettingEntity GetSettingByNameAndGuid(string name, Guid pluginGuid); void SaveSetting(PluginSettingEntity pluginSettingEntity); void RemoveSettings(Guid pluginGuid); - - void AddQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity); - List GetQueuedActions(); - List GetQueuedActions(Guid pluginGuid); - void RemoveQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity); } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs new file mode 100644 index 000000000..20d7db070 --- /dev/null +++ b/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Artemis.Storage.Entities.General; + +namespace Artemis.Storage.Repositories.Interfaces +{ + public interface IQueuedActionRepository : IRepository + { + void Add(QueuedActionEntity queuedActionEntity); + void Remove(QueuedActionEntity queuedActionEntity); + List GetAll(); + List GetByType(string type); + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ModuleRepository.cs b/src/Artemis.Storage/Repositories/ModuleRepository.cs index 9d733507c..aae79d974 100644 --- a/src/Artemis.Storage/Repositories/ModuleRepository.cs +++ b/src/Artemis.Storage/Repositories/ModuleRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Artemis.Storage.Entities.Module; using Artemis.Storage.Repositories.Interfaces; using LiteDB; diff --git a/src/Artemis.Storage/Repositories/PluginRepository.cs b/src/Artemis.Storage/Repositories/PluginRepository.cs index db67f0c91..9a2ec11e3 100644 --- a/src/Artemis.Storage/Repositories/PluginRepository.cs +++ b/src/Artemis.Storage/Repositories/PluginRepository.cs @@ -15,7 +15,6 @@ namespace Artemis.Storage.Repositories _repository = repository; _repository.Database.GetCollection().EnsureIndex(s => new {s.Name, s.PluginGuid}, true); - _repository.Database.GetCollection().EnsureIndex(s => s.PluginGuid); } public void AddPlugin(PluginEntity pluginEntity) @@ -59,24 +58,5 @@ namespace Artemis.Storage.Repositories { _repository.DeleteMany(s => s.PluginGuid == pluginGuid); } - - public List GetQueuedActions() - { - return _repository.Query().ToList(); - } - - public List GetQueuedActions(Guid pluginGuid) - { - return _repository.Query().Where(q => q.PluginGuid == pluginGuid).ToList(); - } - - public void AddQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity) - { - _repository.Upsert(pluginQueuedActionEntity); - } - public void RemoveQueuedAction(PluginQueuedActionEntity pluginQueuedActionEntity) - { - _repository.Delete(pluginQueuedActionEntity.Id); - } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/QueuedActionRepository.cs b/src/Artemis.Storage/Repositories/QueuedActionRepository.cs new file mode 100644 index 000000000..1376149d5 --- /dev/null +++ b/src/Artemis.Storage/Repositories/QueuedActionRepository.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Artemis.Storage.Entities.General; +using Artemis.Storage.Repositories.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Repositories +{ + public class QueuedActionRepository : IQueuedActionRepository + { + private readonly LiteRepository _repository; + + public QueuedActionRepository(LiteRepository repository) + { + _repository = repository; + _repository.Database.GetCollection().EnsureIndex(s => s.Type); + } + + #region Implementation of IQueuedActionRepository + + /// + public void Add(QueuedActionEntity queuedActionEntity) + { + _repository.Insert(queuedActionEntity); + } + + /// + public void Remove(QueuedActionEntity queuedActionEntity) + { + _repository.Delete(queuedActionEntity.Id); + } + + /// + public List GetAll() + { + return _repository.Query().ToList(); + } + + /// + public List GetByType(string type) + { + return _repository.Query().Where(q => q.Type == type).ToList(); + } + + #endregion + } +} \ No newline at end of file From 42fdf44df975af7e37bbe3b14616c7515a8583a7 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 25 May 2021 23:19:14 +0200 Subject: [PATCH 3/7] Prerequisites - Added optional arguments to PowerShell actions --- .../RunInlinePowerShellAction.cs | 15 +++++++++++++-- .../PrerequisiteAction/RunPowerShellAction.cs | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs index 08f6e63ca..c291b81a3 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs @@ -15,10 +15,15 @@ namespace Artemis.Core /// The name of the action /// The inline code to run /// A boolean indicating whether the file should run with administrator privileges - public RunInlinePowerShellAction(string name, string code, bool elevate = false) : base(name) + /// + /// Optional arguments to pass to your script, you are responsible for proper quoting etc. + /// Arguments are available in PowerShell as $args[0], $args[1] etc. + /// + public RunInlinePowerShellAction(string name, string code, bool elevate = false, string? arguments = null) : base(name) { Code = code; Elevate = elevate; + Arguments = arguments; ProgressIndeterminate = true; } @@ -32,6 +37,12 @@ namespace Artemis.Core /// public bool Elevate { get; } + /// + /// Gets optional arguments to pass to your script, you are responsible for proper quoting etc. + /// Arguments are available in PowerShell as $args[0], $args[1] etc. + /// + public string? Arguments { get; } + /// public override async Task Execute(CancellationToken cancellationToken) { @@ -57,7 +68,7 @@ namespace Artemis.Core ShowProgressBar = true; ProgressIndeterminate = true; - int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-File {file}", Elevate); + int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-File {file} {Arguments}", Elevate); Status = $"PowerShell exited with code {result}"; } diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs index 8255328ee..4ace34904 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs @@ -16,10 +16,15 @@ namespace Artemis.Core /// The name of the action /// The full path of the script to run /// A boolean indicating whether the file should run with administrator privileges - public RunPowerShellAction(string name, string scriptPath, bool elevate = false) : base(name) + /// + /// Optional arguments to pass to your script, you are responsible for proper quoting etc. + /// Arguments are available in PowerShell as $args[0], $args[1] etc. + /// + public RunPowerShellAction(string name, string scriptPath, bool elevate = false, string? arguments = null) : base(name) { ScriptPath = scriptPath; Elevate = elevate; + Arguments = arguments; ProgressIndeterminate = true; } @@ -33,6 +38,12 @@ namespace Artemis.Core /// public bool Elevate { get; } + /// + /// Gets optional arguments to pass to your script, you are responsible for proper quoting etc. + /// Arguments are available in PowerShell as $args[0], $args[1] etc. + /// + public string? Arguments { get; } + /// public override async Task Execute(CancellationToken cancellationToken) { @@ -45,7 +56,7 @@ namespace Artemis.Core ShowProgressBar = true; ProgressIndeterminate = true; - int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-File {ScriptPath}", Elevate); + int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-File {ScriptPath} {Arguments}", Elevate); Status = $"PowerShell exited with code {result}"; } From 7325469948e68dcaf6a621b36fb21e4e8685f9c5 Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Tue, 25 May 2021 22:32:57 +0100 Subject: [PATCH 4/7] Prerequisites - Run powershell scripts unrestricted --- .../PrerequisiteAction/RunInlinePowerShellAction.cs | 2 +- .../Prerequisites/PrerequisiteAction/RunPowerShellAction.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs index c291b81a3..df50a122c 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs @@ -68,7 +68,7 @@ namespace Artemis.Core ShowProgressBar = true; ProgressIndeterminate = true; - int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-File {file} {Arguments}", Elevate); + int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {file} {Arguments}", Elevate); Status = $"PowerShell exited with code {result}"; } diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs index 4ace34904..4b19a03a6 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs @@ -56,7 +56,7 @@ namespace Artemis.Core ShowProgressBar = true; ProgressIndeterminate = true; - int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-File {ScriptPath} {Arguments}", Elevate); + int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {ScriptPath} {Arguments}", Elevate); Status = $"PowerShell exited with code {result}"; } From bb55f2e1bb1dd6bccc015bc9c036d5713ed68147 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 25 May 2021 23:58:44 +0200 Subject: [PATCH 5/7] Core - Hardened data model paths against hidden inherited members --- .../Profile/DataModel/DataModelPathSegment.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs index 27f4294ed..7531cb09f 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs @@ -17,6 +17,7 @@ namespace Artemis.Core private DataModel? _dynamicDataModel; private Type? _dynamicDataModelType; private DataModelPropertyAttribute? _dynamicDataModelAttribute; + private PropertyInfo? _property; internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path) { @@ -99,7 +100,7 @@ namespace Artemis.Core return null; // If this is not the first segment in a path, the property is located on the previous segment - return Previous?.GetPropertyType()?.GetProperty(Identifier); + return Previous?.GetPropertyType()?.GetProperties().FirstOrDefault(p => p.Name == Identifier); } /// @@ -209,14 +210,18 @@ namespace Artemis.Core accessorExpression = expression; // A static segment just needs to access the property or filed else if (Type == DataModelPathSegmentType.Static) - accessorExpression = Expression.PropertyOrField(expression, Identifier); + { + accessorExpression = _property != null + ? Expression.Property(expression, _property) + : Expression.PropertyOrField(expression, Identifier); + } // A dynamic segment calls the generic method DataModel.DynamicChild and provides the identifier as an argument else { accessorExpression = Expression.Call( expression, nameof(DataModel.GetDynamicChildValue), - _dynamicDataModelType != null ? new[] { _dynamicDataModelType } : null, + _dynamicDataModelType != null ? new[] {_dynamicDataModelType} : null, Expression.Constant(Identifier) ); } @@ -243,8 +248,11 @@ namespace Artemis.Core private void DetermineStaticType(Type previousType) { - PropertyInfo? property = previousType.GetProperty(Identifier, BindingFlags.Public | BindingFlags.Instance); - Type = property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static; + // Situations in which AmbiguousMatchException occurs ... + // + // ...derived type declares a property that hides an inherited property with the same name, by using the new modifier + _property = previousType.GetProperties(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(p => p.Name == Identifier); + Type = _property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static; } #region IDisposable From fb3f7e0cc16936a10b92f1e195416cc702ffba8e Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 27 May 2021 18:43:37 +0200 Subject: [PATCH 6/7] UI - Virtualize enum comboboxes --- .../DataModel/Input/EnumDataModelInputView.xaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Artemis.UI/DefaultTypes/DataModel/Input/EnumDataModelInputView.xaml b/src/Artemis.UI/DefaultTypes/DataModel/Input/EnumDataModelInputView.xaml index 6fd659d69..340c9aa68 100644 --- a/src/Artemis.UI/DefaultTypes/DataModel/Input/EnumDataModelInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/DataModel/Input/EnumDataModelInputView.xaml @@ -14,5 +14,11 @@ SelectedValuePath="Value" DisplayMemberPath="Description" SelectedValue="{Binding Path=InputValue}" - IsDropDownOpen="True"/> + IsDropDownOpen="True"> + + + + + + \ No newline at end of file From cef37677f24d7947f899e04295c2365c650afa8d Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 3 Jun 2021 11:10:07 +0200 Subject: [PATCH 7/7] Devices - Added option to disable default layouts --- .../Models/Surface/ArtemisDevice.cs | 22 ++++++++++ src/Artemis.Core/Services/RgbService.cs | 17 +++++--- .../Entities/Surface/DeviceEntity.cs | 1 + .../Behaviors/OpenInBrowser.cs | 41 +++++++++++++++++++ .../Device/Tabs/DevicePropertiesTabView.xaml | 34 +++++++++++++-- .../Tabs/DevicePropertiesTabViewModel.cs | 12 +++++- 6 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 src/Artemis.UI.Shared/Behaviors/OpenInBrowser.cs diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 7f4e8dc3d..9dec7f4bc 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -33,6 +33,8 @@ namespace Artemis.Core BlueScale = 1; IsEnabled = true; + LedIds = new ReadOnlyDictionary(new Dictionary()); + Leds = new ReadOnlyCollection(new List()); InputIdentifiers = new List(); InputMappings = new Dictionary(); Categories = new HashSet(); @@ -51,6 +53,8 @@ namespace Artemis.Core RgbDevice = rgbDevice; DeviceProvider = deviceProvider; + LedIds = new ReadOnlyDictionary(new Dictionary()); + Leds = new ReadOnlyCollection(new List()); InputIdentifiers = new List(); InputMappings = new Dictionary(); Categories = new HashSet(); @@ -258,6 +262,19 @@ namespace Artemis.Core } } + /// + /// Gets or sets a boolean indicating whether falling back to default layouts is enabled or not + /// + public bool DisableDefaultLayout + { + get => DeviceEntity.DisableDefaultLayout; + set + { + DeviceEntity.DisableDefaultLayout = value; + OnPropertyChanged(nameof(DisableDefaultLayout)); + } + } + /// /// Gets or sets the logical layout of the device e.g. DE, UK or US. /// Only applicable to keyboards @@ -532,6 +549,11 @@ namespace Artemis.Core else LogicalLayout = DeviceEntity.LogicalLayout; } + + public void ClearLayout() + { + // TODO + } } /// diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index ae1ea1fc9..e65b74b4b 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -354,19 +354,26 @@ namespace Artemis.Core.Services } // Finally fall back to a default layout - layout = ArtemisLayout.GetDefaultLayout(device); - if (layout != null) - ApplyDeviceLayout(device, layout); + if (!device.DisableDefaultLayout) + layout = ArtemisLayout.GetDefaultLayout(device); + ApplyDeviceLayout(device, layout); return layout; } - public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout) + public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout) { + if (layout == null) + { + if (device.Layout != null) + device.ClearLayout(); + return; + } + if (layout.Source == LayoutSource.Default) device.ApplyLayout(layout, false, false); else device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); - + UpdateLedGroup(); } diff --git a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs index ecacd69dd..6cce72dcc 100644 --- a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs +++ b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs @@ -22,6 +22,7 @@ namespace Artemis.Storage.Entities.Surface public float BlueScale { get; set; } public bool IsEnabled { get; set; } + public bool DisableDefaultLayout { get; set; } public int PhysicalLayout { get; set; } public string LogicalLayout { get; set; } public string CustomLayoutPath { get; set; } diff --git a/src/Artemis.UI.Shared/Behaviors/OpenInBrowser.cs b/src/Artemis.UI.Shared/Behaviors/OpenInBrowser.cs new file mode 100644 index 000000000..e9c980033 --- /dev/null +++ b/src/Artemis.UI.Shared/Behaviors/OpenInBrowser.cs @@ -0,0 +1,41 @@ +using System.Windows.Documents; +using System.Windows.Navigation; +using Artemis.Core; +using Microsoft.Xaml.Behaviors; + +namespace Artemis.UI.Shared +{ + /// + /// Represents a behavior that opens the URI of the hyperlink in the browser when requested + /// + public class OpenInBrowser : Behavior + { + private Hyperlink? _hyperLink; + + /// + protected override void OnAttached() + { + base.OnAttached(); + + _hyperLink = AssociatedObject; + if (_hyperLink == null) + return; + + _hyperLink.RequestNavigate += HyperLinkOnRequestNavigate; + } + + /// + protected override void OnDetaching() + { + if (_hyperLink == null) return; + _hyperLink.RequestNavigate -= HyperLinkOnRequestNavigate; + + base.OnDetaching(); + } + + private void HyperLinkOnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + Utilities.OpenUrl(e.Uri.AbsoluteUri); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabView.xaml b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabView.xaml index 890f730ee..344e87fee 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabView.xaml @@ -7,6 +7,7 @@ xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:s="https://github.com/canton7/Stylet" + xmlns:b="http://schemas.microsoft.com/xaml/behaviors" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:DevicePropertiesTabViewModel}"> @@ -29,7 +30,7 @@ Foreground="{DynamicResource MaterialDesignBodyLight}" TextWrapping="Wrap" TextAlignment="Justify"> - Artemis uses categories to determine where the layers of imported profiles are applied to. + Artemis uses categories to determine where the layers of imported profiles are applied to. You can hover over a category for a more detailed description. @@ -192,15 +193,25 @@ - Custom layout + Layout - Select a custom layout below if you want to change the appearance and/or LEDs of this device. + The device layout is used to determine the position of LEDs and to create the visual representation of the device you see on the left side of this window. + + + + Use default layout if needed + + + + + - Layout path + Custom layout path + + Select a custom layout below if you want to change the appearance and/or LEDs of this device. + For info on how to create layouts, check out + + this wiki article + + + + + . + diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs index 6189a6972..4af2c8313 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DevicePropertiesTabViewModel.cs @@ -134,6 +134,16 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs set => SetCategory(DeviceCategory.Peripherals, value); } + public bool UseDefaultLayout + { + get => !Device.DisableDefaultLayout; + set + { + Device.DisableDefaultLayout = !value; + NotifyOfPropertyChange(nameof(UseDefaultLayout)); + } + } + public void ApplyScaling() { Device.RedScale = RedScale / 100f; @@ -255,7 +265,7 @@ namespace Artemis.UI.Screens.Settings.Device.Tabs private void DeviceOnPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(Device.CustomLayoutPath)) + if (e.PropertyName == nameof(Device.CustomLayoutPath) || e.PropertyName == nameof(Device.DisableDefaultLayout)) _rgbService.ApplyBestDeviceLayout(Device); }