From b290284a9813f570e244613bc5ed0a4a349083d4 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Tue, 13 Jan 2026 16:28:41 +0100 Subject: [PATCH 1/4] Wrap `PopulateAppSettings` with error handling and ensure consistent initialization across all services --- src/ServiceControl.Audit/Program.cs | 3 +- .../ExeConfiguration.cs | 47 ++++++++++++------- src/ServiceControl.Monitoring/Program.cs | 4 +- src/ServiceControl/Program.cs | 4 +- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/ServiceControl.Audit/Program.cs b/src/ServiceControl.Audit/Program.cs index dbe5268e93..763329c2f6 100644 --- a/src/ServiceControl.Audit/Program.cs +++ b/src/ServiceControl.Audit/Program.cs @@ -11,6 +11,8 @@ try { + ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); + var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); @@ -25,7 +27,6 @@ return exitCode; } - ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); var arguments = new HostArguments(args); diff --git a/src/ServiceControl.Configuration/ExeConfiguration.cs b/src/ServiceControl.Configuration/ExeConfiguration.cs index 2f0b0f3c5b..671d490ab3 100644 --- a/src/ServiceControl.Configuration/ExeConfiguration.cs +++ b/src/ServiceControl.Configuration/ExeConfiguration.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Configuration { + using System; using System.Configuration; using System.IO; using System.Linq; @@ -12,27 +13,37 @@ public static class ExeConfiguration // This code reads in the exe.config files and adds all the values into the ConfigurationManager's collections. public static void PopulateAppSettings(Assembly assembly) { - var location = Path.GetDirectoryName(assembly.Location); - var assemblyName = Path.GetFileNameWithoutExtension(assembly.Location); - var exeConfigPath = Path.Combine(location, $"{assemblyName}.exe.config"); - var fileMap = new ExeConfigurationFileMap { ExeConfigFilename = exeConfigPath }; - var configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None); - - foreach (var key in configuration.AppSettings.Settings.AllKeys) + try { - ConfigurationManager.AppSettings.Set(key, configuration.AppSettings.Settings[key].Value); + var location = Path.GetDirectoryName(assembly.Location); + var assemblyName = Path.GetFileNameWithoutExtension(assembly.Location); + var exeConfigPath = Path.Combine(location, $"{assemblyName}.exe.config"); + var fileMap = new ExeConfigurationFileMap + { + ExeConfigFilename = exeConfigPath + }; + var configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None); + + foreach (var key in configuration.AppSettings.Settings.AllKeys) + { + ConfigurationManager.AppSettings.Set(key, configuration.AppSettings.Settings[key].Value); + } + + // The connection strings collection has had its read only flag set, so we need to clear it before we can add items to it + UnsetCollectionReadonly(ConfigurationManager.ConnectionStrings); + + foreach (var connectionStringSetting in configuration.ConnectionStrings.ConnectionStrings.Cast()) + { + ConfigurationManager.ConnectionStrings.Add(connectionStringSetting); + } + + // Put the collection back into its previous state after we're done adding items to it + SetCollectionReadOnly(ConfigurationManager.ConnectionStrings); } - - // The connection strings collection has had its read only flag set, so we need to clear it before we can add items to it - UnsetCollectionReadonly(ConfigurationManager.ConnectionStrings); - - foreach (var connectionStringSetting in configuration.ConnectionStrings.ConnectionStrings.Cast()) + catch (Exception e) { - ConfigurationManager.ConnectionStrings.Add(connectionStringSetting); + throw new Exception("Failed to populate app settings.", e); } - - // Put the collection back into its previous state after we're done adding items to it - SetCollectionReadOnly(ConfigurationManager.ConnectionStrings); } static void UnsetCollectionReadonly(ConfigurationElementCollection collection) @@ -47,4 +58,4 @@ static void UnsetCollectionReadonly(ConfigurationElementCollection collection) [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "SetReadOnly")] static extern void SetCollectionReadOnly(ConfigurationElementCollection collection); } -} +} \ No newline at end of file diff --git a/src/ServiceControl.Monitoring/Program.cs b/src/ServiceControl.Monitoring/Program.cs index e8dce81512..81393c0c61 100644 --- a/src/ServiceControl.Monitoring/Program.cs +++ b/src/ServiceControl.Monitoring/Program.cs @@ -9,6 +9,8 @@ try { + ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); + var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); @@ -23,8 +25,6 @@ return exitCode; } - ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - var arguments = new HostArguments(args); var settings = new Settings(loggingSettings: loggingSettings); diff --git a/src/ServiceControl/Program.cs b/src/ServiceControl/Program.cs index ec4a0bc70d..857ed0a782 100644 --- a/src/ServiceControl/Program.cs +++ b/src/ServiceControl/Program.cs @@ -11,6 +11,8 @@ try { + ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); + var loggingSettings = new LoggingSettings(Settings.SettingsRootNamespace); LoggingConfigurator.ConfigureLogging(loggingSettings); logger = LoggerUtil.CreateStaticLogger(typeof(Program)); @@ -25,8 +27,6 @@ return exitCode; } - ExeConfiguration.PopulateAppSettings(Assembly.GetExecutingAssembly()); - var arguments = new HostArguments(args); if (arguments.Help) From cc44656d7268932960e5cc48004642829f8b0c7d Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Tue, 13 Jan 2026 17:32:33 +0100 Subject: [PATCH 2/4] Add unit tests to verify appSettings population for ServiceControl, ServiceControl.Audit, and ServiceControl.Monitoring --- .../PopulateAppSettingsTests.cs | 76 +++++++++++++++++++ .../PopulateAppSettingsTests.cs | 76 +++++++++++++++++++ .../PopulateAppSettingsTests.cs | 74 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs create mode 100644 src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs create mode 100644 src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs diff --git a/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs new file mode 100644 index 0000000000..70e07218a2 --- /dev/null +++ b/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs @@ -0,0 +1,76 @@ +namespace ServiceControl.UnitTests; + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; + +[TestFixture] +public class PopulateAppSettingsTests +{ + [Test] + public async Task Should_populate_appSettings_from_exe_config_file() + { + const string MagicValue = "7303A0AA-1003-4DC4-823B-4E8B2A35CF57"; + + var config = $""" + + + + + + + """; + + await File.WriteAllTextAsync("ServiceControl.Audit.exe.config", config); + +#if WINDOWS + const string fileName = "ServiceControl.Audit.exe"; +#else + const string fileName = "ServiceControl.Audit"; +#endif + var startInfo = new ProcessStartInfo(fileName) + { + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var p = Process.Start(startInfo); + + if (p == null) + { + throw new Exception($"Failed to start {fileName}"); + } + + var pathIsSet = false; + + var outputTask = Task.Run(async () => + { + while (!p.StandardOutput.EndOfStream) + { + var line = await p.StandardOutput.ReadLineAsync(); + + Console.WriteLine(line); + + if (line.Contains($"Logging to {MagicValue}")) + { + pathIsSet = true; + p.Kill(true); + } + } + }); + + if(!p.WaitForExit(5000)) + { + p.Kill(true); + } + + await outputTask; + + Assert.That(pathIsSet, Is.True); + } + + [TearDown] + public void TearDown() => File.Delete("ServiceControl.exe.config"); +} \ No newline at end of file diff --git a/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs new file mode 100644 index 0000000000..eabf3e0204 --- /dev/null +++ b/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs @@ -0,0 +1,76 @@ +namespace ServiceControl.UnitTests; + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; + +[TestFixture] +public class PopulateAppSettingsTests +{ + [Test] + public async Task Should_populate_appSettings_from_exe_config_file() + { + const string MagicValue = "7303A0AA-1003-4DC4-823B-4E8B2A35CF57"; + + var config = $""" + + + + + + + """; + + await File.WriteAllTextAsync("ServiceControl.Monitoring.exe.config", config); + +#if WINDOWS + const string fileName = "ServiceControl.Monitoring.exe"; +#else + const string fileName = "ServiceControl.Monitoring"; +#endif + var startInfo = new ProcessStartInfo(fileName) + { + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var p = Process.Start(startInfo); + + if (p == null) + { + throw new Exception($"Failed to start {fileName}"); + } + + var pathIsSet = false; + + var outputTask = Task.Run(async () => + { + while (!p.StandardOutput.EndOfStream) + { + var line = await p.StandardOutput.ReadLineAsync(); + + Console.WriteLine(line); + + if (line.Contains($"Logging to {MagicValue}")) + { + pathIsSet = true; + p.Kill(true); + } + } + }); + + if(!p.WaitForExit(5000)) + { + p.Kill(true); + } + + await outputTask; + + Assert.That(pathIsSet, Is.True); + } + + [TearDown] + public void TearDown() => File.Delete("ServiceControl.exe.config"); +} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs new file mode 100644 index 0000000000..9ad0aabf71 --- /dev/null +++ b/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs @@ -0,0 +1,74 @@ +namespace ServiceControl.UnitTests; + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; + +[TestFixture] +public class PopulateAppSettingsTests +{ + [Test] + public async Task Should_populate_appSettings_from_exe_config_file() + { + const string MagicValue = "7303A0AA-1003-4DC4-823B-4E8B2A35CF57"; + + var config = $""" + + + + + + + """; + + await File.WriteAllTextAsync("ServiceControl.exe.config", config); + +#if WINDOWS + const string fileName = "ServiceControl.exe"; +#else + const string fileName = "ServiceControl"; +#endif + var startInfo = new ProcessStartInfo(fileName) + { + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var p = Process.Start(startInfo); + + if (p == null) + { + throw new Exception("Failed to start ServiceControl"); + } + + var pathIsSet = false; + + var outputTask = Task.Run(async () => + { + while (!p.StandardOutput.EndOfStream) + { + var line = await p.StandardOutput.ReadLineAsync(); + + if (line.Contains($"Logging to {MagicValue}")) + { + pathIsSet = true; + p.Kill(true); + } + } + }); + + if(!p.WaitForExit(5000)) + { + p.Kill(true); + } + + await outputTask; + + Assert.That(pathIsSet, Is.True); + } + + [TearDown] + public void TearDown() => File.Delete("ServiceControl.exe.config"); +} \ No newline at end of file From c9fd6dce336e83700791b5fb5ad8033681d84c6e Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Tue, 13 Jan 2026 17:53:15 +0100 Subject: [PATCH 3/4] Fix formatting errors --- src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs | 2 +- .../PopulateAppSettingsTests.cs | 2 +- src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs index 70e07218a2..39e7424d0b 100644 --- a/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs +++ b/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs @@ -61,7 +61,7 @@ public async Task Should_populate_appSettings_from_exe_config_file() } }); - if(!p.WaitForExit(5000)) + if (!p.WaitForExit(5000)) { p.Kill(true); } diff --git a/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs index eabf3e0204..6839f75681 100644 --- a/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs +++ b/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs @@ -61,7 +61,7 @@ public async Task Should_populate_appSettings_from_exe_config_file() } }); - if(!p.WaitForExit(5000)) + if (!p.WaitForExit(5000)) { p.Kill(true); } diff --git a/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs index 9ad0aabf71..fd7f1eb0f0 100644 --- a/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs +++ b/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs @@ -59,7 +59,7 @@ public async Task Should_populate_appSettings_from_exe_config_file() } }); - if(!p.WaitForExit(5000)) + if (!p.WaitForExit(5000)) { p.Kill(true); } From 6bd729051282918c4830fd812d2e819dad863a3b Mon Sep 17 00:00:00 2001 From: Mauro Servienti Date: Wed, 14 Jan 2026 08:43:32 +0100 Subject: [PATCH 4/4] Use RuntimeInformation to determine the executable file name --- .../PopulateAppSettingsTests.cs | 16 +++++++--------- .../PopulateAppSettingsTests.cs | 12 +++++++----- .../PopulateAppSettingsTests.cs | 12 +++++++----- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs index 39e7424d0b..294e40cd78 100644 --- a/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs +++ b/src/ServiceControl.Audit.UnitTests/PopulateAppSettingsTests.cs @@ -3,6 +3,7 @@ namespace ServiceControl.UnitTests; using System; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using NUnit.Framework; @@ -25,16 +26,13 @@ public async Task Should_populate_appSettings_from_exe_config_file() await File.WriteAllTextAsync("ServiceControl.Audit.exe.config", config); -#if WINDOWS - const string fileName = "ServiceControl.Audit.exe"; -#else - const string fileName = "ServiceControl.Audit"; -#endif - var startInfo = new ProcessStartInfo(fileName) + var fileName = "ServiceControl.Audit"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - RedirectStandardOutput = true, - UseShellExecute = false - }; + fileName = "ServiceControl.Audit.exe"; + } + + var startInfo = new ProcessStartInfo(fileName) { RedirectStandardOutput = true, UseShellExecute = false }; var p = Process.Start(startInfo); diff --git a/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs index 6839f75681..b7abd42264 100644 --- a/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs +++ b/src/ServiceControl.Monitoring.UnitTests/PopulateAppSettingsTests.cs @@ -3,6 +3,7 @@ namespace ServiceControl.UnitTests; using System; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using NUnit.Framework; @@ -25,11 +26,12 @@ public async Task Should_populate_appSettings_from_exe_config_file() await File.WriteAllTextAsync("ServiceControl.Monitoring.exe.config", config); -#if WINDOWS - const string fileName = "ServiceControl.Monitoring.exe"; -#else - const string fileName = "ServiceControl.Monitoring"; -#endif + var fileName = "ServiceControl.Monitoring"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fileName = "ServiceControl.Monitoring.exe"; + } + var startInfo = new ProcessStartInfo(fileName) { RedirectStandardOutput = true, diff --git a/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs b/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs index fd7f1eb0f0..28b5e3e088 100644 --- a/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs +++ b/src/ServiceControl.UnitTests/PopulateAppSettingsTests.cs @@ -3,6 +3,7 @@ namespace ServiceControl.UnitTests; using System; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; using NUnit.Framework; @@ -25,11 +26,12 @@ public async Task Should_populate_appSettings_from_exe_config_file() await File.WriteAllTextAsync("ServiceControl.exe.config", config); -#if WINDOWS - const string fileName = "ServiceControl.exe"; -#else - const string fileName = "ServiceControl"; -#endif + var fileName = "ServiceControl"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fileName = "ServiceControl.exe"; + } + var startInfo = new ProcessStartInfo(fileName) { RedirectStandardOutput = true,