Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
+ Added `DiscoveryConfig.WatchEmailMappings`
+ Added `DiscoveryConfig.WatchFiles`
+ Added `DiscoveryConfig.PreReloadCommand´
  • Loading branch information
nd1012 committed Apr 8, 2024
1 parent a0b97be commit f0ac2eb
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 132 deletions.
68 changes: 46 additions & 22 deletions src/wan24-AutoDiscover Shared/Models/DiscoveryConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ public DiscoveryConfig() { }
/// </summary>
public string? EmailMappings { get; init; }

/// <summary>
/// Watch email mappings list file changes for reloading the configuration?
/// </summary>
public bool WatchEmailMappings { get; init; } = true;

/// <summary>
/// Additional file paths to watch for an automatic configuration reload
/// </summary>
public string[]? WatchFiles { get; init; }

/// <summary>
/// Command to execute (and optional arguments) before reloading the configuration when any <see cref="WatchFiles"/> file changed
/// </summary>
public string[]? PreReloadCommand { get; init; }

/// <summary>
/// Get the discovery configuration
/// </summary>
Expand All @@ -78,12 +93,12 @@ public virtual async Task<IReadOnlyDictionary<string, DomainConfig>> 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);
Expand All @@ -95,36 +110,45 @@ public virtual async Task<IReadOnlyDictionary<string, DomainConfig>> 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<EmailMapping[]>(fs).DynamicContext()
mappings = await JsonHelper.DecodeAsync<EmailMapping[]>(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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="ObjectValidation" Version="2.5.0" />
<PackageReference Include="wan24-Core" Version="2.16.0" />
<PackageReference Include="wan24-Core" Version="2.17.0" />
</ItemGroup>

</Project>
134 changes: 57 additions & 77 deletions src/wan24-AutoDiscover/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,9 +19,11 @@
}

// Load the configuration
using SemaphoreSync configSync = new();
string configFile = Path.Combine(ENV.AppFolder, "appsettings.json");
async Task<IConfigurationRoot> LoadConfigAsync()
{
using SemaphoreSyncContext ssc = await configSync.SyncContextAsync().DynamicContext();
ConfigurationBuilder configBuilder = new();
configBuilder.AddJsonFile(configFile, optional: false);
IConfigurationRoot config = configBuilder.Build();
Expand All @@ -44,18 +47,19 @@ async Task<IConfigurationRoot> 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");
}
Expand All @@ -65,82 +69,57 @@ async Task<IConfigurationRoot> 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");
Expand All @@ -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<XmlDocumentInstances>())
.AddHostedService(services => fsw)
.AddExceptionHandler<ExceptionHandler>()
.AddHttpLogging(options => options.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders);
builder.Services.Configure<ForwardedHeadersOptions>(options =>
Expand Down
31 changes: 0 additions & 31 deletions src/wan24-AutoDiscover/Services/ConfigChangeEventThrottle.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/wan24-AutoDiscover/wan24-AutoDiscover.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<ItemGroup>
<PackageReference Include="ObjectValidation" Version="2.5.0" />
<PackageReference Include="wan24-CLI" Version="1.3.0" />
<PackageReference Include="wan24-Core" Version="2.16.0" />
<PackageReference Include="wan24-Core" Version="2.17.0" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit f0ac2eb

Please sign in to comment.