diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs
index 21fff2a6..12fde81c 100644
--- a/src/PluginRegistry/PluginHashGenerator.Generated.cs
+++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs
@@ -10,7 +10,7 @@ public static partial class PluginValidator
{
///
/// Gets pre-calculated SHA256 hashes for built-in plugins.
- /// Generated: 2026-06-03 11:38:48 UTC
+ /// Generated: 2026-06-08 11:41:54 UTC
/// Configuration: Release
/// Plugin count: 21
///
@@ -18,27 +18,27 @@ public static Dictionary GetBuiltInPluginHashes()
{
return new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- ["AutoColumnizer.dll"] = "B0965F04F73C61B0BC29208E9E652A9DB208E2E98C277F324C5E9FB51C2E8BD5",
+ ["AutoColumnizer.dll"] = "FAE9B82D5E6C5D84F9A9D605F0BD4907275B8606224E9FEA63384EDFEF37C6E2",
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
- ["CsvColumnizer.dll"] = "CEC990903F1BD94965E37726A6025D8501C18C9932D96B111FA3FB9CDF71FCF7",
- ["CsvColumnizer.dll (x86)"] = "CEC990903F1BD94965E37726A6025D8501C18C9932D96B111FA3FB9CDF71FCF7",
- ["DefaultPlugins.dll"] = "B0766FFD6AA499BE4A854013B280A5DFF490B989EDB7406468D266A1DF892EBE",
- ["FlashIconHighlighter.dll"] = "456544EA8F97FF53DD2203F99A00A0F7A669F355C1A6E9B0A6F5F44A498B52CA",
- ["GlassfishColumnizer.dll"] = "64021C14C36807716462B9E9AC2B7AD5514061D74F90D08074D0BBAFF74BE3CD",
- ["JsonColumnizer.dll"] = "F1D9F58572FC6795FCB855EE70A7F0717C291BFA0623B8D49B5ADB3381DEFC4B",
- ["JsonCompactColumnizer.dll"] = "4A9CC547B1F05C073D66216F0A05D416D316D4A784D3DC7993AE127F8A83D916",
- ["Log4jXmlColumnizer.dll"] = "04FD914B7587A5BF29B3D0DF13B0F939D50C9C78A1C3988B25FC5E6CE40302A6",
- ["LogExpert.Resources.dll"] = "DF7378CA858BC60D5A781331D8CC6B79CF1026075D20202E6E7A384805351889",
+ ["CsvColumnizer.dll"] = "A32752B4A6A32D848714F76802CAD2C594111B9E82BB82321596BDF088742D35",
+ ["CsvColumnizer.dll (x86)"] = "A32752B4A6A32D848714F76802CAD2C594111B9E82BB82321596BDF088742D35",
+ ["DefaultPlugins.dll"] = "CE38F9B211EFC963BDF32435888AB9B57EAB67A4AF30BCD81B45CECC7F7A083E",
+ ["FlashIconHighlighter.dll"] = "5FCE162F24D638BA1FBBED873E730D91EE4369CABF2EB8622823D067F8807A58",
+ ["GlassfishColumnizer.dll"] = "230440A980B3CA824F4ED9CE15C790DE7D1E6A5347B5B560D18F553445FE9D87",
+ ["JsonColumnizer.dll"] = "52FDCE7A4527CAC3FF52638D6C71DCFB476BE3EA71DFF169844250E4A94630F7",
+ ["JsonCompactColumnizer.dll"] = "9A64B9CA107428C2AA4D11089A7E02F70CD2226001401574A21A5009B8E09F1F",
+ ["Log4jXmlColumnizer.dll"] = "BB21894E3E2E417970808C5489B2E105AD0093FC63CFA63D802C44F0CFCC07F8",
+ ["LogExpert.Resources.dll"] = "2B319CB107E22D7E1FCBCB553F9A5D19EBAF6CA1B9AE906BED8C5F46BCF31A46",
["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
- ["RegexColumnizer.dll"] = "36EB2C8B04313A4FE068CB7F69C057EF441471D12E636797092C87A63FF021A0",
- ["SftpFileSystem.dll"] = "166BAA0702A9C0913CDA6F6B529601911BF145FDE7BB74A37C6CF086CAADB63C",
- ["SftpFileSystem.dll (x86)"] = "B0ECE111B2C74EFD52181D070A757B1ACC3005B6C714FD231FAC73B26EFE9A90",
- ["SftpFileSystem.Resources.dll"] = "BD2280AC54C20AC563000128080A2C9B7C8B6E689353E319FB7AE5371225FEFA",
- ["SftpFileSystem.Resources.dll (x86)"] = "BD2280AC54C20AC563000128080A2C9B7C8B6E689353E319FB7AE5371225FEFA",
+ ["RegexColumnizer.dll"] = "AE31F0AD10FDEEB9AE403E57DAEDC320820F6C8FA653397A13DEEDE9C61CE796",
+ ["SftpFileSystem.dll"] = "01022F360A3D56E2E6C70BFDE496CF34B480C16067A174341929E49C23524C90",
+ ["SftpFileSystem.dll (x86)"] = "9740AE69A43EF9DB24132DFC1DC61540C6CF2438CC50800EAD3D80455D067E12",
+ ["SftpFileSystem.Resources.dll"] = "9753DC9D7191CD5EF9D65F206068FACE05F0BFE9A64272B3B8830ADDCE7D0F75",
+ ["SftpFileSystem.Resources.dll (x86)"] = "9753DC9D7191CD5EF9D65F206068FACE05F0BFE9A64272B3B8830ADDCE7D0F75",
};
}
diff --git a/src/PluginRegistry/PluginRegistry.cs b/src/PluginRegistry/PluginRegistry.cs
index 3eb28c49..431c75fe 100644
--- a/src/PluginRegistry/PluginRegistry.cs
+++ b/src/PluginRegistry/PluginRegistry.cs
@@ -28,7 +28,7 @@ public class PluginRegistry : IPluginRegistry
#region Fields
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
- private static PluginRegistry? _instance;
+ private static volatile PluginRegistry? _instance;
private static readonly Lock _lock = new();
private readonly IFileSystemCallback _fileSystemCallback = new FileSystemCallback();
@@ -101,14 +101,25 @@ public static PluginRegistry Create (string applicationConfigurationFolder, int
lock (_lock)
{
- _instance = new PluginRegistry(applicationConfigurationFolder, pollingInterval);
- }
+ // Re-check inside the lock: another thread may have created the
+ // instance while this one was waiting on the lock.
+ // Happens mostly in UnitTests where Create() is called multiple times.
+ // CA1508 is not applicable here because the null check is intentional for initialization.
+#pragma warning disable CA1508 // Avoid dead conditional code
+ if (_instance == null)
+ {
+ var registry = new PluginRegistry(applicationConfigurationFolder, pollingInterval);
- _applicationConfigurationFolder = applicationConfigurationFolder;
- PollingInterval = pollingInterval;
+ // Fully initialize before publishing so the lock-free fast path
+ // above never returns a half-loaded registry.
+ registry.LoadPlugins();
+
+ _instance = registry;
+ }
+#pragma warning restore CA1508 // Avoid dead conditional code
+ }
- _instance.LoadPlugins();
- return Instance;
+ return _instance;
}
///
diff --git a/src/RegexColumnizer.UnitTests/RegexColumnizerErrorHandlingTests.cs b/src/RegexColumnizer.UnitTests/RegexColumnizerErrorHandlingTests.cs
index c2d4ff30..83f51fcb 100644
--- a/src/RegexColumnizer.UnitTests/RegexColumnizerErrorHandlingTests.cs
+++ b/src/RegexColumnizer.UnitTests/RegexColumnizerErrorHandlingTests.cs
@@ -1,4 +1,5 @@
using System.Runtime.Versioning;
+using System.Windows.Forms;
using ColumnizerLib;
@@ -127,12 +128,19 @@ public void LoadConfig_CorruptJsonFile_DisplaysErrorAndUsesDefaults ()
string jsonPath = Path.Join(_testDirectory, "Regex1Columnizer.json");
File.WriteAllText(jsonPath, "{ corrupt json content }");
- var columnizer = new Regex1Columnizer();
+ var errors = new List<(string Message, string Title, MessageBoxIcon Icon)>();
+ var columnizer = new Regex1Columnizer
+ {
+ ShowError = (message, title, icon) => errors.Add((message, title, icon))
+ };
// Act - Should not throw, should handle gracefully
Assert.DoesNotThrow(() => columnizer.LoadConfig(_testDirectory));
- // Assert - Should fall back to defaults
+ // Assert - error was reported (dialog would have shown in release)
+ Assert.That(errors, Has.Count.EqualTo(1));
+
+ // Assert - and should have fallen back to defaults
Assert.That(columnizer.GetName(), Is.EqualTo("Regex1"));
Assert.That(columnizer.GetColumnCount(), Is.GreaterThan(0));
}
@@ -173,12 +181,19 @@ public void LoadConfig_CorruptXmlFile_DisplaysErrorAndUsesDefaults ()
string xmlPath = Path.Join(_testDirectory, "Regex1Columnizer.xml");
File.WriteAllText(xmlPath, "No closing tag");
- var columnizer = new Regex1Columnizer();
+ var errors = new List<(string Message, string Title, MessageBoxIcon Icon)>();
+ var columnizer = new Regex1Columnizer
+ {
+ ShowError = (message, title, icon) => errors.Add((message, title, icon))
+ };
// Act - Should not throw
Assert.DoesNotThrow(() => columnizer.LoadConfig(_testDirectory));
- // Assert - Should fall back to defaults
+ // Assert - error was reported (dialog would have shown in release)
+ Assert.That(errors, Has.Count.EqualTo(1));
+
+ // Assert - and should have fallen back to defaults
Assert.That(columnizer.GetName(), Is.EqualTo("Regex1"));
}
diff --git a/src/RegexColumnizer.UnitTests/RegexColumnizerLoadConfigTests.cs b/src/RegexColumnizer.UnitTests/RegexColumnizerLoadConfigTests.cs
index 70d5d0c0..eecf40c1 100644
--- a/src/RegexColumnizer.UnitTests/RegexColumnizerLoadConfigTests.cs
+++ b/src/RegexColumnizer.UnitTests/RegexColumnizerLoadConfigTests.cs
@@ -1,4 +1,5 @@
using System.Runtime.Versioning;
+using System.Windows.Forms;
using NUnit.Framework;
@@ -119,12 +120,19 @@ public void LoadConfig_CorruptJsonFile_FallsBackToDefault ()
string jsonPath = Path.Join(_testDirectory, "Regex1Columnizer.json");
File.WriteAllText(jsonPath, "{ this is not valid json }");
- var columnizer = new Regex1Columnizer();
+ var errors = new List<(string Message, string Title, MessageBoxIcon Icon)>();
+ var columnizer = new Regex1Columnizer
+ {
+ ShowError = (message, title, icon) => errors.Add((message, title, icon))
+ };
// Act - Should not throw, should fall back to defaults
Assert.DoesNotThrow(() => columnizer.LoadConfig(_testDirectory));
- // Assert
+ // Assert - error was reported (dialog would have shown in release)
+ Assert.That(errors, Has.Count.EqualTo(1));
+
+ // Assert - and should have fallen back to defaults
Assert.That(columnizer.GetName(), Is.EqualTo("Regex1"));
}
diff --git a/src/RegexColumnizer/AssemblyInfo.cs b/src/RegexColumnizer/AssemblyInfo.cs
new file mode 100644
index 00000000..4d520926
--- /dev/null
+++ b/src/RegexColumnizer/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("LogExpert.RegexColumnizer.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100619e9beea345a3bb5e15f55b29ddf40d96e9bb473ae58304fc63dfb3e9c94d8944bb7e45324ee0bef3e345dccba79b0bf64b85a128a7f261861899add639218ddaeb2acc6fcc746d6acb5bb212d375a0967756af192cfdb6cf0bff666a0fe535600abda860d3eafaff4ef1c9b5710181f72d996ca9c29ed64bae4a5fd916dea5")]
\ No newline at end of file
diff --git a/src/RegexColumnizer/RegexColumnizer.cs b/src/RegexColumnizer/RegexColumnizer.cs
index b13cae87..f31ae313 100644
--- a/src/RegexColumnizer/RegexColumnizer.cs
+++ b/src/RegexColumnizer/RegexColumnizer.cs
@@ -28,6 +28,10 @@ public abstract class BaseRegexColumnizer : ILogLineMemoryColumnizer, IColumnize
private readonly XmlSerializer _xml = new(typeof(RegexColumnizerConfig));
private string[] _columns;
private RegexColumnizerConfig _config;
+
+ // Seam for error notification.
+ // Defaults to a modal dialog, in tests replaced so they can assert without blocking on a headless runner.
+ internal Action ShowError { get; set; } = static (message, title, icon) => MessageBox.Show(message, title, MessageBoxButtons.OK, icon);
#endregion
#region Properties
@@ -233,7 +237,7 @@ ArgumentNullException or
FileNotFoundException or
DirectoryNotFoundException)
{
- _ = MessageBox.Show(ex.Message, Resources.RegexColumnizer_UI_Title_Deserialize);
+ ShowError(ex.Message, Resources.RegexColumnizer_UI_Title_Deserialize, MessageBoxIcon.Error);
_config = new RegexColumnizerConfig
{
Name = GetName()
@@ -252,7 +256,7 @@ FileNotFoundException or
}
catch (JsonException ex)
{
- _ = MessageBox.Show(ex.Message, Resources.RegexColumnizer_UI_Title_Deserialize);
+ ShowError(ex.Message, Resources.RegexColumnizer_UI_Title_Deserialize, MessageBoxIcon.Error);
_config = new RegexColumnizerConfig
{
Name = GetName()
@@ -424,10 +428,9 @@ public void Configure (ILogLineMemoryColumnizerCallback callback, string configD
catch (Exception ex) when (ex is IOException or
UnauthorizedAccessException)
{
- _ = MessageBox.Show(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_FailedToCreateConfigurationDirectory, ex.Message),
- Resources.RegexColumnizer_UI_Title_Error,
- MessageBoxButtons.OK,
- MessageBoxIcon.Error);
+ ShowError(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_FailedToCreateConfigurationDirectory, ex.Message),
+ Resources.RegexColumnizer_UI_Title_Error,
+ MessageBoxIcon.Error);
return;
}
}
@@ -460,15 +463,15 @@ public void Configure (ILogLineMemoryColumnizerCallback callback, string configD
}
catch (RegexMatchTimeoutException ex)
{
- _ = MessageBox.Show(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_RegexTimeout, ex.Message), Resources.RegexColumnizer_UI_Title_Warning, MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ ShowError(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_RegexTimeout, ex.Message), Resources.RegexColumnizer_UI_Title_Warning, MessageBoxIcon.Error);
}
catch (ArgumentException ex)
{
- _ = MessageBox.Show(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_InvalidRegexPattern, ex.Message), Resources.RegexColumnizer_UI_Title_Error, MessageBoxButtons.OK, MessageBoxIcon.Error);
+ ShowError(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_InvalidRegexPattern, ex.Message), Resources.RegexColumnizer_UI_Title_Error, MessageBoxIcon.Error);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
- _ = MessageBox.Show(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_FailedToSaveConfiguration, ex.Message), Resources.RegexColumnizer_UI_Title_Error, MessageBoxButtons.OK, MessageBoxIcon.Error);
+ ShowError(string.Format(CultureInfo.InvariantCulture, Resources.RegexColumnizer_UI_Message_FailedToSaveConfiguration, ex.Message), Resources.RegexColumnizer_UI_Title_Error, MessageBoxIcon.Error);
}
}
}
diff --git a/src/RegexColumnizer/RegexColumnizer.csproj b/src/RegexColumnizer/RegexColumnizer.csproj
index 6797cf88..dcffd8ba 100644
--- a/src/RegexColumnizer/RegexColumnizer.csproj
+++ b/src/RegexColumnizer/RegexColumnizer.csproj
@@ -27,7 +27,7 @@
Resources.Designer.cs
-
+
Resources.resx
@@ -41,7 +41,7 @@
-
+
PreserveNewest