1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

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
This commit is contained in:
Robert 2024-03-12 21:26:47 +01:00
parent f66df43cc2
commit d7a0c2ac4a
15 changed files with 91 additions and 82 deletions

View File

@ -98,26 +98,15 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel
/// <summary>
/// Adds a profile configuration to this category
/// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
public void AddProfileConfiguration(ProfileConfiguration configuration, ProfileConfiguration? target)
{
List<ProfileConfiguration> 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<ProfileConfiguration> 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<ProfileConfiguration>(targetList);

View File

@ -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;
}
/// <inheritdoc />

View File

@ -71,6 +71,7 @@ internal class CoreService : ICoreService
// Initialize the services
_pluginManagementService.CopyBuiltInPlugins();
_pluginManagementService.LoadPlugins(IsElevated);
_pluginManagementService.StartHotReload();
_renderService.Initialize();
OnInitialized();

View File

@ -16,7 +16,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// Gets a list containing additional directories in which plugins are located, used while loading plugins.
/// </summary>
List<DirectoryInfo> AdditionalPluginDirectories { get; }
/// <summary>
/// Indicates whether or not plugins are currently being loaded
/// </summary>
@ -33,6 +33,11 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// </summary>
void LoadPlugins(bool isElevated);
/// <summary>
/// Starts monitoring plugin directories for changes and reloads plugins when changes are detected
/// </summary>
void StartHotReload();
/// <summary>
/// Unloads all installed plugins.
/// </summary>
@ -145,7 +150,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// <param name="device"></param>
/// <returns></returns>
DeviceProvider GetDeviceProviderByDevice(IRGBDevice device);
/// <summary>
/// Occurs when built-in plugins are being loaded
/// </summary>

View File

@ -41,37 +41,6 @@ internal class PluginManagementService : IPluginManagementService
_pluginRepository = pluginRepository;
_deviceRepository = deviceRepository;
_plugins = new List<Plugin>();
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<DirectoryInfo> 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<Plugin> 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<Type> 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.

View File

@ -115,9 +115,9 @@ public interface IProfileService : IArtemisService
/// any changes are made to it.
/// </param>
/// <param name="nameAffix">Text to add after the name of the profile (separated by a dash).</param>
/// <param name="targetIndex">The index at which to import the profile into the category.</param>
/// <param name="target">The profile before which to import the profile into the category.</param>
/// <returns>The resulting profile configuration.</returns>
Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", int targetIndex = 0);
Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", ProfileConfiguration? target = null);
/// <summary>
/// Imports the provided ZIP archive stream into the provided profile configuration

View File

@ -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
}
/// <inheritdoc />
public async Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, int targetIndex = 0)
public async Task<ProfileConfiguration> 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<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
profileConfiguration.LoadModules(modules);
@ -436,7 +436,7 @@ internal class ProfileService : IProfileService
/// <inheritdoc />
public async Task<ProfileConfiguration> 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);

View File

@ -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<string, object>? Metadata { get; set; }
public Dictionary<string, JsonNode>? 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<string, object>()
Metadata = Metadata ?? new Dictionary<string, JsonNode>()
};
}
}

View File

@ -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<Dictionary<string, object>>(v, JsonSerializerOptions) ?? new Dictionary<string, object>());
v => JsonSerializer.Deserialize<Dictionary<string, JsonNode>>(v, JsonSerializerOptions) ?? new Dictionary<string, JsonNode>());
modelBuilder.Entity<ProfileContainerEntity>()
.Property(e => e.ProfileConfiguration)

View File

@ -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<string, object>? Metadata { get; set; }
public Dictionary<string, JsonNode>? Metadata { get; set; }
}

View File

@ -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<ArtemisDbContext> getContext, List<IProfileMigration> profileMigrators) : IProfileRepository
public class ProfileRepository(ILogger logger, Func<ArtemisDbContext> getContext, List<IProfileMigration> profileMigrators) : IProfileRepository
{
public void Add(ProfileContainerEntity profileContainerEntity)
{
@ -83,6 +84,8 @@ public class ProfileRepository(Func<ArtemisDbContext> getContext, List<IProfileM
{
if (profileMigrator.Version <= configurationJson["Version"]!.GetValue<int>())
continue;
logger.Information("Migrating profile from version {OldVersion} to {NewVersion}", configurationJson["Version"], profileMigrator.Version);
profileMigrator.Migrate(configurationJson, profileJson);
configurationJson["Version"] = profileMigrator.Version;

View File

@ -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)

View File

@ -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
{

View File

@ -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

View File

@ -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<string, object> _metadata = new();
private Dictionary<string, JsonNode> _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<string, object>(Entity.Metadata) : new Dictionary<string, object>();
_metadata = Entity.Metadata != null ? new Dictionary<string, JsonNode>(Entity.Metadata) : new Dictionary<string, JsonNode>();
}
internal void Save()
@ -67,7 +69,7 @@ public class InstalledEntry
Entity.ReleaseVersion = ReleaseVersion;
Entity.InstalledAt = InstalledAt;
Entity.Metadata = new Dictionary<string, object>(_metadata);
Entity.Metadata = new Dictionary<string, JsonNode>(_metadata);
}
/// <summary>
@ -80,14 +82,14 @@ public class InstalledEntry
/// <returns><see langword="true"/> if the metadata contains an element with the specified key; otherwise, <see langword="false"/>.</returns>
public bool TryGetMetadata<T>(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<T>();
return value != null;
}
/// <summary>
@ -97,7 +99,7 @@ public class InstalledEntry
/// <param name="value">The value to set.</param>
public void SetMetadata(string key, object value)
{
_metadata[key] = value;
_metadata[key] = JsonSerializer.SerializeToNode(value) ?? throw new InvalidOperationException();
}
/// <summary>