From 0bd82ad6d5196dbd045b0ba4b3b1d1bfd61596fc Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 9 Aug 2025 19:27:27 +0200 Subject: [PATCH] Replaced `SettingsLocker` with `ManagedConfiguration` for improved extensibility and streamlined property management. Refactored related components and logic. --- .../Settings/SettingsPanelApp.razor | 2 +- .../Components/Settings/SettingsPanelBase.cs | 3 - .../Layout/MainLayout.razor.cs | 4 + app/MindWork AI Studio/Program.cs | 1 - app/MindWork AI Studio/Settings/ConfigMeta.cs | 88 ++++++++ .../Settings/ConfigMetaBase.cs | 6 + .../Settings/DataModel/Data.cs | 2 +- .../Settings/DataModel/DataApp.cs | 19 +- .../DataModel/PreviousModels/DataV4.cs | 2 +- .../Settings/ExpressionExtensions.cs | 2 +- app/MindWork AI Studio/Settings/IConfig.cs | 3 + .../Settings/ManagedConfiguration.cs | 198 ++++++++++++++++++ app/MindWork AI Studio/Settings/NoConfig.cs | 12 ++ .../Settings/SettingsLocker.cs | 76 ------- .../Settings/SettingsManager.cs | 4 +- .../Settings/SettingsMigrations.cs | 2 +- .../Tools/PluginSystem/PluginConfiguration.cs | 38 ++-- .../PluginSystem/PluginFactory.Loading.cs | 30 ++- .../PluginSystem/PluginFactory.Starting.cs | 2 +- .../Tools/PluginSystem/PluginFactory.cs | 1 - 20 files changed, 365 insertions(+), 130 deletions(-) create mode 100644 app/MindWork AI Studio/Settings/ConfigMeta.cs create mode 100644 app/MindWork AI Studio/Settings/ConfigMetaBase.cs create mode 100644 app/MindWork AI Studio/Settings/IConfig.cs create mode 100644 app/MindWork AI Studio/Settings/ManagedConfiguration.cs create mode 100644 app/MindWork AI Studio/Settings/NoConfig.cs delete mode 100644 app/MindWork AI Studio/Settings/SettingsLocker.cs diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index e220d41d..fc466e7a 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -13,7 +13,7 @@ - + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs index dded906c..bad3fca3 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs @@ -15,7 +15,4 @@ public abstract class SettingsPanelBase : MSGComponentBase [Inject] protected RustService RustService { get; init; } = null!; - - [Inject] - protected SettingsLocker SettingsLocker { get; init; } = null!; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 2367aff4..cc115a2c 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -215,7 +215,11 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseEnvironment.ConfigurationId, enterpriseEnvironment.ConfigurationServerUrl); // Load (but not start) all plugins without waiting for them: + #if DEBUG + var pluginLoadingTimeout = new CancellationTokenSource(); + #else var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + #endif await PluginFactory.LoadAll(pluginLoadingTimeout.Token); // Set up hot reloading for plugins: diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 072dd3ad..a4d9c2b4 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -126,7 +126,6 @@ internal sealed class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/app/MindWork AI Studio/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs new file mode 100644 index 00000000..f8d50ecc --- /dev/null +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -0,0 +1,88 @@ +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +/// +/// Represents configuration metadata for a specific class and property. +/// +/// The class type that contains the configuration property. +/// The type of the configuration property value. +public record ConfigMeta : ConfigMetaBase +{ + public ConfigMeta(Expression> configSelection, Expression> propertyExpression) + { + this.ConfigSelection = configSelection; + this.PropertyExpression = propertyExpression; + } + + /// + /// The expression to select the configuration class from the settings data. + /// + private Expression> ConfigSelection { get; } + + /// + /// The expression to select the property within the configuration class. + /// + private Expression> PropertyExpression { get; } + + /// + /// Indicates whether the configuration is managed by a plugin and is therefore locked. + /// + public bool IsLocked { get; private set; } + + /// + /// The ID of the plugin that manages this configuration. This is set when the configuration is locked. + /// + public Guid MangedByConfigPluginId { get; private set; } + + /// + /// The default value for the configuration property. This is used when resetting the property to its default state. + /// + public required TValue Default { get; init; } + + /// + /// Locks the configuration state, indicating that it is managed by a specific plugin. + /// + /// The ID of the plugin that is managing this configuration. + public void LockManagedState(Guid pluginId) + { + this.IsLocked = true; + this.MangedByConfigPluginId = pluginId; + } + + /// + /// Resets the managed state of the configuration, allowing it to be modified again. + /// This will also reset the property to its default value. + /// + public void ResetManagedState() + { + this.IsLocked = false; + this.MangedByConfigPluginId = Guid.Empty; + this.Reset(); + } + + /// + /// Resets the configuration property to its default value. + /// + public void Reset() + { + var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var memberExpression = this.PropertyExpression.GetMemberExpression(); + if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo) + propertyInfo.SetValue(configInstance, this.Default); + } + + /// + /// Sets the value of the configuration property to the specified value. + /// + /// The value to set for the configuration property. + public void SetValue(TValue value) + { + var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var memberExpression = this.PropertyExpression.GetMemberExpression(); + if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo) + propertyInfo.SetValue(configInstance, value); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ConfigMetaBase.cs b/app/MindWork AI Studio/Settings/ConfigMetaBase.cs new file mode 100644 index 00000000..4ef74e88 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ConfigMetaBase.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Settings; + +public abstract record ConfigMetaBase : IConfig +{ + protected static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index 695d2ad8..0e825aa0 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -71,7 +71,7 @@ public sealed class Data /// public uint NextChatTemplateNum { get; set; } = 1; - public DataApp App { get; init; } = new(); + public DataApp App { get; init; } = new(x => x.App); public DataChat Chat { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index 022cd8c8..477afa30 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -1,7 +1,16 @@ +using System.Linq.Expressions; + namespace AIStudio.Settings.DataModel; -public sealed class DataApp +public sealed class DataApp(Expression>? configSelection = null) { + /// + /// The default constructor for the JSON deserializer. + /// + public DataApp() : this(null) + { + } + /// /// The language behavior. /// @@ -21,7 +30,7 @@ public sealed class DataApp /// Should we save energy? When true, we will update content streamed /// from the server, i.e., AI, less frequently. /// - public bool IsSavingEnergy { get; set; } + public bool IsSavingEnergy { get; set; } = ManagedConfiguration.Register(configSelection, n => n.IsSavingEnergy, false); /// /// Should we enable spellchecking for all input fields? @@ -31,7 +40,7 @@ public sealed class DataApp /// /// If and when we should look for updates. /// - public UpdateBehavior UpdateBehavior { get; set; } = UpdateBehavior.HOURLY; + public UpdateBehavior UpdateBehavior { get; set; } = ManagedConfiguration.Register(configSelection, n => n.UpdateBehavior, UpdateBehavior.HOURLY); /// /// The navigation behavior. @@ -41,7 +50,7 @@ public sealed class DataApp /// /// The visibility setting for previews features. /// - public PreviewVisibility PreviewVisibility { get; set; } = PreviewVisibility.NONE; + public PreviewVisibility PreviewVisibility { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreviewVisibility, PreviewVisibility.NONE); /// /// The enabled preview features. @@ -66,5 +75,5 @@ public sealed class DataApp /// /// Should the user be allowed to add providers? /// - public bool AllowUserToAddProvider { get; set; } = true; + public bool AllowUserToAddProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddProvider, true); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs index ecaebe8a..61555a3c 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs @@ -33,7 +33,7 @@ public sealed class DataV4 /// public uint NextProfileNum { get; set; } = 1; - public DataApp App { get; init; } = new(); + public DataApp App { get; init; } = new(x => x.App); public DataChat Chat { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/ExpressionExtensions.cs b/app/MindWork AI Studio/Settings/ExpressionExtensions.cs index 582bf65c..5d567c03 100644 --- a/app/MindWork AI Studio/Settings/ExpressionExtensions.cs +++ b/app/MindWork AI Studio/Settings/ExpressionExtensions.cs @@ -6,7 +6,7 @@ public static class ExpressionExtensions { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions)); - public static MemberExpression GetMemberExpression(this Expression> expression) + public static MemberExpression GetMemberExpression(this Expression> expression) { switch (expression.Body) { diff --git a/app/MindWork AI Studio/Settings/IConfig.cs b/app/MindWork AI Studio/Settings/IConfig.cs new file mode 100644 index 00000000..c9db8958 --- /dev/null +++ b/app/MindWork AI Studio/Settings/IConfig.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Settings; + +public interface IConfig; \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs new file mode 100644 index 00000000..3767cd66 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs @@ -0,0 +1,198 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; +using AIStudio.Tools.PluginSystem; + +using Lua; + +namespace AIStudio.Settings; + +public static class ManagedConfiguration +{ + private static readonly ConcurrentDictionary METADATA = new(); + + /// + /// Registers a configuration setting with a default value. + /// + /// + /// When called from the JSON deserializer, the configSelection parameter will be null. + /// In this case, the method will return the default value without registering the setting. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The default value to use when the setting is not configured. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// The default value. + public static TValue Register(Expression>? configSelection, Expression> propertyExpression, TValue defaultValue) + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if(configSelection is null) + return defaultValue; + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return defaultValue; + + METADATA[configPath] = new ConfigMeta(configSelection, propertyExpression) + { + Default = defaultValue, + }; + + return defaultValue; + } + + /// + /// Attempts to retrieve the configuration metadata for a given configuration selection and property expression. + /// + /// + /// When no configuration metadata is found, it returns a NoConfig instance with the default value set to default(TValue). + /// This allows the caller to handle the absence of configuration gracefully. In such cases, the return value of the method will be false. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The output parameter that will hold the configuration metadata if found. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True if the configuration metadata was found, otherwise false. + public static bool TryGet(Expression> configSelection, Expression> propertyExpression, out ConfigMeta configMeta) + { + var configPath = Path(configSelection, propertyExpression); + if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta meta) + { + configMeta = meta; + return true; + } + + configMeta = new NoConfig(configSelection, propertyExpression) + { + Default = default!, + }; + + return false; + } + + /// + /// Attempts to process the configuration settings from a Lua table. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// The ID of the related configuration plugin. + /// The Lua table containing the settings to process. + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// When true, the method will not apply any changes, but only check if the configuration can be read. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration(Expression> configSelection, Expression> propertyExpression, Guid configPluginId, LuaTable settings, bool dryRun) + { + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var (configuredValue, successful) = configMeta.Default switch + { + Enum => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredEnumValue) && configuredEnumValue.TryRead(out var configuredEnumText) && Enum.TryParse(typeof(TValue), configuredEnumText, true, out var configuredEnum) ? ((TValue)configuredEnum, true) : (configMeta.Default, false), + Guid => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredGuidValue) && configuredGuidValue.TryRead(out var configuredGuidText) && Guid.TryParse(configuredGuidText, out var configuredGuid) ? ((TValue)(object)configuredGuid, true) : (configMeta.Default, false), + + string => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredTextValue) && configuredTextValue.TryRead(out var configuredText) ? ((TValue)(object)configuredText, true) : (configMeta.Default, false), + bool => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredBoolValue) && configuredBoolValue.TryRead(out var configuredState) ? ((TValue)(object)configuredState, true) : (configMeta.Default, false), + + int => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredIntValue) && configuredIntValue.TryRead(out var configuredInt) ? ((TValue)(object)configuredInt, true) : (configMeta.Default, false), + double => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredDoubleValue) && configuredDoubleValue.TryRead(out var configuredDouble) ? ((TValue)(object)configuredDouble, true) : (configMeta.Default, false), + float => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredFloatValue) && configuredFloatValue.TryRead(out var configuredFloat) ? ((TValue)(object)configuredFloat, true) : (configMeta.Default, false), + + _ => (configMeta.Default, false), + }; + + if(dryRun) + return successful; + + switch (successful) + { + case true: + // + // Case: the setting was configured, and we could read the value successfully. + // + configMeta.SetValue(configuredValue); + configMeta.LockManagedState(configPluginId); + break; + + case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId: + // + // Case: the setting was configured previously, but we could not read the value successfully. + // This happens when the setting was removed from the configuration plugin. We handle that + // case only when the setting was locked and managed by the same configuration plugin. + // + // The other case, when the setting was locked and managed by a different configuration plugin, + // is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin + // is still available. If it is not available, it resets the managed state of the + // configuration setting, allowing it to be reconfigured by a different plugin or left unchanged. + // + configMeta.ResetManagedState(); + break; + + case false: + // + // Case: the setting was not configured, or we could not read the value successfully. + // We do not change the setting, and it remains at whatever value it had before. + // + break; + } + + return successful; + } + + /// + /// Checks if a configuration setting is left over from a configuration plugin that is no longer available. + /// If the configuration setting is locked and managed by a configuration plugin that is not available, + /// it resets the managed state of the configuration setting and returns true. + /// Otherwise, it returns false. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The collection of available plugins to check against. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True if the configuration setting is left over and was reset, otherwise false. + public static bool IsConfigurationLeftOver(Expression> configSelection, Expression> propertyExpression, IEnumerable availablePlugins) + { + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + if(configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + return false; + + // Check if the configuration plugin ID is valid against the available plugin IDs: + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + if (plugin is null) + { + // Remove the locked state: + configMeta.ResetManagedState(); + return true; + } + + return false; + } + + private static string Path(Expression> configSelection, Expression> propertyExpression) + { + var className = typeof(TClass).Name; + + var memberExpressionConfig = configSelection.GetMemberExpression(); + var configName = memberExpressionConfig.Member.Name; + + var memberExpressionProperty = propertyExpression.GetMemberExpression(); + var propertyName = memberExpressionProperty.Member.Name; + + var configPath = $"{configName}.{className}.{propertyName}"; + return configPath; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/NoConfig.cs b/app/MindWork AI Studio/Settings/NoConfig.cs new file mode 100644 index 00000000..79a1f9dd --- /dev/null +++ b/app/MindWork AI Studio/Settings/NoConfig.cs @@ -0,0 +1,12 @@ +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +public sealed record NoConfig : ConfigMeta +{ + public NoConfig(Expression> configSelection, Expression> propertyExpression) : base(configSelection, propertyExpression) + { + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsLocker.cs b/app/MindWork AI Studio/Settings/SettingsLocker.cs deleted file mode 100644 index cc4a918f..00000000 --- a/app/MindWork AI Studio/Settings/SettingsLocker.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Linq.Expressions; - -namespace AIStudio.Settings; - -public sealed class SettingsLocker -{ - private readonly Dictionary> lockedProperties = new(); - - /// - /// Registers a property of a class to be locked by a specific configuration plugin ID. - /// - /// The property expression to lock. - /// The ID of the configuration plugin that locks the property. - /// The type of the class that contains the property. - public void Register(Expression> propertyExpression, Guid configurationPluginId) - { - var memberExpression = propertyExpression.GetMemberExpression(); - var className = typeof(T).Name; - var propertyName = memberExpression.Member.Name; - - if (!this.lockedProperties.ContainsKey(className)) - this.lockedProperties[className] = []; - - this.lockedProperties[className].TryAdd(propertyName, configurationPluginId); - } - - /// - /// Removes the lock for a property of a class. - /// - /// The property expression to remove the lock for. - /// The type of the class that contains the property. - public void Remove(Expression> propertyExpression) - { - var memberExpression = propertyExpression.GetMemberExpression(); - var className = typeof(T).Name; - var propertyName = memberExpression.Member.Name; - - if (this.lockedProperties.TryGetValue(className, out var props)) - { - if (props.Remove(propertyName)) - { - // If the property was removed, check if the class has no more locked properties: - if (props.Count == 0) - this.lockedProperties.Remove(className); - } - } - } - - /// - /// Gets the configuration plugin ID that locks a specific property of a class. - /// - /// - /// - /// - public Guid GetConfigurationPluginId(Expression> propertyExpression) - { - var memberExpression = propertyExpression.GetMemberExpression(); - var className = typeof(T).Name; - var propertyName = memberExpression.Member.Name; - - if (this.lockedProperties.TryGetValue(className, out var props) && props.TryGetValue(propertyName, out var configurationPluginId)) - return configurationPluginId; - - // No configuration plugin ID found for this property: - return Guid.Empty; - } - - public bool IsLocked(Expression> propertyExpression) - { - var memberExpression = propertyExpression.GetMemberExpression(); - var className = typeof(T).Name; - var propertyName = memberExpression.Member.Name; - - return this.lockedProperties.TryGetValue(className, out var props) && props.ContainsKey(propertyName); - } -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index cb376479..059d0f12 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -349,7 +349,7 @@ public sealed class SettingsManager } } - public static string ToSettingName(Expression> propertyExpression) + public static string ToSettingName(Expression> propertyExpression) { MemberExpression? memberExpr; @@ -363,6 +363,6 @@ public sealed class SettingsManager throw new ArgumentException("Expression must be a property access", nameof(propertyExpression)); // Return the full name of the property, including the class name: - return $"{typeof(T).Name}.{memberExpr.Member.Name}"; + return $"{typeof(TIn).Name}.{memberExpr.Member.Name}"; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsMigrations.cs b/app/MindWork AI Studio/Settings/SettingsMigrations.cs index 98482ceb..7c5a4293 100644 --- a/app/MindWork AI Studio/Settings/SettingsMigrations.cs +++ b/app/MindWork AI Studio/Settings/SettingsMigrations.cs @@ -137,7 +137,7 @@ public static class SettingsMigrations Providers = previousConfig.Providers, NextProviderNum = previousConfig.NextProviderNum, - App = new() + App = new(x => x.App) { EnableSpellchecking = previousConfig.EnableSpellchecking, IsSavingEnergy = previousConfig.IsSavingEnergy, diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 1988e8de..7de7137f 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -1,6 +1,5 @@ using AIStudio.Provider; using AIStudio.Settings; -using AIStudio.Settings.DataModel; using Lua; @@ -13,24 +12,27 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration)); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); - private static readonly SettingsLocker SETTINGS_LOCKER = Program.SERVICE_PROVIDER.GetRequiredService(); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); - public async Task InitializeAsync() + public async Task InitializeAsync(bool dryRun) { - if(!this.TryProcessConfiguration(out var issue)) + if(!this.TryProcessConfiguration(dryRun, out var issue)) this.pluginIssues.Add(issue); - - await SETTINGS_MANAGER.StoreSettings(); - await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED); + + if (!dryRun) + { + await SETTINGS_MANAGER.StoreSettings(); + await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED); + } } - + /// /// Tries to initialize the UI text content of the plugin. /// + /// When true, the method will not apply any changes, but only check if the configuration can be read. /// The error message, when the UI text content could not be read. /// True, when the UI text content could be read successfully. - private bool TryProcessConfiguration(out string message) + private bool TryProcessConfiguration(bool dryRun, out string message) { // Ensure that the main CONFIG table exists and is a valid Lua table: if (!this.state.Environment["CONFIG"].TryRead(out var mainTable)) @@ -40,25 +42,21 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT } // + // =========================================== // Configured settings + // =========================================== // if (!mainTable.TryGetValue("SETTINGS", out var settingsValue) || !settingsValue.TryRead(out var settingsTable)) { message = TB("The SETTINGS table does not exist or is not a valid table."); return false; } - - if (settingsTable.TryGetValue(SettingsManager.ToSettingName(x => x.UpdateBehavior), out var updateBehaviorValue) && updateBehaviorValue.TryRead(out var updateBehaviorText) && Enum.TryParse(updateBehaviorText, true, out var updateBehavior)) - { - SETTINGS_LOCKER.Register(x => x.UpdateBehavior, this.Id); - SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior = updateBehavior; - } - if (settingsTable.TryGetValue(SettingsManager.ToSettingName(x => x.AllowUserToAddProvider), out var dontAllowUserToAddProviderValue) && dontAllowUserToAddProviderValue.TryRead(out var dontAllowUserToAddProviderEntry)) - { - SETTINGS_LOCKER.Register(x => x.AllowUserToAddProvider, this.Id); - SETTINGS_MANAGER.ConfigurationData.App.AllowUserToAddProvider = dontAllowUserToAddProviderEntry; - } + // Check for updates, and if so, how often? + ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateBehavior, this.Id, settingsTable, dryRun); + + // Allow the user to add providers? + ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); // // Configured providers diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 244e3984..667ed867 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -1,5 +1,6 @@ using System.Text; +using AIStudio.Settings; using AIStudio.Settings.DataModel; using Lua; @@ -120,8 +121,10 @@ public static partial class PluginFactory } // + // ========================================================= // Next, we have to clean up our settings. It is possible that a configuration plugin was removed. // We have to remove the related settings as well: + // ========================================================= // var wasConfigurationChanged = false; @@ -150,23 +153,18 @@ public static partial class PluginFactory #pragma warning restore MWAIS0001 // + // ========================================================== // Check all possible settings: + // ========================================================== // - if (SETTINGS_LOCKER.GetConfigurationPluginId(x => x.UpdateBehavior) is var updateBehaviorPluginId && updateBehaviorPluginId != Guid.Empty) - { - var sourcePlugin = AVAILABLE_PLUGINS.FirstOrDefault(plugin => plugin.Id == updateBehaviorPluginId); - if (sourcePlugin is null) - { - // Remove the locked state: - SETTINGS_LOCKER.Remove(x => x.UpdateBehavior); - - // Reset the setting to the default value: - SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior = UpdateBehavior.HOURLY; - - LOG.LogWarning($"The configured update behavior is based on a plugin that is not available anymore. Resetting the setting to the default value: {SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior}."); - wasConfigurationChanged = true; - } - } + + // Check for updates, and if so, how often? + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateBehavior, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + // Allow the user to add providers? + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; if (wasConfigurationChanged) { @@ -225,7 +223,7 @@ public static partial class PluginFactory case PluginType.CONFIGURATION: var configPlug = new PluginConfiguration(isInternal, state, type); - await configPlug.InitializeAsync(); + await configPlug.InitializeAsync(true); return configPlug; default: diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs index 983b84da..0943a48e 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs @@ -103,7 +103,7 @@ public static partial class PluginFactory languagePlugin.SetBaseLanguage(BASE_LANGUAGE_PLUGIN); if(plugin is PluginConfiguration configPlugin) - await configPlugin.InitializeAsync(); + await configPlugin.InitializeAsync(false); LOG.LogInformation($"Successfully started plugin: Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}'"); return plugin; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 2d7b38b0..1fd30fb2 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -6,7 +6,6 @@ public static partial class PluginFactory { private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory)); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); - private static readonly SettingsLocker SETTINGS_LOCKER = Program.SERVICE_PROVIDER.GetRequiredService(); private static bool IS_INITIALIZED; private static string DATA_DIR = string.Empty;