Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions src/PluginRegistry/PluginHashGenerator.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,35 @@ public static partial class PluginValidator
{
/// <summary>
/// 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
/// </summary>
public static Dictionary<string, string> GetBuiltInPluginHashes()
{
return new Dictionary<string, string>(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",

};
}
Expand Down
25 changes: 18 additions & 7 deletions src/PluginRegistry/PluginRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}

/// <summary>
Expand Down
23 changes: 19 additions & 4 deletions src/RegexColumnizer.UnitTests/RegexColumnizerErrorHandlingTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.Versioning;
using System.Windows.Forms;

using ColumnizerLib;

Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -173,12 +181,19 @@ public void LoadConfig_CorruptXmlFile_DisplaysErrorAndUsesDefaults ()
string xmlPath = Path.Join(_testDirectory, "Regex1Columnizer.xml");
File.WriteAllText(xmlPath, "<InvalidXml>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"));
}

Expand Down
12 changes: 10 additions & 2 deletions src/RegexColumnizer.UnitTests/RegexColumnizerLoadConfigTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.Versioning;
using System.Windows.Forms;

using NUnit.Framework;

Expand Down Expand Up @@ -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"));
}

Expand Down
3 changes: 3 additions & 0 deletions src/RegexColumnizer/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("LogExpert.RegexColumnizer.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100619e9beea345a3bb5e15f55b29ddf40d96e9bb473ae58304fc63dfb3e9c94d8944bb7e45324ee0bef3e345dccba79b0bf64b85a128a7f261861899add639218ddaeb2acc6fcc746d6acb5bb212d375a0967756af192cfdb6cf0bff666a0fe535600abda860d3eafaff4ef1c9b5710181f72d996ca9c29ed64bae4a5fd916dea5")]
21 changes: 12 additions & 9 deletions src/RegexColumnizer/RegexColumnizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string, MessageBoxIcon> ShowError { get; set; } = static (message, title, icon) => MessageBox.Show(message, title, MessageBoxButtons.OK, icon);
#endregion

#region Properties
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/RegexColumnizer/RegexColumnizer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<CustomToolNamespace></CustomToolNamespace>
</EmbeddedResource>

<EmbeddedResource Update="Resources.de.resx">
<DependentUpon>Resources.resx</DependentUpon>
</EmbeddedResource>
Expand All @@ -41,7 +41,7 @@
</Compile>
</ItemGroup>

<ItemGroup>
<ItemGroup>
<None Update="RegexColumnizer.manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down