From f0ac2eb181b886adc9922059157b459f1ed16166 Mon Sep 17 00:00:00 2001 From: nd Date: Mon, 8 Apr 2024 19:28:17 +0200 Subject: [PATCH] Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + Added `DiscoveryConfig.WatchEmailMappings` + Added `DiscoveryConfig.WatchFiles` + Added `DiscoveryConfig.PreReloadCommand´ --- .../Models/DiscoveryConfig.cs | 68 ++++++--- .../wan24-AutoDiscover Shared.csproj | 2 +- src/wan24-AutoDiscover/Program.cs | 134 ++++++++---------- .../Services/ConfigChangeEventThrottle.cs | 31 ---- .../wan24-AutoDiscover.csproj | 2 +- 5 files changed, 105 insertions(+), 132 deletions(-) delete mode 100644 src/wan24-AutoDiscover/Services/ConfigChangeEventThrottle.cs diff --git a/src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs b/src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs index 89fbf36..b9a8635 100644 --- a/src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs +++ b/src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs @@ -61,6 +61,21 @@ public DiscoveryConfig() { } /// public string? EmailMappings { get; init; } + /// + /// Watch email mappings list file changes for reloading the configuration? + /// + public bool WatchEmailMappings { get; init; } = true; + + /// + /// Additional file paths to watch for an automatic configuration reload + /// + public string[]? WatchFiles { get; init; } + + /// + /// Command to execute (and optional arguments) before reloading the configuration when any file changed + /// + public string[]? PreReloadCommand { get; init; } + /// /// Get the discovery configuration /// @@ -78,12 +93,12 @@ public virtual async Task> GetDiscover if (gt.Length != 2) throw new InvalidDataException($"Discovery type must be a generic type with two type arguments"); if (gt[0] != typeof(string)) - throw new InvalidDataException($"Discovery types first generic type argument must be a {typeof(string)}"); + throw new InvalidDataException($"Discovery types first generic type argument must be {typeof(string)}"); if (!typeof(DomainConfig).IsAssignableFrom(gt[1])) throw new InvalidDataException($"Discovery types second generic type argument must be a {typeof(DomainConfig)}"); // Parse the discovery configuration IDictionary discovery = config.GetRequiredSection("DiscoveryConfig:Discovery").Get(discoveryType) as IDictionary - ?? throw new InvalidDataException("Failed to get discovery configuration"); + ?? throw new InvalidDataException("Failed to get discovery configuration from the DiscoveryConfig:Discovery section"); object[] keys = new object[discovery.Count], values = new object[discovery.Count]; discovery.Keys.CopyTo(keys, index: 0); @@ -95,36 +110,45 @@ public virtual async Task> GetDiscover if (!string.IsNullOrWhiteSpace(EmailMappings)) if (File.Exists(EmailMappings)) { - Logging.WriteInfo($"Loading email mappings from \"{EmailMappings}\""); + Logging.WriteInfo($"Loading email mappings from {EmailMappings.ToQuotedLiteral()}"); FileStream fs = FsHelper.CreateFileStream(EmailMappings, FileMode.Open, FileAccess.Read, FileShare.Read); + EmailMapping[] mappings; await using (fs.DynamicContext()) - { - EmailMapping[] mappings = await JsonHelper.DecodeAsync(fs).DynamicContext() + mappings = await JsonHelper.DecodeAsync(fs).DynamicContext() ?? throw new InvalidDataException("Invalid email mappings"); - foreach(EmailMapping mapping in mappings) + foreach(EmailMapping mapping in mappings) + { + if (!mapping.Email.Contains('@')) + { + if (Logging.Debug) + Logging.WriteDebug($"Skipping invalid email address {mapping.Email.ToQuotedLiteral()}"); + continue; + } + string email = mapping.Email.ToLower(); + string[] emailParts = email.Split('@', 2); + if ( + emailParts.Length != 2 || + !MailAddress.TryCreate(email, out MailAddress? emailAddress) || + (emailAddress.User.Length == 1 && (emailAddress.User[0] == '*' || emailAddress.User[0] == '@')) || + EmailMapping.GetLoginUser(mappings, email) is not string loginUser || + DomainConfig.GetConfig(string.Empty, emailParts) is not DomainConfig domain + ) { - if (!mapping.Email.Contains('@')) - continue; - string email = mapping.Email.ToLower(); - if ( - !MailAddress.TryCreate(mapping.Email, out MailAddress? emailAddress) || - (emailAddress.User.Length == 1 && (emailAddress.User[0] == '*' || emailAddress.User[0] == '@')) || - EmailMapping.GetLoginUser(mappings, email) is not string loginUser - ) - continue; - string[] emailParts = mapping.Email.ToLower().Split('@', 2); - if (emailParts.Length != 2 || DomainConfig.GetConfig(string.Empty, emailParts) is not DomainConfig domain) - continue; if (Logging.Debug) - Logging.WriteDebug($"Mapping email address \"{email}\" to login user \"{loginUser}\""); - domain.LoginNameMapping ??= []; - domain.LoginNameMapping[email] = loginUser; + Logging.WriteDebug($"Mapping email address {email.ToQuotedLiteral()} to login user failed, because it seems to be a redirection to an external target, or no matching domain configuration was found"); + continue; } + if (Logging.Debug) + Logging.WriteDebug($"Mapping email address {email.ToQuotedLiteral()} to login user {loginUser.ToQuotedLiteral()}"); + domain.LoginNameMapping ??= []; + if (Logging.Debug && domain.LoginNameMapping.ContainsKey(email)) + Logging.WriteDebug($"Overwriting existing email address {email.ToQuotedLiteral()} mapping"); + domain.LoginNameMapping[email] = loginUser; } } else { - Logging.WriteWarning($"Email mappings file \"{EmailMappings}\" not found"); + Logging.WriteWarning($"Email mappings file {EmailMappings.ToQuotedLiteral()} not found"); } return discoveryDomains.ToFrozenDictionary(); } diff --git a/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj b/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj index 85a7e98..70948f1 100644 --- a/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj +++ b/src/wan24-AutoDiscover Shared/wan24-AutoDiscover Shared.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/wan24-AutoDiscover/Program.cs b/src/wan24-AutoDiscover/Program.cs index de34396..2517444 100644 --- a/src/wan24-AutoDiscover/Program.cs +++ b/src/wan24-AutoDiscover/Program.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.HttpLogging; using Microsoft.AspNetCore.HttpOverrides; +using System.Diagnostics; using wan24.AutoDiscover.Models; using wan24.AutoDiscover.Services; using wan24.CLI; @@ -18,9 +19,11 @@ } // Load the configuration +using SemaphoreSync configSync = new(); string configFile = Path.Combine(ENV.AppFolder, "appsettings.json"); async Task LoadConfigAsync() { + using SemaphoreSyncContext ssc = await configSync.SyncContextAsync().DynamicContext(); ConfigurationBuilder configBuilder = new(); configBuilder.AddJsonFile(configFile, optional: false); IConfigurationRoot config = configBuilder.Build(); @@ -44,18 +47,19 @@ async Task LoadConfigAsync() Logging.WriteInfo($"Using configuration \"{configFile}\""); // Watch configuration changes -using ConfigChangeEventThrottle fswThrottle = new(); -ConfigChangeEventThrottle.OnConfigChange += async () => +using MultiFileSystemEvents fsw = new();//TODO Throttle only the main service events +fsw.OnEvents += async (s, e) => { try { - Logging.WriteDebug("Handling configuration change"); + if (Logging.Debug) + Logging.WriteDebug("Handling configuration change"); if (File.Exists(configFile)) { Logging.WriteInfo($"Auto-reloading changed configuration from \"{configFile}\""); await LoadConfigAsync().DynamicContext(); } - else + else if(Logging.Trace) { Logging.WriteTrace($"Configuration file \"{configFile}\" doesn't exist"); } @@ -65,82 +69,57 @@ async Task LoadConfigAsync() Logging.WriteWarning($"Failed to reload configuration from \"{configFile}\": {ex}"); } }; -void ReloadConfig(object sender, FileSystemEventArgs e) -{ - try - { - Logging.WriteDebug($"Detected configuration change {e.ChangeType}"); - if (File.Exists(configFile)) - { - if (fswThrottle.IsThrottling) - { - Logging.WriteTrace("Skipping configuration change event due too many events"); - } - else if (fswThrottle.Raise()) - { - Logging.WriteTrace("Configuration change event has been raised"); - } - } - else - { - Logging.WriteTrace($"Configuration file \"{configFile}\" doesn't exist"); - } - } - catch (Exception ex) - { - Logging.WriteWarning($"Failed to handle configuration change of \"{configFile}\": {ex}"); - } -} -using FileSystemWatcher fsw = new(ENV.AppFolder, "appsettings.json") -{ - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, - IncludeSubdirectories = false, - EnableRaisingEvents = true -}; -fsw.Changed += ReloadConfig; -fsw.Created += ReloadConfig; -string? emailMappings = DiscoveryConfig.Current.EmailMappings is null - ? null - : Path.GetFullPath(DiscoveryConfig.Current.EmailMappings); -void ReloadEmailMappings(object sender, FileSystemEventArgs e) -{ - try +fsw.Add(new(ENV.AppFolder, "appsettings.json", NotifyFilters.LastWrite | NotifyFilters.CreationTime, throttle: 250, recursive: false)); +if (DiscoveryConfig.Current.WatchEmailMappings && DiscoveryConfig.Current.EmailMappings is not null) + fsw.Add(new( + Path.GetDirectoryName(Path.GetFullPath(DiscoveryConfig.Current.EmailMappings))!, + Path.GetFileName(DiscoveryConfig.Current.EmailMappings), + NotifyFilters.LastWrite | NotifyFilters.CreationTime, + throttle: 250, + recursive: false, + FileSystemEventTypes.Changes | FileSystemEventTypes.Created | FileSystemEventTypes.Deleted + )); +if (DiscoveryConfig.Current.WatchFiles is not null) + foreach (string file in DiscoveryConfig.Current.WatchFiles) { - Logging.WriteDebug($"Detected email mappings change {e.ChangeType}"); - if (File.Exists(emailMappings)) - { - if (fswThrottle.IsThrottling) - { - Logging.WriteTrace("Skipping email mappings change event due too many events"); - } - else if (fswThrottle.Raise()) + FileSystemEvents fse = new( + Path.GetDirectoryName(Path.GetFullPath(file))!, + Path.GetFileName(file), + NotifyFilters.LastWrite | NotifyFilters.CreationTime, + throttle: 250, + recursive: false, + FileSystemEventTypes.Changes | FileSystemEventTypes.Created | FileSystemEventTypes.Deleted + ); + if (DiscoveryConfig.Current.PreReloadCommand is not null && DiscoveryConfig.Current.PreReloadCommand.Length > 0) + fse.OnEvents += async (s, e) => { - Logging.WriteTrace("Email mappings change event has been raised"); - } - } - else - { - Logging.WriteTrace($"Email mappings file \"{emailMappings}\" doesn't exist"); - } - } - catch (Exception ex) - { - Logging.WriteWarning($"Failed to handle email mappings change of \"{emailMappings}\": {ex}"); + try + { + Logging.WriteInfo($"Executing pre-reload command on detected {file.ToQuotedLiteral()} change {string.Join('|', e.Arguments.Select(a => a.ChangeType.ToString()).Distinct())}"); + using Process proc = new(); + proc.StartInfo.FileName = DiscoveryConfig.Current.PreReloadCommand[0]; + if (DiscoveryConfig.Current.PreReloadCommand.Length > 1) + proc.StartInfo.ArgumentList.AddRange(DiscoveryConfig.Current.PreReloadCommand[1..]); + proc.StartInfo.UseShellExecute = true; + proc.StartInfo.CreateNoWindow = true; + proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + proc.Start(); + await proc.WaitForExitAsync().DynamicContext(); + if (proc.ExitCode != 0) + Logging.WriteWarning($"Pre-reload command exit code was #{proc.ExitCode}"); + } + catch(Exception ex) + { + Logging.WriteError($"Pre-reload command execution failed exceptional: {ex}"); + } + finally + { + if (Logging.Trace) + Logging.WriteTrace("Pre-reload command execution done"); + } + }; + fsw.Add(fse); } -} -using FileSystemWatcher? emailMappingsFsw = emailMappings is null - ? null - : new(Path.GetDirectoryName(emailMappings)!, Path.GetFileName(emailMappings)) - { - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, - IncludeSubdirectories = false, - EnableRaisingEvents = true - }; -if (emailMappingsFsw is not null) -{ - emailMappingsFsw.Changed += ReloadEmailMappings; - emailMappingsFsw.Created += ReloadEmailMappings; -} // Build and run the app Logging.WriteInfo("Autodiscovery service app startup"); @@ -153,6 +132,7 @@ void ReloadEmailMappings(object sender, FileSystemEventArgs e) builder.Services.AddControllers(); builder.Services.AddSingleton(typeof(XmlDocumentInstances), services => new XmlDocumentInstances(capacity: DiscoveryConfig.Current.PreForkResponses)) .AddHostedService(services => services.GetRequiredService()) + .AddHostedService(services => fsw) .AddExceptionHandler() .AddHttpLogging(options => options.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders); builder.Services.Configure(options => diff --git a/src/wan24-AutoDiscover/Services/ConfigChangeEventThrottle.cs b/src/wan24-AutoDiscover/Services/ConfigChangeEventThrottle.cs deleted file mode 100644 index bf523d3..0000000 --- a/src/wan24-AutoDiscover/Services/ConfigChangeEventThrottle.cs +++ /dev/null @@ -1,31 +0,0 @@ -using wan24.Core; - -namespace wan24.AutoDiscover.Services -{ - /// - /// Configuration changed event throttle - /// - public sealed class ConfigChangeEventThrottle : EventThrottle - { - /// - /// Constructor - /// - public ConfigChangeEventThrottle() : base(timeout: 300) { } - - /// - protected override void HandleEvent(in DateTime raised, in int raisedCount) => RaiseOnConfigChange(); - - /// - /// Delegate for the event - /// - public delegate void ConfigChange_Delegate(); - /// - /// Raised on configuration changes - /// - public static event ConfigChange_Delegate? OnConfigChange; - /// - /// Raise the event - /// - private static void RaiseOnConfigChange() => OnConfigChange?.Invoke(); - } -} diff --git a/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj b/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj index 00c146a..04b612f 100644 --- a/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj +++ b/src/wan24-AutoDiscover/wan24-AutoDiscover.csproj @@ -12,7 +12,7 @@ - +