diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index e4823a09a..b0c435f7c 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -317,12 +317,14 @@ namespace IW4MAdmin.Application { if (plugin is ScriptPlugin scriptPlugin) { - await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver); + await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver, + _serviceProvider.GetService>()); scriptPlugin.Watcher.Changed += async (sender, e) => { try { - await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver); + await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver, + _serviceProvider.GetService>()); } catch (Exception ex) diff --git a/Application/IO/BaseConfigurationHandlerV2.cs b/Application/IO/BaseConfigurationHandlerV2.cs new file mode 100644 index 000000000..6a028e98f --- /dev/null +++ b/Application/IO/BaseConfigurationHandlerV2.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharedLibraryCore; +using SharedLibraryCore.Interfaces; + +namespace IW4MAdmin.Application.IO; + +public class BaseConfigurationHandlerV2 : IConfigurationHandlerV2 + where TConfigurationType : class +{ + private readonly ILogger> _logger; + private readonly ConfigurationWatcher _watcher; + private readonly JsonSerializerOptions _serializerOptions = new() + { + WriteIndented = true, + Converters = + { + new JsonStringEnumConverter() + }, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + private readonly SemaphoreSlim _onIo = new(1, 1); + private TConfigurationType _configurationInstance; + private string _path = string.Empty; + private event Action FileUpdated; + + public BaseConfigurationHandlerV2(ILogger> logger, ConfigurationWatcher watcher) + { + _logger = logger; + _watcher = watcher; + FileUpdated += OnFileUpdated; + } + + ~BaseConfigurationHandlerV2() + { + FileUpdated -= OnFileUpdated; + _watcher.Unregister(_path); + } + + public async Task Get(string configurationName, + TConfigurationType defaultConfiguration = default) + { + if (string.IsNullOrWhiteSpace(configurationName)) + { + return defaultConfiguration; + } + + var cleanName = configurationName.Replace("\\", "").Replace("/", ""); + + if (string.IsNullOrWhiteSpace(configurationName)) + { + return defaultConfiguration; + } + + _path = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{cleanName}.json"); + TConfigurationType readConfiguration = null; + + try + { + await _onIo.WaitAsync(); + await using var fileStream = File.OpenRead(_path); + readConfiguration = + await JsonSerializer.DeserializeAsync(fileStream, _serializerOptions); + fileStream.Close(); + _watcher.Register(_path, FileUpdated); + + if (readConfiguration is null) + { + _logger.LogError("Could not parse configuration {Type} at {FileName}", typeof(TConfigurationType).Name, + _path); + + return defaultConfiguration; + } + } + catch (FileNotFoundException) + { + if (defaultConfiguration is not null) + { + await InternalSet(defaultConfiguration, false); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not read configuration file at {Path}", _path); + return defaultConfiguration; + } + finally + { + if (_onIo.CurrentCount == 0) + { + _onIo.Release(1); + } + } + + return _configurationInstance ??= readConfiguration; + } + + public async Task Set(TConfigurationType configuration) + { + await InternalSet(configuration, true); + } + + public async Task Set() + { + if (_configurationInstance is not null) + { + await InternalSet(_configurationInstance, true); + } + } + + private async Task InternalSet(TConfigurationType configuration, bool awaitSemaphore) + { + try + { + if (awaitSemaphore) + { + await _onIo.WaitAsync(); + } + await using var fileStream = File.OpenWrite(_path); + await JsonSerializer.SerializeAsync(fileStream, configuration, _serializerOptions); + fileStream.Close(); + _configurationInstance = configuration; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not save configuration {Type} {Path}", configuration.GetType().Name, _path); + } + finally + { + if (awaitSemaphore && _onIo.CurrentCount == 0) + { + _onIo.Release(1); + } + } + } + + private async void OnFileUpdated(string filePath) + { + try + { + await _onIo.WaitAsync(); + await using var fileStream = File.OpenRead(_path); + var readConfiguration = + await JsonSerializer.DeserializeAsync(fileStream, _serializerOptions); + fileStream.Close(); + + if (readConfiguration is null) + { + _logger.LogWarning("Could not parse updated configuration {Type} at {Path}", + typeof(TConfigurationType).Name, filePath); + } + else + { + CopyUpdatedProperties(readConfiguration); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not parse updated configuration {Type} at {Path}", + typeof(TConfigurationType).Name, filePath); + } + finally + { + if (_onIo.CurrentCount == 0) + { + _onIo.Release(1); + } + } + } + + private void CopyUpdatedProperties(TConfigurationType newConfiguration) + { + if (_configurationInstance is null) + { + _configurationInstance = newConfiguration; + return; + } + + _logger.LogDebug("Updating existing config with new values {Type} at {Path}", typeof(TConfigurationType).Name, _path); + + if (_configurationInstance is IDictionary configDict && newConfiguration is IDictionary newConfigDict) + { + configDict.Clear(); + foreach (var key in newConfigDict.Keys) + { + configDict.Add(key, newConfigDict[key]); + } + } + else + { + foreach (var property in _configurationInstance.GetType().GetProperties() + .Where(prop => prop.CanRead && prop.CanWrite)) + { + property.SetValue(_configurationInstance, property.GetValue(newConfiguration)); + } + } + } +} diff --git a/Application/Main.cs b/Application/Main.cs index a807681c6..daa910395 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -352,6 +352,12 @@ namespace IW4MAdmin.Application // setup the static resources (config/master api/translations) var serviceCollection = new ServiceCollection(); + serviceCollection.AddConfiguration("IW4MAdminSettings") + .AddConfiguration() + .AddConfiguration() + .AddConfiguration("StatsPluginSettings"); + + // for legacy purposes. update at some point var appConfigHandler = new BaseConfigurationHandler("IW4MAdminSettings"); await appConfigHandler.BuildAsync(); var defaultConfigHandler = new BaseConfigurationHandler("DefaultSettings"); @@ -456,6 +462,9 @@ namespace IW4MAdmin.Application .AddTransient() .AddSingleton() .AddSingleton() + .AddSingleton(new ConfigurationWatcher()) + .AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>)) + .AddSingleton() .AddSingleton(translationLookup) .AddDatabaseContextOptions(appConfig); diff --git a/SharedLibraryCore/Interfaces/IConfigurationHandlerV2.cs b/SharedLibraryCore/Interfaces/IConfigurationHandlerV2.cs new file mode 100644 index 000000000..521d38b61 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IConfigurationHandlerV2.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace SharedLibraryCore.Interfaces; + +public interface IConfigurationHandlerV2 where TConfigurationType: class +{ + Task Get(string configurationName, TConfigurationType defaultConfiguration = null); + Task Set(TConfigurationType configuration); + Task Set(); +}