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 @@
-
+