From d7a0c2ac4a8fd2985fb4ee3ebdec636d3a0fa389 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 12 Mar 2024 21:26:47 +0100 Subject: [PATCH] Core - Delay start watching plugins for hot reload after initializing UI - Simplify category management logic UI - Avoid crash during profile icon load Storage - Fix entry metadata retrieval --- .../Models/Profile/ProfileCategory.cs | 19 ++---- src/Artemis.Core/Plugins/Plugin.cs | 8 ++- src/Artemis.Core/Services/CoreService.cs | 1 + .../Interfaces/IPluginManagementService.cs | 9 ++- .../Services/PluginManagementService.cs | 66 +++++++++---------- .../Storage/Interfaces/IProfileService.cs | 4 +- .../Services/Storage/ProfileService.cs | 8 +-- .../Entities/Workshop/EntryEntity.cs | 8 ++- src/Artemis.Storage/ArtemisDbContext.cs | 3 +- .../Entities/Workshop/EntryEntity.cs | 3 +- .../Repositories/ProfileRepository.cs | 5 +- .../ProfileConfigurationIcon.axaml.cs | 15 +++-- .../ProfileConfigurationDropHandler.cs | 4 +- .../Sidebar/SidebarCategoryViewModel.cs | 4 +- .../Models/InstalledEntry.cs | 16 +++-- 15 files changed, 91 insertions(+), 82 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/ProfileCategory.cs b/src/Artemis.Core/Models/Profile/ProfileCategory.cs index cdab02e8a..1a5c6f269 100644 --- a/src/Artemis.Core/Models/Profile/ProfileCategory.cs +++ b/src/Artemis.Core/Models/Profile/ProfileCategory.cs @@ -98,26 +98,15 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel /// /// Adds a profile configuration to this category /// - public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex) + public void AddProfileConfiguration(ProfileConfiguration configuration, ProfileConfiguration? target) { - List targetList = ProfileConfigurations.ToList(); - - // TODO: Look into this, it doesn't seem to make sense - // Removing the original will shift every item in the list forwards, keep that in mind with the target index - if (configuration.Category == this && targetIndex != null && targetIndex.Value > targetList.IndexOf(configuration)) - targetIndex -= 1; - + List targetList = ProfileConfigurations.Where(c => c!= configuration).ToList(); configuration.Category.RemoveProfileConfiguration(configuration); - if (targetIndex != null) - { - targetIndex = Math.Clamp(targetIndex.Value, 0, targetList.Count); - targetList.Insert(targetIndex.Value, configuration); - } + if (target != null) + targetList.Insert(targetList.IndexOf(target), configuration); else - { targetList.Add(configuration); - } configuration.Category = this; ProfileConfigurations = new ReadOnlyCollection(targetList); diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index 5eacf18f0..6f2d2b4d4 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -357,10 +357,10 @@ public class Plugin : CorePropertyChanged, IDisposable return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled); } - internal void AutoEnableIfNew() + internal bool AutoEnableIfNew() { if (_loadedFromStorage) - return; + return false; // Enabled is preset to true if the plugin meets the following criteria // - Requires no admin rights @@ -371,11 +371,13 @@ public class Plugin : CorePropertyChanged, IDisposable Info.ArePrerequisitesMet(); if (!Entity.IsEnabled) - return; + return false; // Also auto-enable any non-device provider feature foreach (PluginFeatureInfo pluginFeatureInfo in Features) pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)); + + return true; } /// diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index b27a8601b..da0f62fd7 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -71,6 +71,7 @@ internal class CoreService : ICoreService // Initialize the services _pluginManagementService.CopyBuiltInPlugins(); _pluginManagementService.LoadPlugins(IsElevated); + _pluginManagementService.StartHotReload(); _renderService.Initialize(); OnInitialized(); diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index 932e3f507..fbd97344d 100644 --- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs @@ -16,7 +16,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable /// Gets a list containing additional directories in which plugins are located, used while loading plugins. /// List AdditionalPluginDirectories { get; } - + /// /// Indicates whether or not plugins are currently being loaded /// @@ -33,6 +33,11 @@ public interface IPluginManagementService : IArtemisService, IDisposable /// void LoadPlugins(bool isElevated); + /// + /// Starts monitoring plugin directories for changes and reloads plugins when changes are detected + /// + void StartHotReload(); + /// /// Unloads all installed plugins. /// @@ -145,7 +150,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable /// /// DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); - + /// /// 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 62bc658ac..0334417f6 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -41,37 +41,6 @@ internal class PluginManagementService : IPluginManagementService _pluginRepository = pluginRepository; _deviceRepository = deviceRepository; _plugins = new List(); - - StartHotReload(); - } - - private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) - { - ZipArchiveEntry metaDataFileEntry = zipArchive.Entries.First(e => e.Name == "plugin.json"); - DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory)); - bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock")); - - // Remove the old directory if it exists - if (Directory.Exists(pluginDirectory.FullName)) - pluginDirectory.Delete(true); - - // Extract everything in the same archive directory to the unique plugin directory - Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); - string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); - foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries) - { - if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory) && !zipArchiveEntry.FullName.EndsWith("/")) - { - string target = Path.Combine(pluginDirectory.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length)); - // Create folders - Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!); - // Extract files - zipArchiveEntry.ExtractToFile(target); - } - } - - if (createLockFile) - File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close(); } public List AdditionalPluginDirectories { get; } = new(); @@ -155,6 +124,35 @@ internal class PluginManagementService : IPluginManagementService } } + private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) + { + ZipArchiveEntry metaDataFileEntry = zipArchive.Entries.First(e => e.Name == "plugin.json"); + DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory)); + bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock")); + + // Remove the old directory if it exists + if (Directory.Exists(pluginDirectory.FullName)) + pluginDirectory.Delete(true); + + // Extract everything in the same archive directory to the unique plugin directory + Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); + string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); + foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries) + { + if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory) && !zipArchiveEntry.FullName.EndsWith("/")) + { + string target = Path.Combine(pluginDirectory.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length)); + // Create folders + Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!); + // Extract files + zipArchiveEntry.ExtractToFile(target); + } + } + + if (createLockFile) + File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close(); + } + #endregion public List GetAllPlugins() @@ -444,7 +442,9 @@ internal class PluginManagementService : IPluginManagementService _logger.Warning("Plugin {plugin} contains no features", plugin); // It is appropriate to call this now that we have the features of this plugin - plugin.AutoEnableIfNew(); + bool autoEnabled = plugin.AutoEnableIfNew(); + if (autoEnabled) + _pluginRepository.SavePlugin(entity); List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList(); if (bootstrappers.Count > 1) @@ -894,7 +894,7 @@ internal class PluginManagementService : IPluginManagementService #region Hot Reload - private void StartHotReload() + public void StartHotReload() { // Watch for changes in the plugin directory, "plugin.json". // If this file is changed, reload the plugin. diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index c8a5f8435..2a55664a9 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -115,9 +115,9 @@ public interface IProfileService : IArtemisService /// any changes are made to it. /// /// Text to add after the name of the profile (separated by a dash). - /// The index at which to import the profile into the category. + /// The profile before which to import the profile into the category. /// The resulting profile configuration. - Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", int targetIndex = 0); + Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", ProfileConfiguration? target = null); /// /// Imports the provided ZIP archive stream into the provided profile configuration diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 6b9889e19..913c38e0e 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -262,7 +262,7 @@ internal class ProfileService : IProfileService { ProfileConfiguration configuration = new(category, name, icon); - category.AddProfileConfiguration(configuration, 0); + category.AddProfileConfiguration(configuration, category.ProfileConfigurations.FirstOrDefault()); SaveProfileCategory(category); return configuration; } @@ -354,7 +354,7 @@ internal class ProfileService : IProfileService } /// - public async Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, int targetIndex = 0) + public async Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, ProfileConfiguration? target) { using ZipArchive archive = new(archiveStream, ZipArchiveMode.Read, true); @@ -424,7 +424,7 @@ internal class ProfileService : IProfileService profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}"; profileConfiguration.Entity.ProfileConfiguration.ProfileId = profileEntity.Id; - category.AddProfileConfiguration(profileConfiguration, targetIndex); + category.AddProfileConfiguration(profileConfiguration, target); List modules = _pluginManagementService.GetFeaturesOfType(); profileConfiguration.LoadModules(modules); @@ -436,7 +436,7 @@ internal class ProfileService : IProfileService /// public async Task OverwriteProfile(MemoryStream archiveStream, ProfileConfiguration profileConfiguration) { - ProfileConfiguration imported = await ImportProfile(archiveStream, profileConfiguration.Category, true, true, null, profileConfiguration.Order + 1); + ProfileConfiguration imported = await ImportProfile(archiveStream, profileConfiguration.Category, true, true, null, profileConfiguration); RemoveProfileConfiguration(profileConfiguration); SaveProfileCategory(imported.Category); diff --git a/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs b/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs index ec0d6c091..aaeecd3bd 100644 --- a/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs +++ b/src/Artemis.Storage.Legacy/Entities/Workshop/EntryEntity.cs @@ -1,4 +1,6 @@ -namespace Artemis.Storage.Legacy.Entities.Workshop; +using System.Text.Json.Nodes; + +namespace Artemis.Storage.Legacy.Entities.Workshop; internal class EntryEntity { @@ -14,7 +16,7 @@ internal class EntryEntity public string ReleaseVersion { get; set; } = string.Empty; public DateTimeOffset InstalledAt { get; set; } - public Dictionary? Metadata { get; set; } + public Dictionary? Metadata { get; set; } public Storage.Entities.Workshop.EntryEntity Migrate() { @@ -29,7 +31,7 @@ internal class EntryEntity ReleaseId = ReleaseId, ReleaseVersion = ReleaseVersion, InstalledAt = InstalledAt, - Metadata = Metadata ?? new Dictionary() + Metadata = Metadata ?? new Dictionary() }; } } \ No newline at end of file diff --git a/src/Artemis.Storage/ArtemisDbContext.cs b/src/Artemis.Storage/ArtemisDbContext.cs index 2a77604fa..32acba6a6 100644 --- a/src/Artemis.Storage/ArtemisDbContext.cs +++ b/src/Artemis.Storage/ArtemisDbContext.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; +using System.Text.Json.Nodes; using Artemis.Storage.Entities.General; using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Profile; @@ -41,7 +42,7 @@ public class ArtemisDbContext : DbContext .Property(e => e.Metadata) .HasConversion( v => JsonSerializer.Serialize(v, JsonSerializerOptions), - v => JsonSerializer.Deserialize>(v, JsonSerializerOptions) ?? new Dictionary()); + v => JsonSerializer.Deserialize>(v, JsonSerializerOptions) ?? new Dictionary()); modelBuilder.Entity() .Property(e => e.ProfileConfiguration) diff --git a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs index 37cdd4cfa..ad11d6188 100644 --- a/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs +++ b/src/Artemis.Storage/Entities/Workshop/EntryEntity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json.Nodes; using Microsoft.EntityFrameworkCore; namespace Artemis.Storage.Entities.Workshop; @@ -19,5 +20,5 @@ public class EntryEntity public string ReleaseVersion { get; set; } = string.Empty; public DateTimeOffset InstalledAt { get; set; } - public Dictionary? Metadata { get; set; } + public Dictionary? Metadata { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ProfileRepository.cs b/src/Artemis.Storage/Repositories/ProfileRepository.cs index 78c9ca6a0..65e7dab78 100644 --- a/src/Artemis.Storage/Repositories/ProfileRepository.cs +++ b/src/Artemis.Storage/Repositories/ProfileRepository.cs @@ -8,10 +8,11 @@ using Artemis.Storage.Exceptions; using Artemis.Storage.Migrations; using Artemis.Storage.Repositories.Interfaces; using Microsoft.EntityFrameworkCore; +using Serilog; namespace Artemis.Storage.Repositories; -public class ProfileRepository(Func getContext, List profileMigrators) : IProfileRepository +public class ProfileRepository(ILogger logger, Func getContext, List profileMigrators) : IProfileRepository { public void Add(ProfileContainerEntity profileContainerEntity) { @@ -83,6 +84,8 @@ public class ProfileRepository(Func getContext, List()) continue; + + logger.Information("Migrating profile from version {OldVersion} to {NewVersion}", configurationJson["Version"], profileMigrator.Version); profileMigrator.Migrate(configurationJson, profileJson); configurationJson["Version"] = profileMigrator.Version; diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs index 2ee9934d6..6b7fd2a02 100644 --- a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs +++ b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs @@ -47,7 +47,7 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable : new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; } else if (ConfigurationIcon.IconBytes != null) - Dispatcher.UIThread.Post(() => LoadFromBitmap(ConfigurationIcon, new MemoryStream(ConfigurationIcon.IconBytes)), DispatcherPriority.ApplicationIdle); + Dispatcher.UIThread.Post(LoadFromBitmap, DispatcherPriority.ApplicationIdle); else Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; } @@ -57,14 +57,17 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable } } - private void LoadFromBitmap(Core.ProfileConfigurationIcon configurationIcon, Stream stream) + private void LoadFromBitmap() { try { - _stream = stream; - if (!configurationIcon.Fill) + if (ConfigurationIcon?.IconBytes == null) + return; + + _stream = new MemoryStream(ConfigurationIcon.IconBytes); + if (!ConfigurationIcon.Fill) { - Content = new Image {Source = new Bitmap(stream)}; + Content = new Image {Source = new Bitmap(_stream)}; return; } @@ -73,7 +76,7 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable Background = TextElement.GetForeground(this), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, - OpacityMask = new ImageBrush(new Bitmap(stream)) + OpacityMask = new ImageBrush(new Bitmap(_stream)) }; } catch (Exception) diff --git a/src/Artemis.UI/Screens/Sidebar/Behaviors/ProfileConfigurationDropHandler.cs b/src/Artemis.UI/Screens/Sidebar/Behaviors/ProfileConfigurationDropHandler.cs index 8e4d4d108..b07a43196 100644 --- a/src/Artemis.UI/Screens/Sidebar/Behaviors/ProfileConfigurationDropHandler.cs +++ b/src/Artemis.UI/Screens/Sidebar/Behaviors/ProfileConfigurationDropHandler.cs @@ -68,8 +68,8 @@ public class SidebarCategoryViewDropHandler : DropHandlerBase { int index = vm.ProfileConfigurations.IndexOf(targetItem); if (!before) - index++; - vm.AddProfileConfiguration(sourceItem.ProfileConfiguration, index); + targetItem = index < vm.ProfileConfigurations.Count - 1 ? vm.ProfileConfigurations[index + 1] : null; + vm.AddProfileConfiguration(sourceItem.ProfileConfiguration, targetItem?.ProfileConfiguration); } else { diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs index 3892fee70..fe6cf2d9e 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs @@ -103,10 +103,10 @@ public partial class SidebarCategoryViewModel : ActivatableViewModelBase public bool IsCollapsed => _isCollapsed?.Value ?? false; public bool IsSuspended => _isSuspended?.Value ?? false; - public void AddProfileConfiguration(ProfileConfiguration profileConfiguration, int? index) + public void AddProfileConfiguration(ProfileConfiguration profileConfiguration, ProfileConfiguration? target) { ProfileCategory oldCategory = profileConfiguration.Category; - ProfileCategory.AddProfileConfiguration(profileConfiguration, index); + ProfileCategory.AddProfileConfiguration(profileConfiguration, target); _profileService.SaveProfileCategory(ProfileCategory); // If the profile moved to a new category, also save the old category diff --git a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs index 90aa4108e..ce90444b0 100644 --- a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs +++ b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs @@ -1,4 +1,6 @@ using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Nodes; using Artemis.Core; using Artemis.Storage.Entities.Workshop; @@ -6,7 +8,7 @@ namespace Artemis.WebClient.Workshop.Models; public class InstalledEntry { - private Dictionary _metadata = new(); + private Dictionary _metadata = new(); internal InstalledEntry(EntryEntity entity) { @@ -52,7 +54,7 @@ public class InstalledEntry ReleaseVersion = Entity.ReleaseVersion; InstalledAt = Entity.InstalledAt; - _metadata = Entity.Metadata != null ? new Dictionary(Entity.Metadata) : new Dictionary(); + _metadata = Entity.Metadata != null ? new Dictionary(Entity.Metadata) : new Dictionary(); } internal void Save() @@ -67,7 +69,7 @@ public class InstalledEntry Entity.ReleaseVersion = ReleaseVersion; Entity.InstalledAt = InstalledAt; - Entity.Metadata = new Dictionary(_metadata); + Entity.Metadata = new Dictionary(_metadata); } /// @@ -80,14 +82,14 @@ public class InstalledEntry /// if the metadata contains an element with the specified key; otherwise, . public bool TryGetMetadata(string key, [NotNullWhen(true)] out T? value) { - if (!_metadata.TryGetValue(key, out object? objectValue) || objectValue is not T result) + if (!_metadata.TryGetValue(key, out JsonNode? element)) { value = default; return false; } - value = result; - return true; + value = element.GetValue(); + return value != null; } /// @@ -97,7 +99,7 @@ public class InstalledEntry /// The value to set. public void SetMetadata(string key, object value) { - _metadata[key] = value; + _metadata[key] = JsonSerializer.SerializeToNode(value) ?? throw new InvalidOperationException(); } ///