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:
parent
f66df43cc2
commit
d7a0c2ac4a
@ -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);
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -71,6 +71,7 @@ internal class CoreService : ICoreService
|
||||
// Initialize the services
|
||||
_pluginManagementService.CopyBuiltInPlugins();
|
||||
_pluginManagementService.LoadPlugins(IsElevated);
|
||||
_pluginManagementService.StartHotReload();
|
||||
_renderService.Initialize();
|
||||
|
||||
OnInitialized();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user