Skip to content

Commit

Permalink
implement IConfigurationHandlerV2
Browse files Browse the repository at this point in the history
  • Loading branch information
RaidMax committed Feb 12, 2023
1 parent 2b6720d commit 957c889
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 2 deletions.
6 changes: 4 additions & 2 deletions Application/ApplicationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,14 @@ public async Task Init()
{
if (plugin is ScriptPlugin scriptPlugin)
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
scriptPlugin.Watcher.Changed += async (sender, e) =>
{
try
{
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver);
await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceResolver,
_serviceProvider.GetService<IConfigurationHandlerV2<ScriptPluginConfiguration>>());
}
catch (Exception ex)
Expand Down
206 changes: 206 additions & 0 deletions Application/IO/BaseConfigurationHandlerV2.cs
Original file line number Diff line number Diff line change
@@ -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<TConfigurationType> : IConfigurationHandlerV2<TConfigurationType>
where TConfigurationType : class
{
private readonly ILogger<BaseConfigurationHandlerV2<TConfigurationType>> _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<string> FileUpdated;

public BaseConfigurationHandlerV2(ILogger<BaseConfigurationHandlerV2<TConfigurationType>> logger, ConfigurationWatcher watcher)
{
_logger = logger;
_watcher = watcher;
FileUpdated += OnFileUpdated;
}

~BaseConfigurationHandlerV2()
{
FileUpdated -= OnFileUpdated;
_watcher.Unregister(_path);
}

public async Task<TConfigurationType> 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<TConfigurationType>(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<TConfigurationType>(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));
}
}
}
}
9 changes: 9 additions & 0 deletions Application/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,12 @@ private static async Task<IServiceCollection> ConfigureServices(string[] args)

// setup the static resources (config/master api/translations)
var serviceCollection = new ServiceCollection();
serviceCollection.AddConfiguration<ApplicationConfiguration>("IW4MAdminSettings")
.AddConfiguration<DefaultSettings>()
.AddConfiguration<CommandConfiguration>()
.AddConfiguration<StatsConfiguration>("StatsPluginSettings");

// for legacy purposes. update at some point
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
await appConfigHandler.BuildAsync();
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
Expand Down Expand Up @@ -456,6 +462,9 @@ private static async Task<IServiceCollection> ConfigureServices(string[] args)
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton<IRemoteCommandService, RemoteCommandService>()
.AddSingleton(new ConfigurationWatcher())
.AddSingleton(typeof(IConfigurationHandlerV2<>), typeof(BaseConfigurationHandlerV2<>))
.AddSingleton<IScriptPluginFactory, ScriptPluginFactory>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);

Expand Down
10 changes: 10 additions & 0 deletions SharedLibraryCore/Interfaces/IConfigurationHandlerV2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;

namespace SharedLibraryCore.Interfaces;

public interface IConfigurationHandlerV2<TConfigurationType> where TConfigurationType: class
{
Task<TConfigurationType> Get(string configurationName, TConfigurationType defaultConfiguration = null);
Task Set(TConfigurationType configuration);
Task Set();
}

0 comments on commit 957c889

Please sign in to comment.