using System.Collections.Concurrent; using System.Linq.Expressions; using AIStudio.Settings.DataModel; using AIStudio.Tools.PluginSystem; namespace AIStudio.Settings; public static partial class ManagedConfiguration { private static readonly ConcurrentDictionary METADATA = new(); private static SettingsManager SettingsManagerAccess => Program.SERVICE_PROVIDER.GetRequiredService(); private static ILogger Log => Program.LOGGER_FACTORY.CreateLogger(nameof(ManagedConfiguration)); /// /// Attempts to retrieve the configuration metadata for a given configuration selection and /// property expression (enum-based). /// /// /// 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) where TValue : Enum { var configPath = Path(configSelection, propertyExpression); if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta meta) { meta.RestoreLockedConfiguration(); configMeta = meta; return true; } configMeta = new NoConfig(configSelection, propertyExpression) { Default = default!, }; return false; } /// /// Attempts to retrieve the configuration metadata for a given configuration selection and /// property expression (string-based). /// /// /// 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. /// 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) { meta.RestoreLockedConfiguration(); configMeta = meta; return true; } configMeta = new NoConfig(configSelection, propertyExpression) { Default = string.Empty, }; return false; } /// /// Attempts to retrieve the configuration metadata for a given configuration selection and /// property expression (ISpanParsable-based). /// /// /// 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. /// An optional parameter to help with method overload resolution. /// The type of the configuration class. /// The type of the property within the configuration class. /// True if the configuration metadata was found, otherwise false. // ReSharper disable MethodOverloadWithOptionalParameter public static bool TryGet(Expression> configSelection, Expression> propertyExpression, out ConfigMeta configMeta, ISpanParsable? _ = null) where TValue : struct, ISpanParsable { var configPath = Path(configSelection, propertyExpression); if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta meta) { meta.RestoreLockedConfiguration(); configMeta = meta; return true; } configMeta = new NoConfig(configSelection, propertyExpression) { Default = default!, }; return false; } // ReSharper restore MethodOverloadWithOptionalParameter /// /// Attempts to retrieve the configuration metadata for a list-based setting. /// /// /// When no configuration metadata is found, it returns a NoConfig instance with the default /// value set to an empty list. 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) { meta.RestoreLockedConfiguration(); configMeta = meta; return true; } configMeta = new NoConfig>(configSelection, propertyExpression) { Default = [], }; return false; } /// /// Attempts to retrieve the configuration metadata for a set-based setting. /// /// /// When no configuration metadata is found, it returns a NoConfig instance with the default /// value set to an empty set. 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) { meta.RestoreLockedConfiguration(); configMeta = meta; return true; } configMeta = new NoConfig>(configSelection, propertyExpression) { Default = new HashSet(), }; return false; } /// /// Attempts to retrieve the configuration metadata for a string dictionary-based setting. /// /// /// When no configuration metadata is found, it returns a NoConfig instance with the default /// value set to an empty dictionary. 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. /// 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) { meta.RestoreLockedConfiguration(); configMeta = meta; return true; } configMeta = new NoConfig>(configSelection, propertyExpression) { Default = new Dictionary(), }; return false; } /// /// Attempts to retrieve the configuration metadata for an enum dictionary-based setting. /// /// /// When no configuration metadata is found, it returns a NoConfig instance with the default /// value set to an empty dictionary. 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 enum type of the dictionary keys. /// The enum type of the dictionary values. /// True if the configuration metadata was found, otherwise false. public static bool TryGet(Expression> configSelection, Expression>> propertyExpression, out ConfigMeta> configMeta) where TKey : struct, Enum where TValue : struct, Enum { var configPath = Path(configSelection, propertyExpression); if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta> meta) { meta.RestoreLockedConfiguration(); configMeta = meta; return true; } configMeta = new NoConfig>(configSelection, propertyExpression) { Default = new Dictionary(), }; return false; } /// /// Checks whether a configuration plugin may manage a setting, or whether that setting belongs /// to the IT department of an organization. /// /// /// A local configuration plugin must not take over a setting an organization manages. Otherwise, /// anyone could hand out a configuration plugin that quietly replaces parts of the organization /// configuration, e.g. the address of a self-hosted provider.

/// Between two configuration plugins of the same organization, we do not interfere: both belong /// to the IT department, so the one processed later wins, as before. ///
/// The configuration plugin which wants to manage the setting. /// The configuration metadata of the setting. /// True when the plugin may manage this setting, otherwise false. private static bool MayManageSetting(Guid configPluginId, ConfigMetaBase configMeta) { var owningConfigPluginId = GetSettingOwner(configMeta); if (owningConfigPluginId == Guid.Empty || owningConfigPluginId == configPluginId) return true; if (!PluginFactory.IsEnterpriseConfigurationPlugin(owningConfigPluginId)) return true; if (PluginFactory.IsEnterpriseConfigurationPlugin(configPluginId)) return true; Log.LogWarning($"The configuration plugin '{configPluginId}' tried to manage the setting '{configMeta.SettingName}', which is managed by the configuration plugin '{owningConfigPluginId}' of your organization. Ignoring the attempt: configurations deployed by your organization's IT take precedence."); return false; } /// /// Determines the configuration plugin which currently manages a setting, if any. /// private static Guid GetSettingOwner(ConfigMetaBase configMeta) { if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty) return configMeta.LockedByConfigPluginId; // The editable default is persisted as well, so we prefer it over the in-memory state: if (TryGetEditableDefaultState(configMeta.SettingName, out var editableDefaultState) && editableDefaultState.ConfigPluginId != Guid.Empty) return editableDefaultState.ConfigPluginId; return configMeta.EditableDefaultByConfigPluginId; } /// /// Removes all managed states whose configuration plugin is not available anymore. /// /// /// This covers every registered setting, regardless of its type: locked settings, editable /// defaults, and additive plugin contributions. Settings do not need to be listed anywhere for /// this cleanup to work, so adding a new managed setting cannot be forgotten here.

/// A locked setting whose plugin is gone is reset to its default value. That is intended: the /// value belonged to the organization, not to the user, and the user might not be able to /// change it at all. ///
/// The collection of available plugins to check against. /// /// The IDs of the configuration plugins which an organization deployed on this machine, including /// those which could not be loaded. A deployed plugin was not removed, so its settings must stay /// untouched. /// /// True when at least one setting was changed, otherwise false. public static bool CleanupLeftOverManagedConfigurations(IReadOnlyCollection availablePlugins, IReadOnlySet deployedEnterpriseConfigPluginIds) { var wasChanged = false; var registeredSettingNames = new HashSet(StringComparer.Ordinal); foreach (var config in METADATA.Values) { if (config is not ConfigMetaBase configMeta) continue; registeredSettingNames.Add(configMeta.SettingName); // // Restore the persisted ownership first. Otherwise, we would not recognize a left-over // lock when nobody has read this setting since the settings were loaded: // configMeta.RestoreLockedConfiguration(); // Check the locked state: if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.LockedByConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds)) { Log.LogInformation($"Resetting the setting '{configMeta.SettingName}': it was locked by the configuration plugin '{configMeta.LockedByConfigPluginId}', which is not available anymore."); configMeta.ResetLockedConfiguration(); wasChanged = true; } // Check the editable default state: if (CleanupEditableDefaultState(configMeta, availablePlugins, deployedEnterpriseConfigPluginIds)) wasChanged = true; // Check the additive plugin contribution: if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.PluginContributionByConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds)) { Log.LogInformation($"Clearing the plugin contribution for the setting '{configMeta.SettingName}': the configuration plugin '{configMeta.PluginContributionByConfigPluginId}' is not available anymore."); configMeta.ClearPluginContribution(); wasChanged = true; } } // Remove persisted states which belong to settings that do not exist anymore: if (RemoveUnknownManagedStates(registeredSettingNames)) wasChanged = true; return wasChanged; } /// /// Checks whether a configuration plugin is still present on this machine. /// /// /// A plugin counts as present when it was loaded, or when it is deployed but could not be loaded. /// The latter matters for organizations: a broken configuration plugin is still in charge, so we /// must not treat its settings as left over. /// private static bool IsPluginPresent(Guid configPluginId, IReadOnlyCollection availablePlugins, IReadOnlySet deployedEnterpriseConfigPluginIds) => deployedEnterpriseConfigPluginIds.Contains(configPluginId) || availablePlugins.Any(x => x.Id == configPluginId); /// /// Removes persisted managed states which belong to settings that are not registered anymore. /// /// /// Without this, states of removed or renamed settings would stay in the settings file forever. /// private static bool RemoveUnknownManagedStates(IReadOnlySet registeredSettingNames) { var wasChanged = false; var configurationData = SettingsManagerAccess.ConfigurationData; foreach (var settingName in configurationData.ManagedLockedConfigurations.Keys.Where(x => !registeredSettingNames.Contains(x)).ToList()) { Log.LogInformation($"Removing the persisted lock of the setting '{settingName}': this setting does not exist anymore."); configurationData.ManagedLockedConfigurations.Remove(settingName); wasChanged = true; } foreach (var settingName in configurationData.ManagedEditableDefaults.Keys.Where(x => !registeredSettingNames.Contains(x)).ToList()) { Log.LogInformation($"Removing the persisted editable default of the setting '{settingName}': this setting does not exist anymore."); configurationData.ManagedEditableDefaults.Remove(settingName); wasChanged = true; } return wasChanged; } 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; } private static string SettingName(Expression> propertyExpression) => SettingsManager.ToSettingName(propertyExpression); private static bool TryGetEditableDefaultState(string settingName, out ManagedEditableDefaultState editableDefaultState) { return SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.TryGetValue(settingName, out editableDefaultState!); } private static void SetEditableDefaultState(string settingName, Guid pluginId, string lastAppliedValue) { SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults[settingName] = new() { ConfigPluginId = pluginId, LastAppliedValue = lastAppliedValue, }; } private static bool ClearEditableDefaultState(string settingName) => SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.Remove(settingName); private static bool CleanupEditableDefaultState(ConfigMetaBase configMeta, IReadOnlyCollection availablePlugins, IReadOnlySet deployedEnterpriseConfigPluginIds) { if (!TryGetEditableDefaultState(configMeta.SettingName, out var editableDefaultState)) { if (configMeta.ManagedMode is not ManagedConfigurationMode.EDITABLE_DEFAULT) return false; configMeta.ClearEditableDefaultConfiguration(); return true; } if (IsPluginPresent(editableDefaultState.ConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds)) return false; Log.LogInformation($"Clearing the editable default of the setting '{configMeta.SettingName}': the configuration plugin '{editableDefaultState.ConfigPluginId}' is not available anymore."); configMeta.ClearEditableDefaultConfiguration(); return ClearEditableDefaultState(configMeta.SettingName); } }