using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.AppBase { /// <summary> /// Class BaseConfigurationManager. /// </summary> public abstract class BaseConfigurationManager : IConfigurationManager { private readonly ConcurrentDictionary<string, object> _configurations = new(); private readonly object _configurationSyncLock = new(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); /// <summary> /// The _configuration. /// </summary> private BaseApplicationConfiguration? _configuration; /// <summary> /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class. /// </summary> /// <param name="applicationPaths">The application paths.</param> /// <param name="loggerFactory">The logger factory.</param> /// <param name="xmlSerializer">The XML serializer.</param> protected BaseConfigurationManager( IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer) { CommonApplicationPaths = applicationPaths; XmlSerializer = xmlSerializer; Logger = loggerFactory.CreateLogger<BaseConfigurationManager>(); UpdateCachePath(); } /// <summary> /// Occurs when [configuration updated]. /// </summary> public event EventHandler<EventArgs>? ConfigurationUpdated; /// <summary> /// Occurs when [configuration updating]. /// </summary> public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating; /// <summary> /// Occurs when [named configuration updated]. /// </summary> public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated; /// <summary> /// Gets the type of the configuration. /// </summary> /// <value>The type of the configuration.</value> protected abstract Type ConfigurationType { get; } /// <summary> /// Gets the logger. /// </summary> /// <value>The logger.</value> protected ILogger<BaseConfigurationManager> Logger { get; private set; } /// <summary> /// Gets the XML serializer. /// </summary> /// <value>The XML serializer.</value> protected IXmlSerializer XmlSerializer { get; private set; } /// <summary> /// Gets the application paths. /// </summary> /// <value>The application paths.</value> public IApplicationPaths CommonApplicationPaths { get; private set; } /// <summary> /// Gets or sets the system configuration. /// </summary> /// <value>The configuration.</value> public BaseApplicationConfiguration CommonConfiguration { get { if (_configuration is not null) { return _configuration; } lock (_configurationSyncLock) { if (_configuration is not null) { return _configuration; } return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); } } protected set { _configuration = value; } } /// <summary> /// Manually pre-loads a factory so that it is available pre system initialisation. /// </summary> /// <typeparam name="T">Class to register.</typeparam> public virtual void RegisterConfiguration<T>() where T : IConfigurationFactory { IConfigurationFactory factory = Activator.CreateInstance<T>(); if (_configurationFactories is null) { _configurationFactories = [factory]; } else { _configurationFactories = [.._configurationFactories, factory]; } _configurationStores = _configurationFactories .SelectMany(i => i.GetConfigurations()) .ToArray(); } /// <summary> /// Adds parts. /// </summary> /// <param name="factories">The configuration factories.</param> public virtual void AddParts(IEnumerable<IConfigurationFactory> factories) { _configurationFactories = factories.ToArray(); _configurationStores = _configurationFactories .SelectMany(i => i.GetConfigurations()) .ToArray(); } /// <summary> /// Saves the configuration. /// </summary> public void SaveConfiguration() { Logger.LogInformation("Saving system configuration"); var path = CommonApplicationPaths.SystemConfigurationFilePath; Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); lock (_configurationSyncLock) { XmlSerializer.SerializeToFile(CommonConfiguration, path); } OnConfigurationUpdated(); } /// <summary> /// Called when [configuration updated]. /// </summary> protected virtual void OnConfigurationUpdated() { UpdateCachePath(); EventHelper.QueueEventIfNotNull(ConfigurationUpdated, this, EventArgs.Empty, Logger); } /// <summary> /// Replaces the configuration. /// </summary> /// <param name="newConfiguration">The new configuration.</param> /// <exception cref="ArgumentNullException"><c>newConfiguration</c> is <c>null</c>.</exception> public virtual void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration) { ArgumentNullException.ThrowIfNull(newConfiguration); ValidateCachePath(newConfiguration); CommonConfiguration = newConfiguration; SaveConfiguration(); } /// <summary> /// Updates the items by name path. /// </summary> private void UpdateCachePath() { string cachePath; // If the configuration file has no entry (i.e. not set in UI) if (string.IsNullOrWhiteSpace(CommonConfiguration.CachePath)) { // If the current live configuration has no entry (i.e. not set on CLI/envvars, during startup) if (string.IsNullOrWhiteSpace(((BaseApplicationPaths)CommonApplicationPaths).CachePath)) { // Set cachePath to a default value under ProgramDataPath cachePath = Path.Combine(((BaseApplicationPaths)CommonApplicationPaths).ProgramDataPath, "cache"); } else { // Set cachePath to the existing live value; will require restart if UI value is removed (but not replaced) // TODO: Figure out how to re-grab this from the CLI/envvars while running cachePath = ((BaseApplicationPaths)CommonApplicationPaths).CachePath; } } else { // Set cachePath to the new UI-set value cachePath = CommonConfiguration.CachePath; } Logger.LogInformation("Setting cache path: {Path}", cachePath); ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath; } /// <summary> /// Replaces the cache path. /// </summary> /// <param name="newConfig">The new configuration.</param> /// <exception cref="DirectoryNotFoundException">The new cache path doesn't exist.</exception> private void ValidateCachePath(BaseApplicationConfiguration newConfig) { var newPath = newConfig.CachePath; if (!string.IsNullOrWhiteSpace(newPath) && !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath, StringComparison.Ordinal)) { // Validate if (!Directory.Exists(newPath)) { throw new DirectoryNotFoundException( string.Format( CultureInfo.InvariantCulture, "{0} does not exist.", newPath)); } EnsureWriteAccess(newPath); } } /// <summary> /// Ensures that we have write access to the path. /// </summary> /// <param name="path">The path.</param> protected void EnsureWriteAccess(string path) { var file = Path.Combine(path, Guid.NewGuid().ToString()); File.WriteAllText(file, string.Empty); File.Delete(file); } private string GetConfigurationFile(string key) { return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLowerInvariant() + ".xml"); } /// <inheritdoc /> public object GetConfiguration(string key) { return _configurations.GetOrAdd( key, static (k, configurationManager) => { var file = configurationManager.GetConfigurationFile(k); var configurationInfo = Array.Find( configurationManager._configurationStores, i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase)); if (configurationInfo is null) { throw new ResourceNotFoundException("Configuration with key " + k + " not found."); } var configurationType = configurationInfo.ConfigurationType; lock (configurationManager._configurationSyncLock) { return configurationManager.LoadConfiguration(file, configurationType); } }, this); } private object LoadConfiguration(string path, Type configurationType) { try { if (File.Exists(path)) { return XmlSerializer.DeserializeFromFile(configurationType, path); } } catch (Exception ex) when (ex is not IOException) { Logger.LogError(ex, "Error loading configuration file: {Path}", path); } return Activator.CreateInstance(configurationType) ?? throw new InvalidOperationException("Configuration type can't be Nullable<T>."); } /// <inheritdoc /> public void SaveConfiguration(string key, object configuration) { var configurationStore = GetConfigurationStore(key); var configurationType = configurationStore.ConfigurationType; if (configuration.GetType() != configurationType) { throw new ArgumentException("Expected configuration type is " + configurationType.Name); } if (configurationStore is IValidatingConfiguration validatingStore) { var currentConfiguration = GetConfiguration(key); validatingStore.Validate(currentConfiguration, configuration); } NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration)); _configurations.AddOrUpdate(key, configuration, (_, _) => configuration); var path = GetConfigurationFile(key); Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); lock (_configurationSyncLock) { XmlSerializer.SerializeToFile(configuration, path); } OnNamedConfigurationUpdated(key, configuration); } /// <summary> /// Event handler for when a named configuration has been updated. /// </summary> /// <param name="key">The key of the configuration.</param> /// <param name="configuration">The old configuration.</param> protected virtual void OnNamedConfigurationUpdated(string key, object configuration) { NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration)); } /// <inheritdoc /> public ConfigurationStore[] GetConfigurationStores() { return _configurationStores; } /// <inheritdoc /> public Type GetConfigurationType(string key) { return GetConfigurationStore(key) .ConfigurationType; } private ConfigurationStore GetConfigurationStore(string key) { return _configurationStores .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase)); } } }