183 lines
5.8 KiB
C#
183 lines
5.8 KiB
C#
using System.Text.Json;
|
|
using MarketAlly.ProcessMonitor.Interfaces;
|
|
using MarketAlly.ProcessMonitor.Models;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace MarketAlly.ProcessMonitor.Services;
|
|
|
|
/// <summary>
|
|
/// Manages application configuration with hot reload support
|
|
/// </summary>
|
|
public class ConfigurationService : IConfigurationService, IDisposable
|
|
{
|
|
private readonly ILogger<ConfigurationService> _logger;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly IMemoryCache _cache;
|
|
private readonly IOptionsMonitor<AppSettings> _appSettings;
|
|
private readonly FileSystemWatcher _fileWatcher;
|
|
private readonly string _configFilePath;
|
|
private readonly SemaphoreSlim _configLock = new(1, 1);
|
|
|
|
public event EventHandler<ProcessConfiguration>? ConfigurationChanged;
|
|
|
|
public ConfigurationService(
|
|
ILogger<ConfigurationService> logger,
|
|
IConfiguration configuration,
|
|
IMemoryCache cache,
|
|
IOptionsMonitor<AppSettings> appSettings)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
|
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
|
_appSettings = appSettings ?? throw new ArgumentNullException(nameof(appSettings));
|
|
|
|
_configFilePath = Path.Combine(AppContext.BaseDirectory, "processlist.json");
|
|
_fileWatcher = InitializeFileWatcher();
|
|
}
|
|
|
|
public async Task<ProcessConfiguration> GetConfigurationAsync()
|
|
{
|
|
const string cacheKey = "ProcessConfiguration";
|
|
|
|
if (_cache.TryGetValue<ProcessConfiguration>(cacheKey, out var cached) && cached != null)
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
await _configLock.WaitAsync();
|
|
try
|
|
{
|
|
// Double-check after acquiring lock
|
|
if (_cache.TryGetValue<ProcessConfiguration>(cacheKey, out cached) && cached != null)
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
var config = await LoadConfigurationAsync();
|
|
|
|
_cache.Set(cacheKey, config, new MemoryCacheEntryOptions
|
|
{
|
|
SlidingExpiration = TimeSpan.FromMinutes(5),
|
|
Priority = CacheItemPriority.High
|
|
});
|
|
|
|
return config;
|
|
}
|
|
finally
|
|
{
|
|
_configLock.Release();
|
|
}
|
|
}
|
|
|
|
public async Task ReloadConfigurationAsync()
|
|
{
|
|
_logger.LogInformation("Reloading process configuration");
|
|
|
|
await _configLock.WaitAsync();
|
|
try
|
|
{
|
|
_cache.Remove("ProcessConfiguration");
|
|
var config = await LoadConfigurationAsync();
|
|
|
|
_cache.Set("ProcessConfiguration", config, new MemoryCacheEntryOptions
|
|
{
|
|
SlidingExpiration = TimeSpan.FromMinutes(5),
|
|
Priority = CacheItemPriority.High
|
|
});
|
|
|
|
ConfigurationChanged?.Invoke(this, config);
|
|
_logger.LogInformation("Configuration reloaded successfully");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to reload configuration");
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_configLock.Release();
|
|
}
|
|
}
|
|
|
|
public AppSettings GetAppSettings()
|
|
{
|
|
return _appSettings.CurrentValue;
|
|
}
|
|
|
|
private async Task<ProcessConfiguration> LoadConfigurationAsync()
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(_configFilePath))
|
|
{
|
|
_logger.LogError("Configuration file not found at {Path}", _configFilePath);
|
|
throw new FileNotFoundException($"Configuration file not found: {_configFilePath}");
|
|
}
|
|
|
|
var json = await File.ReadAllTextAsync(_configFilePath);
|
|
|
|
var options = new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
|
AllowTrailingCommas = true
|
|
};
|
|
|
|
var config = JsonSerializer.Deserialize<ProcessConfiguration>(json, options);
|
|
|
|
if (config == null || config.Processes == null)
|
|
{
|
|
throw new InvalidOperationException("Invalid configuration format");
|
|
}
|
|
|
|
config.LastModified = File.GetLastWriteTimeUtc(_configFilePath);
|
|
|
|
_logger.LogInformation("Loaded configuration with {Count} processes", config.Processes.Count);
|
|
return config;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error loading configuration from {Path}", _configFilePath);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private FileSystemWatcher InitializeFileWatcher()
|
|
{
|
|
var directory = Path.GetDirectoryName(_configFilePath) ?? AppContext.BaseDirectory;
|
|
var fileName = Path.GetFileName(_configFilePath);
|
|
|
|
var watcher = new FileSystemWatcher(directory, fileName)
|
|
{
|
|
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
|
|
EnableRaisingEvents = true
|
|
};
|
|
|
|
watcher.Changed += async (sender, e) =>
|
|
{
|
|
// Debounce file changes
|
|
await Task.Delay(500);
|
|
|
|
try
|
|
{
|
|
_logger.LogInformation("Configuration file changed, reloading");
|
|
await ReloadConfigurationAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error handling configuration file change");
|
|
}
|
|
};
|
|
|
|
return watcher;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_fileWatcher?.Dispose();
|
|
_configLock?.Dispose();
|
|
}
|
|
} |