From 860b211af741c4ed0097c843e3734f2e368dbb20 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Wed, 6 May 2026 20:59:25 -0500 Subject: [PATCH 01/24] Move P/Invoke types to Interop folder and rename Interop class to Win32ErrorCodes --- .../Helpers/EventMethodsTests.cs | 26 +++++----- .../Providers/EventMessageProviderTests.cs | 4 +- .../EventResolvers/EventXmlResolver.cs | 1 + .../Helpers/EventMethods.cs | 49 ++++++++++--------- .../Helpers/NativeMethods.cs | 8 +-- .../{Models => Interop}/EvtHandle.cs | 2 +- .../{Models => Interop}/EvtVariant.cs | 2 +- .../{Models => Interop}/LibraryHandle.cs | 2 +- .../MessageResourceBlock.cs | 2 +- .../{Models => Interop}/SystemTime.cs | 2 +- .../Interop.cs => Interop/Win32ErrorCodes.cs} | 8 +-- .../Providers/EventMessageProvider.cs | 3 +- .../Providers/ProviderMetadata.cs | 11 +++-- .../Readers/EventLogInformation.cs | 6 +-- .../Readers/EventLogReader.cs | 7 +-- .../Readers/EventLogSession.cs | 12 ++--- .../Readers/EventLogWatcher.cs | 1 + 17 files changed, 76 insertions(+), 70 deletions(-) rename src/EventLogExpert.Eventing/{Models => Interop}/EvtHandle.cs (94%) rename src/EventLogExpert.Eventing/{Models => Interop}/EvtVariant.cs (97%) rename src/EventLogExpert.Eventing/{Models => Interop}/LibraryHandle.cs (94%) rename src/EventLogExpert.Eventing/{Models => Interop}/MessageResourceBlock.cs (88%) rename src/EventLogExpert.Eventing/{Models => Interop}/SystemTime.cs (94%) rename src/EventLogExpert.Eventing/{Helpers/Interop.cs => Interop/Win32ErrorCodes.cs} (93%) diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs index e4037139..41fc10dc 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Interop; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -750,7 +750,7 @@ public void ConvertVariant_WhenXml_ShouldReturnString() public void ThrowEventLogException_WhenAccessDenied_ShouldThrowUnauthorizedAccessException() { // Arrange - int error = Interop.ERROR_ACCESS_DENIED; + int error = Win32ErrorCodes.ERROR_ACCESS_DENIED; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -760,7 +760,7 @@ public void ThrowEventLogException_WhenAccessDenied_ShouldThrowUnauthorizedAcces public void ThrowEventLogException_WhenCancelled_ShouldThrowOperationCanceledException() { // Arrange - int error = Interop.ERROR_CANCELLED; + int error = Win32ErrorCodes.ERROR_CANCELLED; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -770,7 +770,7 @@ public void ThrowEventLogException_WhenCancelled_ShouldThrowOperationCanceledExc public void ThrowEventLogException_WhenChannelNotFound_ShouldThrowFileNotFoundException() { // Arrange - int error = Interop.ERROR_EVT_CHANNEL_NOT_FOUND; + int error = Win32ErrorCodes.ERROR_EVT_CHANNEL_NOT_FOUND; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -780,7 +780,7 @@ public void ThrowEventLogException_WhenChannelNotFound_ShouldThrowFileNotFoundEx public void ThrowEventLogException_WhenFileNotFound_ShouldThrowFileNotFoundException() { // Arrange - int error = Interop.ERROR_FILE_NOT_FOUND; + int error = Win32ErrorCodes.ERROR_FILE_NOT_FOUND; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -790,7 +790,7 @@ public void ThrowEventLogException_WhenFileNotFound_ShouldThrowFileNotFoundExcep public void ThrowEventLogException_WhenInvalidData_ShouldThrowInvalidDataException() { // Arrange - int error = Interop.ERROR_INVALID_DATA; + int error = Win32ErrorCodes.ERROR_INVALID_DATA; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -800,7 +800,7 @@ public void ThrowEventLogException_WhenInvalidData_ShouldThrowInvalidDataExcepti public void ThrowEventLogException_WhenInvalidEventData_ShouldThrowInvalidDataException() { // Arrange - int error = Interop.ERROR_EVT_INVALID_EVENT_DATA; + int error = Win32ErrorCodes.ERROR_EVT_INVALID_EVENT_DATA; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -810,7 +810,7 @@ public void ThrowEventLogException_WhenInvalidEventData_ShouldThrowInvalidDataEx public void ThrowEventLogException_WhenInvalidHandle_ShouldThrowUnauthorizedAccessException() { // Arrange - int error = Interop.ERROR_INVALID_HANDLE; + int error = Win32ErrorCodes.ERROR_INVALID_HANDLE; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -820,7 +820,7 @@ public void ThrowEventLogException_WhenInvalidHandle_ShouldThrowUnauthorizedAcce public void ThrowEventLogException_WhenMessageIdNotFound_ShouldThrowFileNotFoundException() { // Arrange - int error = Interop.ERROR_EVT_MESSAGE_ID_NOT_FOUND; + int error = Win32ErrorCodes.ERROR_EVT_MESSAGE_ID_NOT_FOUND; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -830,7 +830,7 @@ public void ThrowEventLogException_WhenMessageIdNotFound_ShouldThrowFileNotFound public void ThrowEventLogException_WhenMessageNotFound_ShouldThrowFileNotFoundException() { // Arrange - int error = Interop.ERROR_EVT_MESSAGE_NOT_FOUND; + int error = Win32ErrorCodes.ERROR_EVT_MESSAGE_NOT_FOUND; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -840,7 +840,7 @@ public void ThrowEventLogException_WhenMessageNotFound_ShouldThrowFileNotFoundEx public void ThrowEventLogException_WhenPathNotFound_ShouldThrowFileNotFoundException() { // Arrange - int error = Interop.ERROR_PATH_NOT_FOUND; + int error = Win32ErrorCodes.ERROR_PATH_NOT_FOUND; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -850,7 +850,7 @@ public void ThrowEventLogException_WhenPathNotFound_ShouldThrowFileNotFoundExcep public void ThrowEventLogException_WhenPublisherMetadataNotFound_ShouldThrowFileNotFoundException() { // Arrange - int error = Interop.ERROR_EVT_PUBLISHER_METADATA_NOT_FOUND; + int error = Win32ErrorCodes.ERROR_EVT_PUBLISHER_METADATA_NOT_FOUND; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); @@ -860,7 +860,7 @@ public void ThrowEventLogException_WhenPublisherMetadataNotFound_ShouldThrowFile public void ThrowEventLogException_WhenRpcCallCanceled_ShouldThrowOperationCanceledException() { // Arrange - int error = Interop.RPC_S_CALL_CANCELED; + int error = Win32ErrorCodes.RPC_S_CALL_CANCELED; // Act & Assert Assert.Throws(() => EventMethods.ThrowEventLogException(error)); diff --git a/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs index a3055d71..ec980a2f 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; @@ -442,7 +442,7 @@ private static bool TryReadOwningPublisher(EvtHandle channelConfig, out string? int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { return false; } diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs index 6b63d847..f9fc6f8a 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; diff --git a/src/EventLogExpert.Eventing/Helpers/EventMethods.cs b/src/EventLogExpert.Eventing/Helpers/EventMethods.cs index 2f3f8b72..0a5ef5fa 100644 --- a/src/EventLogExpert.Eventing/Helpers/EventMethods.cs +++ b/src/EventLogExpert.Eventing/Helpers/EventMethods.cs @@ -1,6 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using Microsoft.Win32.SafeHandles; @@ -532,11 +533,11 @@ internal static string FormatMessage(EvtHandle publisherMetadataHandle, uint mes int error = Marshal.GetLastWin32Error(); if (!success && - error != Interop.ERROR_EVT_UNRESOLVED_VALUE_INSERT && - error != Interop.ERROR_EVT_UNRESOLVED_PARAMETER_INSERT && - error != Interop.ERROR_EVT_MAX_INSERTS_REACHED) + error != Win32ErrorCodes.ERROR_EVT_UNRESOLVED_VALUE_INSERT && + error != Win32ErrorCodes.ERROR_EVT_UNRESOLVED_PARAMETER_INSERT && + error != Win32ErrorCodes.ERROR_EVT_MAX_INSERTS_REACHED) { - if (error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { ThrowEventLogException(error); } @@ -564,9 +565,9 @@ internal static string FormatMessage(EvtHandle publisherMetadataHandle, uint mes error = Marshal.GetLastWin32Error(); if (!success && - error != Interop.ERROR_EVT_UNRESOLVED_VALUE_INSERT && - error != Interop.ERROR_EVT_UNRESOLVED_PARAMETER_INSERT && - error != Interop.ERROR_EVT_MAX_INSERTS_REACHED) + error != Win32ErrorCodes.ERROR_EVT_UNRESOLVED_VALUE_INSERT && + error != Win32ErrorCodes.ERROR_EVT_UNRESOLVED_PARAMETER_INSERT && + error != Win32ErrorCodes.ERROR_EVT_MAX_INSERTS_REACHED) { ThrowEventLogException(error); } @@ -587,7 +588,7 @@ internal static object GetObjectArrayProperty( bool success = EvtGetObjectArrayProperty(array, propertyId, index, 0, 0, IntPtr.Zero, out int bufferUsed); int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { ThrowEventLogException(error); } @@ -664,7 +665,7 @@ internal static EventRecord RenderEvent(EvtHandle eventHandle) int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { ThrowEventLogException(error); } @@ -726,7 +727,7 @@ internal static IReadOnlyList RenderEventProperties(EvtHandle eventHandl int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { ThrowEventLogException(error); } @@ -799,7 +800,7 @@ internal static IReadOnlyList RenderEventProperties(EvtHandle eventHandl int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { ThrowEventLogException(error); } @@ -848,21 +849,21 @@ internal static void ThrowEventLogException(int error) switch (error) { - case Interop.ERROR_FILE_NOT_FOUND: - case Interop.ERROR_PATH_NOT_FOUND: - case Interop.ERROR_EVT_CHANNEL_NOT_FOUND: - case Interop.ERROR_EVT_MESSAGE_NOT_FOUND: - case Interop.ERROR_EVT_MESSAGE_ID_NOT_FOUND: - case Interop.ERROR_EVT_PUBLISHER_METADATA_NOT_FOUND: + case Win32ErrorCodes.ERROR_FILE_NOT_FOUND: + case Win32ErrorCodes.ERROR_PATH_NOT_FOUND: + case Win32ErrorCodes.ERROR_EVT_CHANNEL_NOT_FOUND: + case Win32ErrorCodes.ERROR_EVT_MESSAGE_NOT_FOUND: + case Win32ErrorCodes.ERROR_EVT_MESSAGE_ID_NOT_FOUND: + case Win32ErrorCodes.ERROR_EVT_PUBLISHER_METADATA_NOT_FOUND: throw new FileNotFoundException(message); - case Interop.ERROR_INVALID_DATA: - case Interop.ERROR_EVT_INVALID_EVENT_DATA: + case Win32ErrorCodes.ERROR_INVALID_DATA: + case Win32ErrorCodes.ERROR_EVT_INVALID_EVENT_DATA: throw new InvalidDataException(message); - case Interop.RPC_S_CALL_CANCELED: - case Interop.ERROR_CANCELLED: + case Win32ErrorCodes.RPC_S_CALL_CANCELED: + case Win32ErrorCodes.ERROR_CANCELLED: throw new OperationCanceledException(message); - case Interop.ERROR_ACCESS_DENIED: - case Interop.ERROR_INVALID_HANDLE: + case Win32ErrorCodes.ERROR_ACCESS_DENIED: + case Win32ErrorCodes.ERROR_INVALID_HANDLE: throw new UnauthorizedAccessException(); default: throw new Exception(message); diff --git a/src/EventLogExpert.Eventing/Helpers/NativeMethods.cs b/src/EventLogExpert.Eventing/Helpers/NativeMethods.cs index a1a2e55f..79de424a 100644 --- a/src/EventLogExpert.Eventing/Helpers/NativeMethods.cs +++ b/src/EventLogExpert.Eventing/Helpers/NativeMethods.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Interop; using System.Runtime.InteropServices; // ReSharper disable InconsistentNaming @@ -147,7 +147,7 @@ internal static partial LibraryHandle LoadLibraryExW( if (length > 0) { return TrimFormatMessageResult(stackBuffer[..length]); } - if (Marshal.GetLastWin32Error() != Interop.ERROR_INSUFFICIENT_BUFFER) { return null; } + if (Marshal.GetLastWin32Error() != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { return null; } // Retry with progressively larger pooled buffers ReadOnlySpan retrySizes = [2048, 8192, 32768]; @@ -162,7 +162,7 @@ internal static partial LibraryHandle LoadLibraryExW( if (length > 0) { return TrimFormatMessageResult(rented.AsSpan(0, length)); } - if (Marshal.GetLastWin32Error() != Interop.ERROR_INSUFFICIENT_BUFFER) { return null; } + if (Marshal.GetLastWin32Error() != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { return null; } } finally { diff --git a/src/EventLogExpert.Eventing/Models/EvtHandle.cs b/src/EventLogExpert.Eventing/Interop/EvtHandle.cs similarity index 94% rename from src/EventLogExpert.Eventing/Models/EvtHandle.cs rename to src/EventLogExpert.Eventing/Interop/EvtHandle.cs index 22742723..8687d8f0 100644 --- a/src/EventLogExpert.Eventing/Models/EvtHandle.cs +++ b/src/EventLogExpert.Eventing/Interop/EvtHandle.cs @@ -4,7 +4,7 @@ using EventLogExpert.Eventing.Helpers; using Microsoft.Win32.SafeHandles; -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Interop; internal sealed partial class EvtHandle : SafeHandleZeroOrMinusOneIsInvalid { diff --git a/src/EventLogExpert.Eventing/Models/EvtVariant.cs b/src/EventLogExpert.Eventing/Interop/EvtVariant.cs similarity index 97% rename from src/EventLogExpert.Eventing/Models/EvtVariant.cs rename to src/EventLogExpert.Eventing/Interop/EvtVariant.cs index 1dd935a9..813b0819 100644 --- a/src/EventLogExpert.Eventing/Models/EvtVariant.cs +++ b/src/EventLogExpert.Eventing/Interop/EvtVariant.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Interop; [StructLayout(LayoutKind.Explicit)] internal readonly record struct EvtVariant diff --git a/src/EventLogExpert.Eventing/Models/LibraryHandle.cs b/src/EventLogExpert.Eventing/Interop/LibraryHandle.cs similarity index 94% rename from src/EventLogExpert.Eventing/Models/LibraryHandle.cs rename to src/EventLogExpert.Eventing/Interop/LibraryHandle.cs index a49d733f..c926a3ad 100644 --- a/src/EventLogExpert.Eventing/Models/LibraryHandle.cs +++ b/src/EventLogExpert.Eventing/Interop/LibraryHandle.cs @@ -4,7 +4,7 @@ using EventLogExpert.Eventing.Helpers; using Microsoft.Win32.SafeHandles; -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Interop; internal sealed partial class LibraryHandle : SafeHandleZeroOrMinusOneIsInvalid { diff --git a/src/EventLogExpert.Eventing/Models/MessageResourceBlock.cs b/src/EventLogExpert.Eventing/Interop/MessageResourceBlock.cs similarity index 88% rename from src/EventLogExpert.Eventing/Models/MessageResourceBlock.cs rename to src/EventLogExpert.Eventing/Interop/MessageResourceBlock.cs index 846fa2f7..75ca1aab 100644 --- a/src/EventLogExpert.Eventing/Models/MessageResourceBlock.cs +++ b/src/EventLogExpert.Eventing/Interop/MessageResourceBlock.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Interop; [StructLayout(LayoutKind.Sequential)] internal readonly record struct MessageResourceBlock diff --git a/src/EventLogExpert.Eventing/Models/SystemTime.cs b/src/EventLogExpert.Eventing/Interop/SystemTime.cs similarity index 94% rename from src/EventLogExpert.Eventing/Models/SystemTime.cs rename to src/EventLogExpert.Eventing/Interop/SystemTime.cs index 32c787f0..bb5c8d20 100644 --- a/src/EventLogExpert.Eventing/Models/SystemTime.cs +++ b/src/EventLogExpert.Eventing/Interop/SystemTime.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Interop; [StructLayout(LayoutKind.Sequential)] internal readonly record struct SystemTime diff --git a/src/EventLogExpert.Eventing/Helpers/Interop.cs b/src/EventLogExpert.Eventing/Interop/Win32ErrorCodes.cs similarity index 93% rename from src/EventLogExpert.Eventing/Helpers/Interop.cs rename to src/EventLogExpert.Eventing/Interop/Win32ErrorCodes.cs index a7991618..c01d9b02 100644 --- a/src/EventLogExpert.Eventing/Helpers/Interop.cs +++ b/src/EventLogExpert.Eventing/Interop/Win32ErrorCodes.cs @@ -2,9 +2,9 @@ // // Licensed under the MIT License. // ReSharper disable InconsistentNaming -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Interop; -internal static class Interop +internal static class Win32ErrorCodes { internal const int ERROR_FILE_NOT_FOUND = 2; internal const int ERROR_PATH_NOT_FOUND = 3; @@ -12,13 +12,13 @@ internal static class Interop internal const int ERROR_INVALID_HANDLE = 6; internal const int ERROR_INVALID_DATA = 13; internal const int ERROR_INVALID_PARAMETER = 87; - + internal const int ERROR_INSUFFICIENT_BUFFER = 122; internal const int ERROR_NO_MORE_ITEMS = 259; internal const int RPC_S_CALL_CANCELED = 1818; internal const int ERROR_CANCELLED = 1223; - + internal const int ERROR_EVT_PUBLISHER_METADATA_NOT_FOUND = 0x3A9A; internal const int ERROR_EVT_INVALID_EVENT_DATA = 0x3A9D; internal const int ERROR_EVT_CHANNEL_NOT_FOUND = 0x3A9F; diff --git a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs index 72d36fc0..63e448dd 100644 --- a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using System.Runtime.InteropServices; @@ -473,7 +474,7 @@ private bool TryGetChannelOwningPublisher(string channelName, out string? publis int sizeError = Marshal.GetLastWin32Error(); - if (!success && sizeError != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && sizeError != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { _logger?.Debug( $"{nameof(TryGetChannelOwningPublisher)}: size probe failed for {channelName}. Error: {sizeError}"); diff --git a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs index 58b5213b..2f7d108b 100644 --- a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs +++ b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using System.Collections.ObjectModel; @@ -336,7 +337,7 @@ private static object GetEventMetadataProperty(EvtHandle metadataHandle, EvtEven bool success = EventMethods.EvtGetEventMetadataProperty(metadataHandle, propertyId, 0, 0, IntPtr.Zero, out int bufferSize); int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { EventMethods.ThrowEventLogException(error); } @@ -369,11 +370,11 @@ private static object GetEventMetadataProperty(EvtHandle metadataHandle, EvtEven if (!handle.IsInvalid) { return handle; } - if (error != Interop.ERROR_NO_MORE_ITEMS) + if (error != Win32ErrorCodes.ERROR_NO_MORE_ITEMS) { EventMethods.ThrowEventLogException(error); } - + return null; } @@ -393,7 +394,7 @@ private string GetPublisherMetadataProperty(EvtPublisherMetadataPropertyId prope int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { EventMethods.ThrowEventLogException(error); } @@ -441,7 +442,7 @@ private EvtHandle GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropert int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { EventMethods.ThrowEventLogException(error); } diff --git a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs index ddaf43bf..bec1b6d9 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs @@ -1,8 +1,8 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Interop; using System.Runtime.InteropServices; namespace EventLogExpert.Eventing.Readers; @@ -48,7 +48,7 @@ internal EventLogInformation(EventLogSession session, string logName, PathType p bool success = EventMethods.EvtGetLogInfo(handle, property, 0, IntPtr.Zero, out int bufferSize); int error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { EventMethods.ThrowEventLogException(error); } diff --git a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs index c957ee9a..9ae557bd 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs @@ -1,7 +1,8 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using System.Buffers; using System.Runtime.InteropServices; @@ -66,7 +67,7 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) if (!success) { var error = Marshal.GetLastWin32Error(); - LastErrorCode = error != Interop.ERROR_NO_MORE_ITEMS ? error : null; + LastErrorCode = error != Win32ErrorCodes.ERROR_NO_MORE_ITEMS ? error : null; events = []; return false; } @@ -142,7 +143,7 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) error = Marshal.GetLastWin32Error(); - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { EventMethods.ThrowEventLogException(error); } diff --git a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs index b91fa4cb..59afd9db 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs @@ -1,8 +1,8 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Interop; using System.Runtime.InteropServices; namespace EventLogExpert.Eventing.Readers; @@ -109,14 +109,14 @@ private static string NextChannelPath(EvtHandle channelHandle, ref bool doneRead if (!success) { - if (error == Interop.ERROR_NO_MORE_ITEMS) + if (error == Win32ErrorCodes.ERROR_NO_MORE_ITEMS) { doneReading = true; return string.Empty; } - if (error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { EventMethods.ThrowEventLogException(error); } @@ -142,14 +142,14 @@ private static string NextPublisherId(EvtHandle publisherHandle, ref bool doneRe if (!success) { - if (error == Interop.ERROR_NO_MORE_ITEMS) + if (error == Win32ErrorCodes.ERROR_NO_MORE_ITEMS) { doneReading = true; return string.Empty; } - if (error != Interop.ERROR_INSUFFICIENT_BUFFER) + if (error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { EventMethods.ThrowEventLogException(error); } diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index beb4ae03..79bbbe79 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using System.Runtime.InteropServices; From fbf9890d5416a67b1767b3176a7cec6d967e57e1 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Wed, 6 May 2026 21:25:26 -0500 Subject: [PATCH 02/24] Move and rename internal interop classes to Interop folder --- .../EventResolvers/EventResolverBaseTests.cs | 1 + ...dsTests.cs => NativeErrorResolverTests.cs} | 26 +++++++++---------- .../Helpers/NativeMethodsTests.cs | 2 +- .../EventResolvers/EventResolverBase.cs | 25 +++++++++--------- .../Helpers/EventMethods.cs | 2 +- .../HResultConverter.cs} | 4 +-- .../Interop/LibraryHandle.cs | 1 - .../NativeErrorResolver.cs} | 4 +-- .../{Helpers => Interop}/NativeMethods.cs | 3 +-- .../Providers/ProviderMetadata.cs | 4 +-- 10 files changed, 36 insertions(+), 36 deletions(-) rename src/EventLogExpert.Eventing.Tests/Helpers/{ResolverMethodsTests.cs => NativeErrorResolverTests.cs} (69%) rename src/EventLogExpert.Eventing/{Helpers/Converter.cs => Interop/HResultConverter.cs} (84%) rename src/EventLogExpert.Eventing/{Helpers/ResolverMethods.cs => Interop/NativeErrorResolver.cs} (96%) rename src/EventLogExpert.Eventing/{Helpers => Interop}/NativeMethods.cs (98%) diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs index 3d707165..fe40a900 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs @@ -3,6 +3,7 @@ using EventLogExpert.Eventing.EventResolvers; using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils; diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/ResolverMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Helpers/NativeErrorResolverTests.cs similarity index 69% rename from src/EventLogExpert.Eventing.Tests/Helpers/ResolverMethodsTests.cs rename to src/EventLogExpert.Eventing.Tests/Helpers/NativeErrorResolverTests.cs index daa65988..26ce5bef 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/ResolverMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Helpers/NativeErrorResolverTests.cs @@ -1,21 +1,21 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; namespace EventLogExpert.Eventing.Tests.Helpers; -public sealed class ResolverMethodsTests +public sealed class NativeErrorResolverTests { [Fact] public void GetErrorMessage_ShouldReturnConsistentResults() { // Arrange — use a well-known HRESULT (ERROR_SUCCESS = 0) - const uint errorSuccess = 0; + const uint ErrorSuccess = 0; // Act - var result1 = ResolverMethods.GetErrorMessage(errorSuccess); - var result2 = ResolverMethods.GetErrorMessage(errorSuccess); + var result1 = NativeErrorResolver.GetErrorMessage(ErrorSuccess); + var result2 = NativeErrorResolver.GetErrorMessage(ErrorSuccess); // Assert — same string returned (cached) Assert.NotNull(result1); @@ -26,16 +26,16 @@ public void GetErrorMessage_ShouldReturnConsistentResults() public void GetErrorMessage_WhenUnknownCode_ShouldReturnHexFallback() { // Arrange — use a code unlikely to have a system message - const uint unknownCode = 0xDEADBEEF; + const uint UnknownCode = 0xDEADBEEF; // Act - var result = ResolverMethods.GetErrorMessage(unknownCode); + var result = NativeErrorResolver.GetErrorMessage(UnknownCode); // Assert — if the OS doesn't resolve this code, we get the hex fallback. // On some OS versions/locales FormatMessage may resolve it, so only assert // the hex representation when the system has no message for this code. - var systemMessage = NativeMethods.FormatSystemMessage(unknownCode); - var ntStatusMessage = NativeMethods.FormatNtStatusMessage(unknownCode); + var systemMessage = NativeMethods.FormatSystemMessage(UnknownCode); + var ntStatusMessage = NativeMethods.FormatNtStatusMessage(UnknownCode); if (systemMessage is null && ntStatusMessage is null) { @@ -52,11 +52,11 @@ public void GetErrorMessage_WhenUnknownCode_ShouldReturnHexFallback() public void GetNtStatusMessage_ShouldReturnConsistentResults() { // Arrange — STATUS_SUCCESS = 0 - const uint statusSuccess = 0; + const uint StatusSuccess = 0; // Act - var result1 = ResolverMethods.GetNtStatusMessage(statusSuccess); - var result2 = ResolverMethods.GetNtStatusMessage(statusSuccess); + var result1 = NativeErrorResolver.GetNtStatusMessage(StatusSuccess); + var result2 = NativeErrorResolver.GetNtStatusMessage(StatusSuccess); // Assert Assert.NotNull(result1); @@ -67,6 +67,6 @@ public void GetNtStatusMessage_ShouldReturnConsistentResults() public void MaxCacheSize_ShouldBeReasonableBound() { // Assert — the cache limit is the expected constant - Assert.Equal(4096, ResolverMethods.MaxCacheSize); + Assert.Equal(4096, NativeErrorResolver.MaxCacheSize); } } diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs index 36fd68a2..c0b5c9c4 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; namespace EventLogExpert.Eventing.Tests.Helpers; diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs b/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs index 4e13abdf..0d2a917a 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using System.Buffers; @@ -172,18 +173,18 @@ private static void CleanupFormatting(ReadOnlySpan unformattedString, ref switch (unformattedString[i]) { case '%' when i + 1 < unformattedString.Length: - switch (unformattedString[i + 1]) - { - case 'n': - if (i + 2 >= unformattedString.Length || unformattedString[i + 2] != '\r') - { - buffer[bufferIndex++] = '\r'; - buffer[bufferIndex++] = '\n'; - } + switch (unformattedString[i + 1]) + { + case 'n': + if (i + 2 >= unformattedString.Length || unformattedString[i + 2] != '\r') + { + buffer[bufferIndex++] = '\r'; + buffer[bufferIndex++] = '\n'; + } - i++; + i++; - break; + break; case 't': buffer[bufferIndex++] = '\t'; i++; @@ -784,7 +785,7 @@ private List GetFormattedProperties(ReadOnlySpan template, IReadOn } else if (string.Equals(outType, "win:HResult", StringComparison.OrdinalIgnoreCase) && property is int hResult) { - formattedValues.Add(ResolverMethods.GetErrorMessage((uint)hResult)); + formattedValues.Add(NativeErrorResolver.GetErrorMessage((uint)hResult)); } else if (string.Equals(outType, "win:NTStatus", StringComparison.OrdinalIgnoreCase)) { @@ -801,7 +802,7 @@ private List GetFormattedProperties(ReadOnlySpan template, IReadOn }; formattedValues.Add(property is uint or int or ulong or long or ushort or short or byte - ? ResolverMethods.GetNtStatusMessage(statusCode) + ? NativeErrorResolver.GetNtStatusMessage(statusCode) : $"{property}"); } else diff --git a/src/EventLogExpert.Eventing/Helpers/EventMethods.cs b/src/EventLogExpert.Eventing/Helpers/EventMethods.cs index 0a5ef5fa..7b9ea236 100644 --- a/src/EventLogExpert.Eventing/Helpers/EventMethods.cs +++ b/src/EventLogExpert.Eventing/Helpers/EventMethods.cs @@ -845,7 +845,7 @@ internal static IReadOnlyList RenderEventProperties(EvtHandle eventHandl internal static void ThrowEventLogException(int error) { - var message = ResolverMethods.GetErrorMessage((uint)Converter.HResultFromWin32(error)); + var message = NativeErrorResolver.GetErrorMessage((uint)HResultConverter.HResultFromWin32(error)); switch (error) { diff --git a/src/EventLogExpert.Eventing/Helpers/Converter.cs b/src/EventLogExpert.Eventing/Interop/HResultConverter.cs similarity index 84% rename from src/EventLogExpert.Eventing/Helpers/Converter.cs rename to src/EventLogExpert.Eventing/Interop/HResultConverter.cs index f49c12be..0f723d09 100644 --- a/src/EventLogExpert.Eventing/Helpers/Converter.cs +++ b/src/EventLogExpert.Eventing/Interop/HResultConverter.cs @@ -3,9 +3,9 @@ using System.Runtime.CompilerServices; -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Interop; -internal static class Converter +internal static class HResultConverter { // Implementation of HRESULT_FROM_WIN32 macro [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/EventLogExpert.Eventing/Interop/LibraryHandle.cs b/src/EventLogExpert.Eventing/Interop/LibraryHandle.cs index c926a3ad..1295d37e 100644 --- a/src/EventLogExpert.Eventing/Interop/LibraryHandle.cs +++ b/src/EventLogExpert.Eventing/Interop/LibraryHandle.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using Microsoft.Win32.SafeHandles; namespace EventLogExpert.Eventing.Interop; diff --git a/src/EventLogExpert.Eventing/Helpers/ResolverMethods.cs b/src/EventLogExpert.Eventing/Interop/NativeErrorResolver.cs similarity index 96% rename from src/EventLogExpert.Eventing/Helpers/ResolverMethods.cs rename to src/EventLogExpert.Eventing/Interop/NativeErrorResolver.cs index 5bfff8d8..6a1b6240 100644 --- a/src/EventLogExpert.Eventing/Helpers/ResolverMethods.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeErrorResolver.cs @@ -3,9 +3,9 @@ using System.Collections.Concurrent; -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Interop; -internal static class ResolverMethods +internal static class NativeErrorResolver { internal const int MaxCacheSize = 4096; diff --git a/src/EventLogExpert.Eventing/Helpers/NativeMethods.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.cs similarity index 98% rename from src/EventLogExpert.Eventing/Helpers/NativeMethods.cs rename to src/EventLogExpert.Eventing/Interop/NativeMethods.cs index 79de424a..caddfe6c 100644 --- a/src/EventLogExpert.Eventing/Helpers/NativeMethods.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.cs @@ -1,14 +1,13 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Interop; using System.Runtime.InteropServices; // ReSharper disable InconsistentNaming // We are defining some win32 types in this file, so we // are not following the usual C# naming conventions. -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Interop; [Flags] internal enum LoadLibraryFlags : uint diff --git a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs index 2f7d108b..825d2536 100644 --- a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs +++ b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs @@ -33,7 +33,7 @@ private ProviderMetadata(string providerName, string? metadataPath = null) if (_publisherMetadataHandle.IsInvalid) { - Error = ResolverMethods.GetErrorMessage((uint)Converter.HResultFromWin32(error)); + Error = NativeErrorResolver.GetErrorMessage((uint)HResultConverter.HResultFromWin32(error)); } } @@ -91,7 +91,7 @@ public IEnumerable Events if (handle.IsInvalid) { - Error = ResolverMethods.GetErrorMessage((uint)Converter.HResultFromWin32(error)); + Error = NativeErrorResolver.GetErrorMessage((uint)HResultConverter.HResultFromWin32(error)); return events.AsReadOnly(); } From f5b7334249b8cd9386556973c1699adfc69399cc Mon Sep 17 00:00:00 2001 From: jschick04 Date: Wed, 6 May 2026 21:54:26 -0500 Subject: [PATCH 03/24] Split Helpers/EventMethods.cs into Interop partials and extract LoadLibraryFlags --- .../EventProviderDbContextTests.cs | 2 +- .../EventResolvers/EventResolverCacheTests.cs | 4 +- .../Helpers/EventMethodsTests.cs | 101 +++++----- .../Providers/EventMessageProviderTests.cs | 8 +- .../Providers/RegistryProviderTests.cs | 6 +- .../Readers/EventLogInformationTests.cs | 12 +- .../Readers/EventLogReaderTests.cs | 4 +- .../Readers/EventLogSessionTests.cs | 6 +- .../Readers/EventLogWatcherTests.cs | 12 +- .../EventProviderDbContext.cs | 2 +- .../EventResolvers/EventXmlResolver.cs | 6 +- .../Helpers/PathType.cs | 10 + .../Helpers/TraceLogger.cs | 6 +- .../Interop/EvtEnums.cs | 182 +++++++++++++++++ .../Interop/EvtHandle.cs | 5 +- .../Interop/LoadLibraryFlags.cs | 25 +++ .../NativeMethods.Evt.cs} | 190 +----------------- .../Interop/NativeMethods.cs | 17 -- .../Providers/EventMessageProvider.cs | 8 +- .../Providers/ProviderMetadata.cs | 74 +++---- .../Providers/RegistryProvider.cs | 2 +- .../Readers/EventLogInformation.cs | 12 +- .../Readers/EventLogReader.cs | 26 +-- .../Readers/EventLogSession.cs | 28 +-- .../Readers/EventLogWatcher.cs | 22 +- 25 files changed, 392 insertions(+), 378 deletions(-) create mode 100644 src/EventLogExpert.Eventing/Helpers/PathType.cs create mode 100644 src/EventLogExpert.Eventing/Interop/EvtEnums.cs create mode 100644 src/EventLogExpert.Eventing/Interop/LoadLibraryFlags.cs rename src/EventLogExpert.Eventing/{Helpers/EventMethods.cs => Interop/NativeMethods.Evt.cs} (90%) diff --git a/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs b/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs index a1847f5d..00f64e4f 100644 --- a/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs @@ -568,7 +568,7 @@ public void ProviderDetails_ShouldCompressLargeData() // Compression should result in file size significantly smaller than uncompressed data // Allow for SQLite overhead but verify compression achieved at least 2x reduction - Assert.True(fileSize < UncompressedDataSize / 2, + Assert.True(fileSize < UncompressedDataSize / 2, $"Expected compressed file size to be less than {UncompressedDataSize / 2} bytes, but was {fileSize} bytes. " + $"This suggests compression may not be working effectively."); diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverCacheTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverCacheTests.cs index 4c9ece89..958f8490 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverCacheTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverCacheTests.cs @@ -114,7 +114,7 @@ public void GetOrAddDescription_ConcurrentCalls_ShouldHandleThreadSafely() { var description = $"Description{i % 10}"; var firstOccurrence = results[i]; - + for (int j = i; j < 100; j += 10) { Assert.Same(firstOccurrence, results[j]); @@ -205,7 +205,7 @@ public void GetOrAddValue_ConcurrentCalls_ShouldHandleThreadSafely() { var value = $"Value{i % 10}"; var firstOccurrence = results[i]; - + for (int j = i; j < 100; j += 10) { Assert.Same(firstOccurrence, results[j]); diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs index 41fc10dc..b5bf8e4e 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -22,7 +21,7 @@ public void ConvertVariant_WhenAnsiString_ShouldReturnString() var variant = CreateVariant(EvtVariantType.AnsiString, stringPtr); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -49,7 +48,7 @@ public void ConvertVariant_WhenBinary_ShouldReturnByteArray() var variant = CreateVariantWithCount(EvtVariantType.Binary, binaryPtr, (uint)expectedBytes.Length); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -69,7 +68,7 @@ public void ConvertVariant_WhenBooleanFalse_ShouldReturnFalse() var variant = CreateVariant(EvtVariantType.Boolean, 0u); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -84,7 +83,7 @@ public void ConvertVariant_WhenBooleanTrue_ShouldReturnTrue() var variant = CreateVariant(EvtVariantType.Boolean, 1u); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -100,7 +99,7 @@ public void ConvertVariant_WhenByte_ShouldReturnByte() var variant = CreateVariant(EvtVariantType.Byte, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -122,7 +121,7 @@ public void ConvertVariant_WhenByteArray_ShouldReturnByteArray() var variant = CreateVariantWithCount(EvtVariantType.ByteArray, arrayPtr, (uint)expectedBytes.Length); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -142,7 +141,7 @@ public void ConvertVariant_WhenByteArrayEmpty_ShouldReturnEmptyArray() var variant = CreateVariantWithCount(EvtVariantType.ByteArray, IntPtr.Zero, 0); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -164,7 +163,7 @@ public void ConvertVariant_WhenCountOverflows_ShouldThrowInvalidDataException(in var variant = CreateVariantWithCount(type, IntPtr.Zero, uint.MaxValue); // Act & Assert - var ex = Assert.Throws(() => EventMethods.ConvertVariant(variant)); + var ex = Assert.Throws(() => NativeMethods.ConvertVariant(variant)); Assert.Contains(type.ToString(), ex.Message); Assert.Contains(uint.MaxValue.ToString(), ex.Message); Assert.IsType(ex.InnerException); @@ -178,7 +177,7 @@ public void ConvertVariant_WhenDouble_ShouldReturnDouble() var variant = CreateVariant(EvtVariantType.Double, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -196,7 +195,7 @@ public void ConvertVariant_WhenFileTime_ShouldReturnDateTime() var variant = CreateVariant(EvtVariantType.FileTime, fileTime); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -218,7 +217,7 @@ public void ConvertVariant_WhenGuid_ShouldReturnGuid() var variant = CreateVariant(EvtVariantType.Guid, guidPtr); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -239,7 +238,7 @@ public void ConvertVariant_WhenHexInt32_ShouldReturnInt32() var variant = CreateVariant(EvtVariantType.HexInt32, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -265,7 +264,7 @@ public void ConvertVariant_WhenHexInt32Array_ShouldReturnInt32Array() var variant = CreateVariantWithCount(EvtVariantType.HexInt32Array, arrayPtr, (uint)expectedValues.Length); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -285,7 +284,7 @@ public void ConvertVariant_WhenHexInt32ArrayEmpty_ShouldReturnEmptyArray() var variant = CreateVariantWithCount(EvtVariantType.HexInt32Array, IntPtr.Zero, 0); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -301,7 +300,7 @@ public void ConvertVariant_WhenHexInt64_ShouldReturnUInt64() var variant = CreateVariant(EvtVariantType.HexInt64, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -317,7 +316,7 @@ public void ConvertVariant_WhenInt16_ShouldReturnShort() var variant = CreateVariant(EvtVariantType.Int16, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -333,7 +332,7 @@ public void ConvertVariant_WhenInt32_ShouldReturnInt32() var variant = CreateVariant(EvtVariantType.Int32, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -349,7 +348,7 @@ public void ConvertVariant_WhenInt64_ShouldReturnInt64() var variant = CreateVariant(EvtVariantType.Int64, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -365,7 +364,7 @@ public void ConvertVariant_WhenInvalidType_ShouldThrowInvalidDataException() // Act & Assert var exception = Assert.Throws( - () => EventMethods.ConvertVariant(variant)); + () => NativeMethods.ConvertVariant(variant)); Assert.Contains(nameof(EvtVariantType), exception.Message); Assert.Contains("9999", exception.Message); @@ -385,7 +384,7 @@ public void ConvertVariant_WhenNonZeroCountWithNullReference_ShouldThrowInvalidD var variant = CreateVariantWithCount(type, IntPtr.Zero, 5); // Act & Assert - var ex = Assert.Throws(() => EventMethods.ConvertVariant(variant)); + var ex = Assert.Throws(() => NativeMethods.ConvertVariant(variant)); Assert.Contains(type.ToString(), ex.Message); } @@ -396,7 +395,7 @@ public void ConvertVariant_WhenNull_ShouldReturnNull() var variant = CreateVariant(EvtVariantType.Null); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.Null(result); @@ -410,7 +409,7 @@ public void ConvertVariant_WhenSByte_ShouldReturnByte() var variant = CreateVariant(EvtVariantType.SByte, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -425,7 +424,7 @@ public void ConvertVariant_WhenSidNull_ShouldReturnNull() var variant = CreateVariant(EvtVariantType.Sid, IntPtr.Zero); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.Null(result); @@ -439,7 +438,7 @@ public void ConvertVariant_WhenSingle_ShouldReturnFloat() var variant = CreateVariant(EvtVariantType.Single, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -455,7 +454,7 @@ public void ConvertVariant_WhenSizeT_ShouldReturnIntPtr() var variant = CreateVariant(EvtVariantType.SizeT, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -475,7 +474,7 @@ public void ConvertVariant_WhenString_ShouldReturnString() var variant = CreateVariant(EvtVariantType.String, stringPtr); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -507,7 +506,7 @@ public void ConvertVariant_WhenStringArray_ShouldReturnStringArray() var variant = CreateVariantWithCount(EvtVariantType.StringArray, arrayPtr, (uint)expectedStrings.Length); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -535,7 +534,7 @@ public void ConvertVariant_WhenStringArrayEmpty_ShouldReturnEmptyArray() var variant = CreateVariantWithCount(EvtVariantType.StringArray, IntPtr.Zero, 0); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -568,7 +567,7 @@ public void ConvertVariant_WhenSysTime_ShouldReturnDateTime() var variant = CreateVariant(EvtVariantType.SysTime, sysTimePtr); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -589,7 +588,7 @@ public void ConvertVariant_WhenUInt16_ShouldReturnUInt16() var variant = CreateVariant(EvtVariantType.UInt16, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -615,7 +614,7 @@ public void ConvertVariant_WhenUInt16Array_ShouldReturnUInt16Array() var variant = CreateVariantWithCount(EvtVariantType.UInt16Array, arrayPtr, (uint)expectedValues.Length); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -635,7 +634,7 @@ public void ConvertVariant_WhenUInt16ArrayEmpty_ShouldReturnEmptyArray() var variant = CreateVariantWithCount(EvtVariantType.UInt16Array, IntPtr.Zero, 0); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -651,7 +650,7 @@ public void ConvertVariant_WhenUInt32_ShouldReturnUInt32() var variant = CreateVariant(EvtVariantType.UInt32, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -677,7 +676,7 @@ public void ConvertVariant_WhenUInt32Array_ShouldReturnUInt32Array() var variant = CreateVariantWithCount(EvtVariantType.UInt32Array, arrayPtr, (uint)expectedValues.Length); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -697,7 +696,7 @@ public void ConvertVariant_WhenUInt32ArrayEmpty_ShouldReturnEmptyArray() var variant = CreateVariantWithCount(EvtVariantType.UInt32Array, IntPtr.Zero, 0); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -713,7 +712,7 @@ public void ConvertVariant_WhenUInt64_ShouldReturnUInt64() var variant = CreateVariant(EvtVariantType.UInt64, expectedValue); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -733,7 +732,7 @@ public void ConvertVariant_WhenXml_ShouldReturnString() var variant = CreateVariant(EvtVariantType.Xml, xmlPtr); // Act - var result = EventMethods.ConvertVariant(variant); + var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); @@ -753,7 +752,7 @@ public void ThrowEventLogException_WhenAccessDenied_ShouldThrowUnauthorizedAcces int error = Win32ErrorCodes.ERROR_ACCESS_DENIED; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -763,7 +762,7 @@ public void ThrowEventLogException_WhenCancelled_ShouldThrowOperationCanceledExc int error = Win32ErrorCodes.ERROR_CANCELLED; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -773,7 +772,7 @@ public void ThrowEventLogException_WhenChannelNotFound_ShouldThrowFileNotFoundEx int error = Win32ErrorCodes.ERROR_EVT_CHANNEL_NOT_FOUND; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -783,7 +782,7 @@ public void ThrowEventLogException_WhenFileNotFound_ShouldThrowFileNotFoundExcep int error = Win32ErrorCodes.ERROR_FILE_NOT_FOUND; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -793,7 +792,7 @@ public void ThrowEventLogException_WhenInvalidData_ShouldThrowInvalidDataExcepti int error = Win32ErrorCodes.ERROR_INVALID_DATA; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -803,7 +802,7 @@ public void ThrowEventLogException_WhenInvalidEventData_ShouldThrowInvalidDataEx int error = Win32ErrorCodes.ERROR_EVT_INVALID_EVENT_DATA; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -813,7 +812,7 @@ public void ThrowEventLogException_WhenInvalidHandle_ShouldThrowUnauthorizedAcce int error = Win32ErrorCodes.ERROR_INVALID_HANDLE; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -823,7 +822,7 @@ public void ThrowEventLogException_WhenMessageIdNotFound_ShouldThrowFileNotFound int error = Win32ErrorCodes.ERROR_EVT_MESSAGE_ID_NOT_FOUND; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -833,7 +832,7 @@ public void ThrowEventLogException_WhenMessageNotFound_ShouldThrowFileNotFoundEx int error = Win32ErrorCodes.ERROR_EVT_MESSAGE_NOT_FOUND; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -843,7 +842,7 @@ public void ThrowEventLogException_WhenPathNotFound_ShouldThrowFileNotFoundExcep int error = Win32ErrorCodes.ERROR_PATH_NOT_FOUND; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -853,7 +852,7 @@ public void ThrowEventLogException_WhenPublisherMetadataNotFound_ShouldThrowFile int error = Win32ErrorCodes.ERROR_EVT_PUBLISHER_METADATA_NOT_FOUND; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -863,7 +862,7 @@ public void ThrowEventLogException_WhenRpcCallCanceled_ShouldThrowOperationCance int error = Win32ErrorCodes.RPC_S_CALL_CANCELED; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] @@ -873,7 +872,7 @@ public void ThrowEventLogException_WhenUnknownError_ShouldThrowException() int error = 9999; // Unknown error code // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } // Helper methods to create EvtVariant instances diff --git a/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs index ec980a2f..addf7241 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs @@ -356,7 +356,7 @@ private static bool TryFindChannelWithDistinctOwningPublisher(out string? channe continue; } - using var channelConfig = EventMethods.EvtOpenChannelConfig( + using var channelConfig = NativeMethods.EvtOpenChannelConfig( EventLogSession.GlobalSession.Handle, candidate, 0); @@ -432,7 +432,7 @@ private static bool TryReadOwningPublisher(EvtHandle channelConfig, out string? { publisher = null; - bool success = EventMethods.EvtGetChannelConfigProperty( + bool success = NativeMethods.EvtGetChannelConfigProperty( channelConfig, EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, 0, @@ -451,7 +451,7 @@ private static bool TryReadOwningPublisher(EvtHandle channelConfig, out string? try { - success = EventMethods.EvtGetChannelConfigProperty( + success = NativeMethods.EvtGetChannelConfigProperty( channelConfig, EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, 0, @@ -465,7 +465,7 @@ private static bool TryReadOwningPublisher(EvtHandle channelConfig, out string? } var variant = Marshal.PtrToStructure(buffer); - publisher = EventMethods.ConvertVariant(variant) as string; + publisher = NativeMethods.ConvertVariant(variant) as string; return !string.IsNullOrEmpty(publisher); } diff --git a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs index 1c165d46..011cc3a3 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs @@ -40,7 +40,7 @@ public void GetMessageFilesForLegacyProvider_WhenCalled_ShouldNotIncludeSysFiles // Act var allResults = new List(); var commonProviders = new[] { Constants.ApplicationLogName, Constants.SystemLogName }; - + foreach (var providerName in commonProviders) { allResults.AddRange(provider.GetMessageFilesForLegacyProvider(providerName)); @@ -296,9 +296,9 @@ public void GetMessageFilesForLegacyProvider_WhenProviderFound_ShouldLogFoundMes foreach (var providerName in commonProviders) { var result = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); - + if (result.Count == 0) { continue; } - + // Found a provider, verify logging mockLogger.Received() .Debug(Arg.Is(h => h.ToString().Contains("Found message file for legacy provider"))); diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs index 6b7e7e80..7f8c5fbd 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs @@ -54,7 +54,7 @@ public void Constructor_WhenCalledMultipleTimes_ShouldCreateIndependentInstances Assert.NotNull(logInfo1); Assert.NotNull(logInfo2); Assert.NotSame(logInfo1, logInfo2); - + // Different logs should have potentially different record counts // (though we can't guarantee they're different) Assert.NotNull(logInfo1.RecordCount); @@ -345,7 +345,7 @@ public void GetLogInformation_WhenCalledTwice_ShouldReturnSimilarData() // File size might change slightly, but should be close Assert.NotNull(logInfo1.FileSize); Assert.NotNull(logInfo2.FileSize); - + // Attributes should be identical Assert.Equal(logInfo1.Attributes, logInfo2.Attributes); } @@ -450,10 +450,10 @@ public void Properties_WhenApplicationLog_ShouldBeReadOnly() Assert.True(attributesProperty.CanRead); Assert.False(attributesProperty.CanWrite); - + Assert.True(fileSizeProperty.CanRead); Assert.False(fileSizeProperty.CanWrite); - + Assert.True(recordCountProperty.CanRead); Assert.False(recordCountProperty.CanWrite); } @@ -469,7 +469,7 @@ public void RecordCount_WhenApplicationLog_ShouldBeConsistentWithOldestRecord() // Assert Assert.NotNull(logInfo.RecordCount); - + if (logInfo is { OldestRecordNumber: not null, RecordCount: > 0 }) { // Oldest record number should be reasonable @@ -504,7 +504,7 @@ public void RecordCount_WhenLogHasRecords_ShouldMatchWithOldestRecordNumber() // Assert Assert.NotNull(logInfo.RecordCount); - + if (logInfo.RecordCount.Value == 0) { // If no records, oldest record number might be null or zero diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs index 48fa8b18..fc71d29b 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs @@ -482,7 +482,7 @@ public void TryGetEvents_WhenEventHasError_ShouldSetErrorProperty() // Assert Assert.True(success); - + // Some events might have errors (e.g., corrupt events, missing provider info) // Just verify that if Error is set, it's a non-empty string foreach (var evt in events) @@ -594,7 +594,7 @@ public void TryGetEvents_WhenMultipleReaders_ShouldReadIndependently() // Assert Assert.True(success1); Assert.True(success2); - + // Both readers should get events from the same log if (events1.Length > 0 && events2.Length > 0) { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs index 7b2204fb..29649aa0 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs @@ -217,7 +217,7 @@ public void GetLogNames_WhenCalled_ShouldReturnOrderedList() // Assert Assert.NotEmpty(logNames); - + // Verify the list is sorted var sortedNames = logNames.OrderBy(x => x).ToList(); Assert.Equal(sortedNames, logNames); @@ -235,7 +235,7 @@ public void GetLogNames_WhenCalledMultipleTimes_ShouldReturnConsistentResults() // Assert Assert.Equal(logNames1.Count, logNames2.Count); - + // All names from first call should be in second call foreach (var name in logNames1) { @@ -383,7 +383,7 @@ public void GetProviderNames_WhenCalledMultipleTimes_ShouldReturnConsistentResul // Assert Assert.Equal(providers1.Count, providers2.Count); - + // All providers from first call should be in second call foreach (var provider in providers1) { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs index de0bc0cc..5b5ab15b 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs @@ -111,7 +111,7 @@ public void Constructor_WithValidBookmark_ShouldCreateWatcher() using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); reader.TryGetEvents(out _, 1); - + var bookmark = reader.LastBookmark; // Act @@ -140,7 +140,7 @@ public void Dispose_AfterDispose_ShouldNotReceiveEvents() // Act watcher.Dispose(); - + using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event after dispose", EventLogEntryType.Information); @@ -440,7 +440,7 @@ public void EventRecordWritten_ShouldHaveValidTimeCreated() // Act var beforeWrite = DateTime.UtcNow; - + using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event for timestamp", EventLogEntryType.Information); @@ -579,7 +579,7 @@ public void EventRecordWritten_ShouldReceiveEventsInOrder() // Assert Assert.True(received, "Did not receive all events within timeout period"); Assert.True(receivedEvents.Count >= 3); - + // Verify events are in chronological order (by TimeCreated) for (int i = 1; i < Math.Min(3, receivedEvents.Count); i++) { @@ -942,7 +942,7 @@ public void Multiple_Watchers_OnSameLog_ShouldAllReceiveEvents() // Arrange using var watcher1 = new EventLogWatcher(Constants.ApplicationLogName); using var watcher2 = new EventLogWatcher(Constants.ApplicationLogName); - + var receivedEvents1 = new List(); var receivedEvents2 = new List(); var countdown = new CountdownEvent(2); @@ -1006,7 +1006,7 @@ public void Unsubscribe_MultipleTimes_ShouldNotThrow() watcher.Enabled = false; watcher.Enabled = false; watcher.Enabled = false; - + Assert.False(watcher.Enabled); } diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs b/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs index e8962d35..831011ee 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs +++ b/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs @@ -297,7 +297,7 @@ private static bool TryDetectPrimaryKeyNoCaseCollation(DbConnection connection) using (var indexListCommand = connection.CreateCommand()) { indexListCommand.CommandText = "PRAGMA index_list(\"ProviderDetails\")"; - + using var indexListReader = indexListCommand.ExecuteReader(); while (indexListReader.Read()) diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs index f9fc6f8a..c651fd99 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs @@ -95,7 +95,7 @@ public ValueTask GetXmlAsync(DisplayEventModel evt, CancellationToken ca /// Performs the actual EvtQuery / EvtNext / RenderEventXml work. Virtual for testability. protected virtual string ResolveXml(string owningLog, long recordId, PathType pathType) { - using EvtHandle handle = EventMethods.EvtQuery( + using EvtHandle handle = NativeMethods.EvtQuery( EventLogSession.GlobalSession.Handle, owningLog, $"*[System[EventRecordID='{recordId}']]", @@ -106,7 +106,7 @@ protected virtual string ResolveXml(string owningLog, long recordId, PathType pa var buffer = new IntPtr[1]; int count = 0; - bool success = EventMethods.EvtNext(handle, buffer.Length, buffer, 0, 0, ref count); + bool success = NativeMethods.EvtNext(handle, buffer.Length, buffer, 0, 0, ref count); if (!success || count == 0) { return string.Empty; } @@ -114,7 +114,7 @@ protected virtual string ResolveXml(string owningLog, long recordId, PathType pa if (eventHandle.IsInvalid) { return string.Empty; } - return EventMethods.RenderEventXml(eventHandle) ?? string.Empty; + return NativeMethods.RenderEventXml(eventHandle) ?? string.Empty; } private static async ValueTask AwaitLazyAsync(Lazy> lazy, CancellationToken cancellationToken) diff --git a/src/EventLogExpert.Eventing/Helpers/PathType.cs b/src/EventLogExpert.Eventing/Helpers/PathType.cs new file mode 100644 index 00000000..ec612e05 --- /dev/null +++ b/src/EventLogExpert.Eventing/Helpers/PathType.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.Eventing.Helpers; + +public enum PathType +{ + LogName = 1, + FilePath +} diff --git a/src/EventLogExpert.Eventing/Helpers/TraceLogger.cs b/src/EventLogExpert.Eventing/Helpers/TraceLogger.cs index 13f0ed47..d4cb868e 100644 --- a/src/EventLogExpert.Eventing/Helpers/TraceLogger.cs +++ b/src/EventLogExpert.Eventing/Helpers/TraceLogger.cs @@ -24,13 +24,13 @@ public class TraceLogger(LogLevel loggingLevel) : ITraceLogger private static void Write(bool isEnabled, string message, LogLevel level) { if (!isEnabled) { return; } - + switch (level) { case LogLevel.Trace: case LogLevel.Debug: Console.ForegroundColor = ConsoleColor.Cyan; - + Console.WriteLine($"[{level}] {message}"); break; case LogLevel.Information: @@ -49,7 +49,7 @@ private static void Write(bool isEnabled, string message, LogLevel level) break; } - + Console.ResetColor(); } diff --git a/src/EventLogExpert.Eventing/Interop/EvtEnums.cs b/src/EventLogExpert.Eventing/Interop/EvtEnums.cs new file mode 100644 index 00000000..2786e458 --- /dev/null +++ b/src/EventLogExpert.Eventing/Interop/EvtEnums.cs @@ -0,0 +1,182 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.Eventing.Interop; + +internal enum EvtEventMetadataPropertyId +{ + ID, + Version, + Channel, + Level, + Opcode, + Task, + Keyword, + MessageID, + Template +} + +internal enum EvtFormatMessageFlags +{ + Event = 1, + Level, + Task, + Opcode, + Keyword, + Channel, + Provider, + Id, + Xml +} + +internal enum EvtLogPropertyId +{ + CreationTime, + LastAccessTime, + LastWriteTime, + FileSize, + Attributes, + NumberOfLogRecords, + OldestRecordNumber, + Full, +} + +internal enum EvtPublisherMetadataPropertyId +{ + PublisherGuid, + ResourceFilePath, + ParameterFilePath, + MessageFilePath, + HelpLink, + PublisherMessageID, + ChannelReferences, + ChannelReferencePath, + ChannelReferenceIndex, + ChannelReferenceID, + ChannelReferenceFlags, + ChannelReferenceMessageID, + Levels, + LevelName, + LevelValue, + LevelMessageID, + Tasks, + TaskName, + TaskEventGuid, + TaskValue, + TaskMessageID, + Opcodes, + OpcodeName, + OpcodeValue, + OpcodeMessageID, + Keywords, + KeywordName, + KeywordValue, + KeywordMessageID +} + +internal enum EvtRenderContextFlags +{ + Values, + System, + User +} + +/// Defines the values that specify what to render +internal enum EvtRenderFlags +{ + /// Render the properties specified in the rendering context + EventValues, + /// Render the event as an XML string + EventXml, + /// Render the bookmark as an XML string, so that you can easily persist the bookmark for use later + Bookmark +} + +[Flags] +internal enum EvtSubscribeFlags +{ + ToFutureEvents = 1, + StartAtOldestRecord = 2, + StartAfterBookmark = 3, +#pragma warning disable CA1069 // Enums values should not be duplicated + OriginMask = 3, +#pragma warning restore CA1069 // Enums values should not be duplicated + TolerateQueryErrors = 0x1000, + Strict = 0x10000 +} + +internal enum EvtSystemPropertyId +{ + ProviderName, + ProviderGuid, + EventId, + Qualifiers, + Level, + Task, + Opcode, + Keywords, + TimeCreated, + EventRecordId, + ActivityId, + RelatedActivityID, + ProcessID, + ThreadID, + Channel, + Computer, + UserID, + Version +} + +internal enum EvtChannelConfigPropertyId +{ + EvtChannelConfigEnabled = 0, + EvtChannelConfigIsolation = 1, + EvtChannelConfigType = 2, + EvtChannelConfigOwningPublisher = 3, +} + +internal enum EvtVariantType +{ + Null, + String, + AnsiString, + SByte, + Byte, + Int16, + UInt16, + Int32, + UInt32, + Int64, + UInt64, + Single, + Double, + Boolean, + Binary, + Guid, + SizeT, + FileTime, + SysTime, + Sid, + HexInt32, + HexInt64, + Handle, + Xml, + + // Array types (base type | EVT_VARIANT_TYPE_ARRAY) + StringArray = 129, + ByteArray = 132, + UInt16Array = 134, + UInt32Array = 136, + HexInt32Array = 148 +} + +[Flags] +internal enum SeekFlags +{ + RelativeToFirst = 1, + RelativeToLast = 2, + RelativeToCurrent = 3, + RelativeToBookmark = 4, + OriginMask = 7, + Strict = 65536 +} diff --git a/src/EventLogExpert.Eventing/Interop/EvtHandle.cs b/src/EventLogExpert.Eventing/Interop/EvtHandle.cs index 8687d8f0..d7bb49de 100644 --- a/src/EventLogExpert.Eventing/Interop/EvtHandle.cs +++ b/src/EventLogExpert.Eventing/Interop/EvtHandle.cs @@ -1,7 +1,6 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using Microsoft.Win32.SafeHandles; namespace EventLogExpert.Eventing.Interop; @@ -25,7 +24,7 @@ internal EvtHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) protected override bool ReleaseHandle() { - EventMethods.EvtClose(handle); + NativeMethods.EvtClose(handle); handle = IntPtr.Zero; return true; diff --git a/src/EventLogExpert.Eventing/Interop/LoadLibraryFlags.cs b/src/EventLogExpert.Eventing/Interop/LoadLibraryFlags.cs new file mode 100644 index 00000000..03b74a8d --- /dev/null +++ b/src/EventLogExpert.Eventing/Interop/LoadLibraryFlags.cs @@ -0,0 +1,25 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +// ReSharper disable InconsistentNaming +// We are defining some win32 types in this file, so we +// are not following the usual C# naming conventions. + +namespace EventLogExpert.Eventing.Interop; + +[Flags] +internal enum LoadLibraryFlags : uint +{ + None = 0, + DONT_RESOLVE_DLL_REFERENCES = 0x00000001, + LOAD_IGNORE_CODE_AUTHZ_LEVEL = 0x00000010, + LOAD_LIBRARY_AS_DATAFILE = 0x00000002, + LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040, + LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020, + LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200, + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100, + LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800, + LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400, + LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008 +} diff --git a/src/EventLogExpert.Eventing/Helpers/EventMethods.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs similarity index 90% rename from src/EventLogExpert.Eventing/Helpers/EventMethods.cs rename to src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs index 7b9ea236..1d2fff3f 100644 --- a/src/EventLogExpert.Eventing/Helpers/EventMethods.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Interop; +using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using Microsoft.Win32.SafeHandles; @@ -9,193 +9,9 @@ using System.Runtime.InteropServices; using System.Security.Principal; -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Interop; -public enum PathType -{ - LogName = 1, - FilePath -} - -internal enum EvtEventMetadataPropertyId -{ - ID, - Version, - Channel, - Level, - Opcode, - Task, - Keyword, - MessageID, - Template -} - -internal enum EvtFormatMessageFlags -{ - Event = 1, - Level, - Task, - Opcode, - Keyword, - Channel, - Provider, - Id, - Xml -} - -internal enum EvtLogPropertyId -{ - CreationTime, - LastAccessTime, - LastWriteTime, - FileSize, - Attributes, - NumberOfLogRecords, - OldestRecordNumber, - Full, -} - -internal enum EvtPublisherMetadataPropertyId -{ - PublisherGuid, - ResourceFilePath, - ParameterFilePath, - MessageFilePath, - HelpLink, - PublisherMessageID, - ChannelReferences, - ChannelReferencePath, - ChannelReferenceIndex, - ChannelReferenceID, - ChannelReferenceFlags, - ChannelReferenceMessageID, - Levels, - LevelName, - LevelValue, - LevelMessageID, - Tasks, - TaskName, - TaskEventGuid, - TaskValue, - TaskMessageID, - Opcodes, - OpcodeName, - OpcodeValue, - OpcodeMessageID, - Keywords, - KeywordName, - KeywordValue, - KeywordMessageID -} - -internal enum EvtRenderContextFlags -{ - Values, - System, - User -} - -/// Defines the values that specify what to render -internal enum EvtRenderFlags -{ - /// Render the properties specified in the rendering context - EventValues, - /// Render the event as an XML string - EventXml, - /// Render the bookmark as an XML string, so that you can easily persist the bookmark for use later - Bookmark -} - -[Flags] -internal enum EvtSubscribeFlags -{ - ToFutureEvents = 1, - StartAtOldestRecord = 2, - StartAfterBookmark = 3, -#pragma warning disable CA1069 // Enums values should not be duplicated - OriginMask = 3, -#pragma warning restore CA1069 // Enums values should not be duplicated - TolerateQueryErrors = 0x1000, - Strict = 0x10000 -} - -internal enum EvtSystemPropertyId -{ - ProviderName, - ProviderGuid, - EventId, - Qualifiers, - Level, - Task, - Opcode, - Keywords, - TimeCreated, - EventRecordId, - ActivityId, - RelatedActivityID, - ProcessID, - ThreadID, - Channel, - Computer, - UserID, - Version -} - -internal enum EvtChannelConfigPropertyId -{ - EvtChannelConfigEnabled = 0, - EvtChannelConfigIsolation = 1, - EvtChannelConfigType = 2, - EvtChannelConfigOwningPublisher = 3, -} - -internal enum EvtVariantType -{ - Null, - String, - AnsiString, - SByte, - Byte, - Int16, - UInt16, - Int32, - UInt32, - Int64, - UInt64, - Single, - Double, - Boolean, - Binary, - Guid, - SizeT, - FileTime, - SysTime, - Sid, - HexInt32, - HexInt64, - Handle, - Xml, - - // Array types (base type | EVT_VARIANT_TYPE_ARRAY) - StringArray = 129, - ByteArray = 132, - UInt16Array = 134, - UInt32Array = 136, - HexInt32Array = 148 -} - -[Flags] -internal enum SeekFlags -{ - RelativeToFirst = 1, - RelativeToLast = 2, - RelativeToCurrent = 3, - RelativeToBookmark = 4, - OriginMask = 7, - Strict = 65536 -} - -internal static partial class EventMethods +internal static partial class NativeMethods { private const string EventLogApi = "wevtapi.dll"; private const int MaxStackAllocChars = 4096; diff --git a/src/EventLogExpert.Eventing/Interop/NativeMethods.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.cs index caddfe6c..7fdb924a 100644 --- a/src/EventLogExpert.Eventing/Interop/NativeMethods.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.cs @@ -9,23 +9,6 @@ namespace EventLogExpert.Eventing.Interop; -[Flags] -internal enum LoadLibraryFlags : uint -{ - None = 0, - DONT_RESOLVE_DLL_REFERENCES = 0x00000001, - LOAD_IGNORE_CODE_AUTHZ_LEVEL = 0x00000010, - LOAD_LIBRARY_AS_DATAFILE = 0x00000002, - LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040, - LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020, - LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200, - LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000, - LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100, - LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800, - LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400, - LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008 -} - internal static partial class NativeMethods { internal const int RT_MESSAGETABLE = 11; diff --git a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs index 63e448dd..2a359c2a 100644 --- a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs @@ -445,7 +445,7 @@ private bool TryGetChannelOwningPublisher(string channelName, out string? publis { publisher = null; - using var channelConfig = EventMethods.EvtOpenChannelConfig( + using var channelConfig = NativeMethods.EvtOpenChannelConfig( EventLogSession.GlobalSession.Handle, channelName, 0); @@ -464,7 +464,7 @@ private bool TryGetChannelOwningPublisher(string channelName, out string? publis try { - bool success = EventMethods.EvtGetChannelConfigProperty( + bool success = NativeMethods.EvtGetChannelConfigProperty( channelConfig, EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, 0, @@ -484,7 +484,7 @@ private bool TryGetChannelOwningPublisher(string channelName, out string? publis buffer = Marshal.AllocHGlobal(bufferSize); - success = EventMethods.EvtGetChannelConfigProperty( + success = NativeMethods.EvtGetChannelConfigProperty( channelConfig, EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, 0, @@ -503,7 +503,7 @@ private bool TryGetChannelOwningPublisher(string channelName, out string? publis } var variant = Marshal.PtrToStructure(buffer); - var value = EventMethods.ConvertVariant(variant) as string; + var value = NativeMethods.ConvertVariant(variant) as string; if (string.IsNullOrWhiteSpace(value)) { diff --git a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs index 825d2536..c82a82b3 100644 --- a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs +++ b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs @@ -28,7 +28,7 @@ internal sealed partial class ProviderMetadata private ProviderMetadata(string providerName, string? metadataPath = null) { - _publisherMetadataHandle = EventMethods.EvtOpenPublisherMetadata(EventLogSession.GlobalSession.Handle, providerName, metadataPath, 0, 0); + _publisherMetadataHandle = NativeMethods.EvtOpenPublisherMetadata(EventLogSession.GlobalSession.Handle, providerName, metadataPath, 0, 0); int error = Marshal.GetLastWin32Error(); if (_publisherMetadataHandle.IsInvalid) @@ -50,18 +50,18 @@ public IDictionary Channels using EvtHandle channelRefHandle = GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropertyId.ChannelReferences); - int size = EventMethods.GetObjectArraySize(channelRefHandle); + int size = NativeMethods.GetObjectArraySize(channelRefHandle); Dictionary channels = new(size); for (int i = 0; i < size; i++) { - uint channelId = (uint)EventMethods.GetObjectArrayProperty( + uint channelId = (uint)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.ChannelReferenceID); - string channelName = (string)EventMethods.GetObjectArrayProperty( + string channelName = (string)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.ChannelReferencePath); @@ -86,7 +86,7 @@ public IEnumerable Events { List events = []; - using EvtHandle handle = EventMethods.EvtOpenEventMetadataEnum(_publisherMetadataHandle, 0); + using EvtHandle handle = NativeMethods.EvtOpenEventMetadataEnum(_publisherMetadataHandle, 0); int error = Marshal.GetLastWin32Error(); if (handle.IsInvalid) @@ -114,7 +114,7 @@ public IEnumerable Events string message = messageId == -1 ? string.Empty : - EventMethods.FormatMessage(_publisherMetadataHandle, (uint)messageId); + NativeMethods.FormatMessage(_publisherMetadataHandle, (uint)messageId); events.Add(new EventMetadata(id, version, channelId, level, opcode, task, keywords, template, message, this)); } @@ -136,30 +136,30 @@ public IDictionary Keywords using EvtHandle channelRefHandle = GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropertyId.Keywords); - int size = EventMethods.GetObjectArraySize(channelRefHandle); + int size = NativeMethods.GetObjectArraySize(channelRefHandle); Dictionary keywords = new(size); for (int i = 0; i < size; i++) { - string name = (string)EventMethods.GetObjectArrayProperty( + string name = (string)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.KeywordName); - long value = (long)(ulong)EventMethods.GetObjectArrayProperty( + long value = (long)(ulong)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.KeywordValue); - int messageId = (int)(uint)EventMethods.GetObjectArrayProperty( + int messageId = (int)(uint)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.KeywordMessageID); string displayName = messageId == -1 ? name : - EventMethods.FormatMessage(_publisherMetadataHandle, (uint)messageId); + NativeMethods.FormatMessage(_publisherMetadataHandle, (uint)messageId); keywords.TryAdd(value, displayName); } @@ -190,30 +190,30 @@ public IDictionary Opcodes using EvtHandle channelRefHandle = GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropertyId.Opcodes); - int size = EventMethods.GetObjectArraySize(channelRefHandle); + int size = NativeMethods.GetObjectArraySize(channelRefHandle); Dictionary opcodes = new(size); for (int i = 0; i < size; i++) { - string name = (string)EventMethods.GetObjectArrayProperty( + string name = (string)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.OpcodeName); - uint value = (uint)EventMethods.GetObjectArrayProperty( + uint value = (uint)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.OpcodeValue); - int messageId = (int)(uint)EventMethods.GetObjectArrayProperty( + int messageId = (int)(uint)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.OpcodeMessageID); string displayName = messageId == -1 ? name : - EventMethods.FormatMessage(_publisherMetadataHandle, (uint)messageId); + NativeMethods.FormatMessage(_publisherMetadataHandle, (uint)messageId); opcodes.TryAdd((int)(value >> 16), displayName); } @@ -244,30 +244,30 @@ public IDictionary Tasks using EvtHandle channelRefHandle = GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropertyId.Tasks); - int size = EventMethods.GetObjectArraySize(channelRefHandle); + int size = NativeMethods.GetObjectArraySize(channelRefHandle); Dictionary tasks = new(size); for (int i = 0; i < size; i++) { - string name = (string)EventMethods.GetObjectArrayProperty( + string name = (string)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.TaskName); - int value = (int)(uint)EventMethods.GetObjectArrayProperty( + int value = (int)(uint)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.TaskValue); - int messageId = (int)(uint)EventMethods.GetObjectArrayProperty( + int messageId = (int)(uint)NativeMethods.GetObjectArrayProperty( channelRefHandle, i, EvtPublisherMetadataPropertyId.TaskMessageID); string displayName = messageId == -1 ? name : - EventMethods.FormatMessage(_publisherMetadataHandle, (uint)messageId); + NativeMethods.FormatMessage(_publisherMetadataHandle, (uint)messageId); tasks.TryAdd(value, displayName); } @@ -334,27 +334,27 @@ private static object GetEventMetadataProperty(EvtHandle metadataHandle, EvtEven try { - bool success = EventMethods.EvtGetEventMetadataProperty(metadataHandle, propertyId, 0, 0, IntPtr.Zero, out int bufferSize); + bool success = NativeMethods.EvtGetEventMetadataProperty(metadataHandle, propertyId, 0, 0, IntPtr.Zero, out int bufferSize); int error = Marshal.GetLastWin32Error(); if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } buffer = Marshal.AllocHGlobal(bufferSize); - success = EventMethods.EvtGetEventMetadataProperty(metadataHandle, propertyId, 0, bufferSize, buffer, out bufferSize); + success = NativeMethods.EvtGetEventMetadataProperty(metadataHandle, propertyId, 0, bufferSize, buffer, out bufferSize); error = Marshal.GetLastWin32Error(); if (!success) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } var variant = Marshal.PtrToStructure(buffer); - return EventMethods.ConvertVariant(variant) ?? + return NativeMethods.ConvertVariant(variant) ?? throw new InvalidDataException($"Invalid Metadata for PropertyId: {propertyId}"); } finally @@ -365,14 +365,14 @@ private static object GetEventMetadataProperty(EvtHandle metadataHandle, EvtEven private static EvtHandle? NextEventMetadata(EvtHandle metadataHandle, int flags) { - EvtHandle handle = EventMethods.EvtNextEventMetadata(metadataHandle, flags); + EvtHandle handle = NativeMethods.EvtNextEventMetadata(metadataHandle, flags); int error = Marshal.GetLastWin32Error(); if (!handle.IsInvalid) { return handle; } if (error != Win32ErrorCodes.ERROR_NO_MORE_ITEMS) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } return null; @@ -384,7 +384,7 @@ private string GetPublisherMetadataProperty(EvtPublisherMetadataPropertyId prope try { - bool success = EventMethods.EvtGetPublisherMetadataProperty( + bool success = NativeMethods.EvtGetPublisherMetadataProperty( _publisherMetadataHandle, propertyId, 0, @@ -396,12 +396,12 @@ private string GetPublisherMetadataProperty(EvtPublisherMetadataPropertyId prope if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } buffer = Marshal.AllocHGlobal(bufferUsed); - success = EventMethods.EvtGetPublisherMetadataProperty( + success = NativeMethods.EvtGetPublisherMetadataProperty( _publisherMetadataHandle, propertyId, 0, @@ -413,12 +413,12 @@ private string GetPublisherMetadataProperty(EvtPublisherMetadataPropertyId prope if (!success) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } var variant = Marshal.PtrToStructure(buffer); - return (string?)EventMethods.ConvertVariant(variant) ?? string.Empty; + return (string?)NativeMethods.ConvertVariant(variant) ?? string.Empty; } finally { @@ -432,7 +432,7 @@ private EvtHandle GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropert try { - bool success = EventMethods.EvtGetPublisherMetadataProperty( + bool success = NativeMethods.EvtGetPublisherMetadataProperty( _publisherMetadataHandle, propertyId, 0, @@ -444,12 +444,12 @@ private EvtHandle GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropert if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } buffer = Marshal.AllocHGlobal(bufferUsed); - success = EventMethods.EvtGetPublisherMetadataProperty( + success = NativeMethods.EvtGetPublisherMetadataProperty( _publisherMetadataHandle, propertyId, 0, @@ -461,7 +461,7 @@ private EvtHandle GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropert if (!success) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } var variant = Marshal.PtrToStructure(buffer); diff --git a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs index bebc81e4..f654f315 100644 --- a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs @@ -92,5 +92,5 @@ public IEnumerable GetMessageFilesForLegacyProvider(string providerName) return []; } - private class OpenEventLogRegistryKeyFailedException(string msg) : Exception(msg) {} + private class OpenEventLogRegistryKeyFailedException(string msg) : Exception(msg) { /* marker exception — no extra state needed */ } } diff --git a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs index bec1b6d9..97d5f21f 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs @@ -11,7 +11,7 @@ public sealed class EventLogInformation { internal EventLogInformation(EventLogSession session, string logName, PathType pathType) { - using EvtHandle handle = EventMethods.EvtOpenLog(session.Handle, logName, pathType); + using EvtHandle handle = NativeMethods.EvtOpenLog(session.Handle, logName, pathType); Attributes = (int?)(uint?)GetLogInfo(handle, EvtLogPropertyId.Attributes); CreationTime = (DateTime?)GetLogInfo(handle, EvtLogPropertyId.CreationTime); @@ -45,27 +45,27 @@ internal EventLogInformation(EventLogSession session, string logName, PathType p try { - bool success = EventMethods.EvtGetLogInfo(handle, property, 0, IntPtr.Zero, out int bufferSize); + bool success = NativeMethods.EvtGetLogInfo(handle, property, 0, IntPtr.Zero, out int bufferSize); int error = Marshal.GetLastWin32Error(); if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } buffer = Marshal.AllocHGlobal(bufferSize); - success = EventMethods.EvtGetLogInfo(handle, property, bufferSize, buffer, out bufferSize); + success = NativeMethods.EvtGetLogInfo(handle, property, bufferSize, buffer, out bufferSize); error = Marshal.GetLastWin32Error(); if (!success) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } var variant = Marshal.PtrToStructure(buffer); - return EventMethods.ConvertVariant(variant); + return NativeMethods.ConvertVariant(variant); } finally { diff --git a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs index 9ae557bd..a6e6a0d0 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs @@ -13,7 +13,7 @@ public sealed partial class EventLogReader(string path, PathType pathType, bool { private readonly Lock _eventLock = new(); private readonly EvtHandle _handle = - EventMethods.EvtQuery(EventLogSession.GlobalSession.Handle, path, null, pathType); + NativeMethods.EvtQuery(EventLogSession.GlobalSession.Handle, path, null, pathType); private int _disposed; @@ -62,7 +62,7 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) try { - bool success = EventMethods.EvtNext(_handle, batchSize, buffer, 0, 0, ref count); + bool success = NativeMethods.EvtNext(_handle, batchSize, buffer, 0, 0, ref count); if (!success) { @@ -87,12 +87,12 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) try { - events[i] = EventMethods.RenderEvent(eventHandle); - events[i].Properties = EventMethods.RenderEventProperties(eventHandle); + events[i] = NativeMethods.RenderEvent(eventHandle); + events[i].Properties = NativeMethods.RenderEventProperties(eventHandle); if (renderXml) { - events[i].Xml = EventMethods.RenderEventXml(eventHandle); + events[i].Xml = NativeMethods.RenderEventXml(eventHandle); } } catch (Exception ex) @@ -116,23 +116,23 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) private static string? CreateBookmark(EvtHandle eventHandle) { - using EvtHandle handle = EventMethods.EvtCreateBookmark(null); + using EvtHandle handle = NativeMethods.EvtCreateBookmark(null); int error = Marshal.GetLastWin32Error(); if (handle.IsInvalid) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } - bool success = EventMethods.EvtUpdateBookmark(handle, eventHandle); + bool success = NativeMethods.EvtUpdateBookmark(handle, eventHandle); error = Marshal.GetLastWin32Error(); if (!success) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } - success = EventMethods.EvtRender( + success = NativeMethods.EvtRender( EvtHandle.Zero, handle, EvtRenderFlags.Bookmark, @@ -145,7 +145,7 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } Span buffer = stackalloc char[bufferUsed / sizeof(char)]; @@ -154,7 +154,7 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) { fixed (char* bufferPtr = buffer) { - success = EventMethods.EvtRender( + success = NativeMethods.EvtRender( EvtHandle.Zero, handle, EvtRenderFlags.Bookmark, @@ -169,7 +169,7 @@ public bool TryGetEvents(out EventRecord[] events, int batchSize = 30) if (!success) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } return bufferUsed - 1 <= 0 ? null : new string(buffer[..^1]); diff --git a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs index 59afd9db..55be3f82 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs @@ -35,12 +35,12 @@ public IEnumerable GetLogNames() { List paths = []; - using EvtHandle channelHandle = EventMethods.EvtOpenChannelEnum(Handle, 0); + using EvtHandle channelHandle = NativeMethods.EvtOpenChannelEnum(Handle, 0); int error = Marshal.GetLastWin32Error(); if (channelHandle.IsInvalid) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } bool doneReading = false; @@ -63,12 +63,12 @@ public HashSet GetProviderNames() { HashSet providers = []; - using EvtHandle providerHandle = EventMethods.EvtOpenPublisherEnum(Handle, 0); + using EvtHandle providerHandle = NativeMethods.EvtOpenPublisherEnum(Handle, 0); int error = Marshal.GetLastWin32Error(); if (providerHandle.IsInvalid) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } bool doneReading = false; @@ -89,14 +89,14 @@ public HashSet GetProviderNames() private static EvtHandle CreateRenderContext(EvtRenderContextFlags renderContextFlags) { - EvtHandle renderContextHandle = EventMethods.EvtCreateRenderContext(0, null, renderContextFlags); + EvtHandle renderContextHandle = NativeMethods.EvtCreateRenderContext(0, null, renderContextFlags); int error = Marshal.GetLastWin32Error(); if (renderContextHandle.IsInvalid) { renderContextHandle.Dispose(); - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } return renderContextHandle; @@ -104,7 +104,7 @@ private static EvtHandle CreateRenderContext(EvtRenderContextFlags renderContext private static string NextChannelPath(EvtHandle channelHandle, ref bool doneReading) { - bool success = EventMethods.EvtNextChannelPath(channelHandle, 0, null, out int bufferSize); + bool success = NativeMethods.EvtNextChannelPath(channelHandle, 0, null, out int bufferSize); int error = Marshal.GetLastWin32Error(); if (!success) @@ -118,18 +118,18 @@ private static string NextChannelPath(EvtHandle channelHandle, ref bool doneRead if (error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } } Span buffer = stackalloc char[bufferSize]; - success = EventMethods.EvtNextChannelPath(channelHandle, bufferSize, buffer, out bufferSize); + success = NativeMethods.EvtNextChannelPath(channelHandle, bufferSize, buffer, out bufferSize); error = Marshal.GetLastWin32Error(); if (!success) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } return bufferSize - 1 <= 0 ? string.Empty : new string(buffer[..(bufferSize - 1)]); @@ -137,7 +137,7 @@ private static string NextChannelPath(EvtHandle channelHandle, ref bool doneRead private static string NextPublisherId(EvtHandle publisherHandle, ref bool doneReading) { - bool success = EventMethods.EvtNextPublisherId(publisherHandle, 0, null, out int bufferSize); + bool success = NativeMethods.EvtNextPublisherId(publisherHandle, 0, null, out int bufferSize); int error = Marshal.GetLastWin32Error(); if (!success) @@ -151,18 +151,18 @@ private static string NextPublisherId(EvtHandle publisherHandle, ref bool doneRe if (error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } } Span buffer = stackalloc char[bufferSize]; - success = EventMethods.EvtNextPublisherId(publisherHandle, bufferSize, buffer, out bufferSize); + success = NativeMethods.EvtNextPublisherId(publisherHandle, bufferSize, buffer, out bufferSize); error = Marshal.GetLastWin32Error(); if (!success) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } return bufferSize - 1 <= 0 ? string.Empty : new string(buffer[..(bufferSize - 1)]); diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index 79bbbe79..cd97f7a8 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; @@ -30,13 +30,13 @@ public EventLogWatcher(string path, string? bookmark, bool renderXml = false) _renderXml = renderXml; // Validate the log exists by attempting to open it - _queryHandle = EventMethods.EvtQuery(EventLogSession.GlobalSession.Handle, path, null, PathType.LogName); + _queryHandle = NativeMethods.EvtQuery(EventLogSession.GlobalSession.Handle, path, null, PathType.LogName); int error = Marshal.GetLastWin32Error(); if (_queryHandle is { IsInvalid: false }) { return; } _queryHandle.Dispose(); - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } public EventLogWatcher(string path, bool renderXml = false) : this(path, null, renderXml) { } @@ -105,7 +105,7 @@ private void ProcessNewEvents(object? state, bool timedOut) int count = 0; - success = EventMethods.EvtNext(_subscriptionHandle, buffer.Length, buffer, 0, 0, ref count); + success = NativeMethods.EvtNext(_subscriptionHandle, buffer.Length, buffer, 0, 0, ref count); if (!success) { return; } @@ -116,12 +116,12 @@ private void ProcessNewEvents(object? state, bool timedOut) try { - @event = EventMethods.RenderEvent(eventHandle); - @event.Properties = EventMethods.RenderEventProperties(eventHandle); + @event = NativeMethods.RenderEvent(eventHandle); + @event.Properties = NativeMethods.RenderEventProperties(eventHandle); if (_renderXml) { - @event.Xml = EventMethods.RenderEventXml(eventHandle); + @event.Xml = NativeMethods.RenderEventXml(eventHandle); } } catch (Exception ex) @@ -146,16 +146,16 @@ private void Subscribe() EvtHandle bookmarkHandle = string.IsNullOrEmpty(_bookmark) ? EvtHandle.Zero : - EventMethods.EvtCreateBookmark(_bookmark); + NativeMethods.EvtCreateBookmark(_bookmark); if (!string.IsNullOrEmpty(_bookmark) && bookmarkHandle.IsInvalid) { int error = Marshal.GetLastWin32Error(); bookmarkHandle.Dispose(); - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } - _subscriptionHandle = EventMethods.EvtSubscribe( + _subscriptionHandle = NativeMethods.EvtSubscribe( EventLogSession.GlobalSession.Handle, _newEvents.SafeWaitHandle, _path, @@ -174,7 +174,7 @@ private void Subscribe() if (_subscriptionHandle.IsInvalid) { - EventMethods.ThrowEventLogException(subscriptionError); + NativeMethods.ThrowEventLogException(subscriptionError); } _isSubscribed = true; From fab021955ef049728ef8ff30089ca4592e23ed7d Mon Sep 17 00:00:00 2001 From: jschick04 Date: Wed, 6 May 2026 22:54:57 -0500 Subject: [PATCH 04/24] Move PathType to Readers namespace --- .../BannerHost.razor.cs | 4 ++-- .../Filters/SubFilterRow.razor.cs | 2 +- .../EventResolvers/EventXmlResolverTests.cs | 2 +- .../EventResolvers/EventResolverBase.cs | 1 + .../Models/DisplayEventModel.cs | 4 ++-- .../Models/EventRecord.cs | 4 ++-- .../{Helpers => Readers}/PathType.cs | 2 +- .../DateRangeDefaultsTests.cs | 2 +- .../Services/FilterCategoryItemsCacheTests.cs | 1 + .../Services/FilterServiceTests.cs | 2 +- .../Store/EventLog/EventLogEffectsTests.cs | 1 + .../Store/EventLog/EventLogStoreTests.cs | 1 + .../Store/EventTable/EventTableStoreTests.cs | 2 +- .../Store/FilterPane/FilterPaneEffectsTests.cs | 2 +- .../TestUtils/EventUtils.cs | 4 ++-- src/EventLogExpert.UI/Models/EventLogData.cs | 1 + src/EventLogExpert.UI/Models/EventTableModel.cs | 4 ++-- .../Services/DatabaseService.cs | 3 ++- .../Services/DeploymentService.cs | 10 +++++----- .../Store/EventLog/EventLogAction.cs | 2 +- .../Store/EventLog/EventLogReducers.cs | 2 +- .../Store/EventLog/ILogReloadCoordinator.cs | 2 +- .../Store/EventTable/EventTableReducers.cs | 2 +- .../Store/FilterCache/FilterCacheReducers.cs | 16 ++++++++-------- src/EventLogExpert.UI/Store/LoggingMiddleware.cs | 1 + .../Components/Sections/SplitLogTabPane.razor.cs | 4 ++-- src/EventLogExpert/MainPage.xaml.cs | 1 + src/EventLogExpert/MauiProgram.cs | 2 +- src/EventLogExpert/_Imports.razor | 1 + 29 files changed, 47 insertions(+), 38 deletions(-) rename src/EventLogExpert.Eventing/{Helpers => Readers}/PathType.cs (76%) diff --git a/src/EventLogExpert.Components/BannerHost.razor.cs b/src/EventLogExpert.Components/BannerHost.razor.cs index 9a10cdfe..65feac67 100644 --- a/src/EventLogExpert.Components/BannerHost.razor.cs +++ b/src/EventLogExpert.Components/BannerHost.razor.cs @@ -261,14 +261,14 @@ private async Task OnReloadClickedAsync() _displayedIndex = i; _selectedItem = items[i]; - + return (_selectedItem, _selectedItem.View); } } _displayedIndex = Math.Clamp(_displayedIndex, 0, items.Count - 1); _selectedItem = items[_displayedIndex]; - + return (_selectedItem, _selectedItem.View); } } diff --git a/src/EventLogExpert.Components/Filters/SubFilterRow.razor.cs b/src/EventLogExpert.Components/Filters/SubFilterRow.razor.cs index b9fa14c4..57d8db5a 100644 --- a/src/EventLogExpert.Components/Filters/SubFilterRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/SubFilterRow.razor.cs @@ -12,4 +12,4 @@ public sealed partial class SubFilterRow : FilterRowBase [Parameter] public EventCallback OnRemove { get; set; } private async Task RemoveSubFilter() => await OnRemove.InvokeAsync(Value.Id); -} \ No newline at end of file +} diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs index b85bb9a6..c16b9fd9 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs @@ -2,8 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; namespace EventLogExpert.Eventing.Tests.EventResolvers; diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs b/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs index 0d2a917a..177c6851 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs @@ -5,6 +5,7 @@ using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; +using EventLogExpert.Eventing.Readers; using System.Buffers; using System.Collections.Concurrent; using System.Security.Principal; diff --git a/src/EventLogExpert.Eventing/Models/DisplayEventModel.cs b/src/EventLogExpert.Eventing/Models/DisplayEventModel.cs index 9c45823c..7e656bbe 100644 --- a/src/EventLogExpert.Eventing/Models/DisplayEventModel.cs +++ b/src/EventLogExpert.Eventing/Models/DisplayEventModel.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; using System.Security.Principal; namespace EventLogExpert.Eventing.Models; diff --git a/src/EventLogExpert.Eventing/Models/EventRecord.cs b/src/EventLogExpert.Eventing/Models/EventRecord.cs index 147c8f87..f695b502 100644 --- a/src/EventLogExpert.Eventing/Models/EventRecord.cs +++ b/src/EventLogExpert.Eventing/Models/EventRecord.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; using System.Security.Principal; namespace EventLogExpert.Eventing.Models; diff --git a/src/EventLogExpert.Eventing/Helpers/PathType.cs b/src/EventLogExpert.Eventing/Readers/PathType.cs similarity index 76% rename from src/EventLogExpert.Eventing/Helpers/PathType.cs rename to src/EventLogExpert.Eventing/Readers/PathType.cs index ec612e05..49621514 100644 --- a/src/EventLogExpert.Eventing/Helpers/PathType.cs +++ b/src/EventLogExpert.Eventing/Readers/PathType.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Readers; public enum PathType { diff --git a/src/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs b/src/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs index 2083d1c3..f5a0f6c5 100644 --- a/src/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs +++ b/src/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; using EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs b/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs index c8bd040f..9a7a216b 100644 --- a/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs @@ -3,6 +3,7 @@ using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs b/src/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs index bf7eb6a2..06162bb6 100644 --- a/src/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs index cee29888..faf25f60 100644 --- a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs +++ b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs @@ -4,6 +4,7 @@ using EventLogExpert.Eventing.EventResolvers; using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; diff --git a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs index 555cd077..a361458c 100644 --- a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs +++ b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs @@ -3,6 +3,7 @@ using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs b/src/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs index 9401ef9a..949f71d8 100644 --- a/src/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs +++ b/src/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs b/src/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs index 2387cdc5..b7ffef71 100644 --- a/src/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs +++ b/src/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterCache; diff --git a/src/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs b/src/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs index 558b5dea..8a5ca00f 100644 --- a/src/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs +++ b/src/EventLogExpert.UI.Tests/TestUtils/EventUtils.cs @@ -1,8 +1,8 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; namespace EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI/Models/EventLogData.cs b/src/EventLogExpert.UI/Models/EventLogData.cs index fcaa2c12..9512f371 100644 --- a/src/EventLogExpert.UI/Models/EventLogData.cs +++ b/src/EventLogExpert.UI/Models/EventLogData.cs @@ -3,6 +3,7 @@ using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Readers; namespace EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.UI/Models/EventTableModel.cs b/src/EventLogExpert.UI/Models/EventTableModel.cs index 336d3ecc..4e097801 100644 --- a/src/EventLogExpert.UI/Models/EventTableModel.cs +++ b/src/EventLogExpert.UI/Models/EventTableModel.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; namespace EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.UI/Services/DatabaseService.cs b/src/EventLogExpert.UI/Services/DatabaseService.cs index 246dda2d..4338680e 100644 --- a/src/EventLogExpert.UI/Services/DatabaseService.cs +++ b/src/EventLogExpert.UI/Services/DatabaseService.cs @@ -1174,7 +1174,8 @@ private async Task ProcessBatchAsync(UpgradeBatch batch) } } - private void RaiseEntriesChanged() { + private void RaiseEntriesChanged() + { var handler = EntriesChanged; if (handler is null) { return; } diff --git a/src/EventLogExpert.UI/Services/DeploymentService.cs b/src/EventLogExpert.UI/Services/DeploymentService.cs index 764f73dd..1685bf47 100644 --- a/src/EventLogExpert.UI/Services/DeploymentService.cs +++ b/src/EventLogExpert.UI/Services/DeploymentService.cs @@ -66,7 +66,7 @@ private void SetCallbacks(IAsyncOperationWithProgress state with - { - FavoriteFilters = action.FavoriteFilters, - RecentFilters = action.RecentFilters - }; + { + FavoriteFilters = action.FavoriteFilters, + RecentFilters = action.RecentFilters + }; [ReducerMethod] public static FilterCacheState ReduceRemoveFavoriteFilterCompleted(FilterCacheState state, FilterCacheAction.RemoveFavoriteFilterCompleted action) => state with - { - FavoriteFilters = action.FavoriteFilters, - RecentFilters = action.RecentFilters - }; + { + FavoriteFilters = action.FavoriteFilters, + RecentFilters = action.RecentFilters + }; } diff --git a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs b/src/EventLogExpert.UI/Store/LoggingMiddleware.cs index e202a177..1bb573e3 100644 --- a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs +++ b/src/EventLogExpert.UI/Store/LoggingMiddleware.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.StatusBar; diff --git a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs b/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs index 656a94f0..d3013190 100644 --- a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; diff --git a/src/EventLogExpert/MainPage.xaml.cs b/src/EventLogExpert/MainPage.xaml.cs index e3c82ed5..d278562f 100644 --- a/src/EventLogExpert/MainPage.xaml.cs +++ b/src/EventLogExpert/MainPage.xaml.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; using EventLogExpert.Services; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 71c5ecf4..21bb36b9 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Components.Modals.Alerts; using EventLogExpert.Eventing.EventResolvers; using EventLogExpert.Eventing.Helpers; using EventLogExpert.Services; -using EventLogExpert.Components.Modals.Alerts; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index 9fa32866..bfb0d2fc 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -18,6 +18,7 @@ @using EventLogExpert.Components.Modals.Filters @using EventLogExpert.Components.Sections @using EventLogExpert.Eventing.Helpers +@using EventLogExpert.Eventing.Readers @using EventLogExpert.UI @using EventLogExpert.UI.Models From 3eeabdc3493de90094ec9344be7c56ced508d15b Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 09:06:09 -0500 Subject: [PATCH 05/24] Delete dead Eventing.Helpers.ExtensionMethods extension --- .../Helpers/ExtensionMethods.cs | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 src/EventLogExpert.Eventing/Helpers/ExtensionMethods.cs diff --git a/src/EventLogExpert.Eventing/Helpers/ExtensionMethods.cs b/src/EventLogExpert.Eventing/Helpers/ExtensionMethods.cs deleted file mode 100644 index 4d809cb8..00000000 --- a/src/EventLogExpert.Eventing/Helpers/ExtensionMethods.cs +++ /dev/null @@ -1,21 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using System.Text.RegularExpressions; - -namespace EventLogExpert.Eventing.Helpers; - -public static class ExtensionMethods -{ - public static string ReplaceCaseInsensitiveFind( - this string str, - string findMe, - string newValue - ) - { - return Regex.Replace(str, - Regex.Escape(findMe), - Regex.Replace(newValue, "\\$[0-9]+", @"$$$0"), - RegexOptions.IgnoreCase); - } -} From c2758c8ab58c61fdb81370e42ccd347303816eab Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 09:22:52 -0500 Subject: [PATCH 06/24] Move SeverityLevel and Severity to Models namespace --- .../SeverityMethods.cs => Models/Severity.cs} | 12 +----------- .../Models/SeverityLevel.cs | 14 ++++++++++++++ .../Services/FilterCategoryItemsCacheTests.cs | 1 - .../Store/EventLog/EventLogStoreTests.cs | 1 - src/EventLogExpert.UI/Models/EventLogData.cs | 1 - .../Services/FilterCategoryItemsCache.cs | 2 +- 6 files changed, 16 insertions(+), 15 deletions(-) rename src/EventLogExpert.Eventing/{Helpers/SeverityMethods.cs => Models/Severity.cs} (77%) create mode 100644 src/EventLogExpert.Eventing/Models/SeverityLevel.cs diff --git a/src/EventLogExpert.Eventing/Helpers/SeverityMethods.cs b/src/EventLogExpert.Eventing/Models/Severity.cs similarity index 77% rename from src/EventLogExpert.Eventing/Helpers/SeverityMethods.cs rename to src/EventLogExpert.Eventing/Models/Severity.cs index fe5e8988..303a6975 100644 --- a/src/EventLogExpert.Eventing/Helpers/SeverityMethods.cs +++ b/src/EventLogExpert.Eventing/Models/Severity.cs @@ -1,17 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.Helpers; - -/// Severity levels with values matching ETW standard levels (1–5). -public enum SeverityLevel -{ - Critical = 1, - Error = 2, - Warning = 3, - Information = 4, - Verbose = 5 -} +namespace EventLogExpert.Eventing.Models; public static class Severity { diff --git a/src/EventLogExpert.Eventing/Models/SeverityLevel.cs b/src/EventLogExpert.Eventing/Models/SeverityLevel.cs new file mode 100644 index 00000000..a5a7d176 --- /dev/null +++ b/src/EventLogExpert.Eventing/Models/SeverityLevel.cs @@ -0,0 +1,14 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.Eventing.Models; + +/// Severity levels with values matching ETW standard levels (1–5). +public enum SeverityLevel +{ + Critical = 1, + Error = 2, + Warning = 3, + Information = 4, + Verbose = 5 +} diff --git a/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs b/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs index 9a7a216b..92a18607 100644 --- a/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs index a361458c..259e505c 100644 --- a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs +++ b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.UI/Models/EventLogData.cs b/src/EventLogExpert.UI/Models/EventLogData.cs index 9512f371..f6f90ecf 100644 --- a/src/EventLogExpert.UI/Models/EventLogData.cs +++ b/src/EventLogExpert.UI/Models/EventLogData.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; diff --git a/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs b/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs index 7ee67062..ebd050dc 100644 --- a/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs +++ b/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Models; using EventLogExpert.UI.Models; using System.Collections.Concurrent; using System.Collections.Immutable; From b471715fa4da67d7e56df1763c7864a47d000715 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 10:29:26 -0500 Subject: [PATCH 07/24] Move Helpers types to Logging and Readers --- .../BannerHostTests.cs | 2 +- .../Database/DatabaseRecoveryDialogTests.cs | 2 +- .../Database/DatabaseRecoveryHostTests.cs | 2 +- .../SettingsUpgradeProgressBannerTests.cs | 2 +- .../BannerHost.razor.cs | 2 +- .../Database/DatabaseRecoveryDialog.razor.cs | 2 +- .../Database/DatabaseRecoveryHost.razor.cs | 2 +- .../SettingsUpgradeProgressBanner.razor.cs | 2 +- .../Menu/MenuBar.razor.cs | 2 +- .../DiffDatabaseCommandTests.cs | 2 +- .../MergeDatabaseCommandTests.cs | 2 +- .../ProviderSourceTests.cs | 2 +- .../UpgradeDatabaseCommandTests.cs | 2 +- .../CreateDatabaseCommand.cs | 2 +- .../DbToolCommand.cs | 2 +- .../DiffDatabaseCommand.cs | 2 +- .../MergeDatabaseCommand.cs | 2 +- .../MtaProviderSource.cs | 2 +- src/EventLogExpert.EventDbTool/Program.cs | 2 +- .../ProviderSource.cs | 2 +- src/EventLogExpert.EventDbTool/RegexHelper.cs | 2 +- src/EventLogExpert.EventDbTool/ShowCommand.cs | 2 +- .../UpgradeDatabaseCommand.cs | 2 +- .../EventProviderDbContextTests.cs | 2 +- ...EventProviderDatabaseEventResolverTests.cs | 2 +- .../EventResolvers/EventResolverBaseTests.cs | 2 +- .../LocalProviderEventResolverTests.cs | 2 +- .../VersatileEventResolverTests.cs | 2 +- .../Helpers/LogNamesTests.cs | 2 +- .../Providers/EventMessageProviderTests.cs | 2 +- .../Providers/ProviderMetadataTests.cs | 2 +- .../Providers/RegistryProviderTests.cs | 2 +- .../Readers/EventLogInformationTests.cs | 1 - .../Readers/EventLogReaderTests.cs | 1 - .../Readers/EventLogSessionTests.cs | 1 - .../Readers/EventLogWatcherTests.cs | 1 - .../EventProviderDbContext.cs | 2 +- .../EventResolvers/EventResolver.cs | 2 +- .../EventResolvers/EventResolverBase.cs | 2 +- .../EventResolvers/EventXmlResolver.cs | 1 - .../Helpers/TraceInterpolatedStringHandler.cs | 332 ------------------ .../Interop/NativeMethods.Evt.cs | 1 - .../Logging/CriticalLogHandler.cs | 46 +++ .../Logging/DebugLogHandler.cs | 46 +++ .../Logging/ErrorLogHandler.cs | 46 +++ .../{Helpers => Logging}/ITraceLogger.cs | 2 +- .../Logging/InfoLogHandler.cs | 46 +++ .../Logging/LogHandlerCore.cs | 100 ++++++ .../Logging/TraceLogHandler.cs | 46 +++ .../{Helpers => Logging}/TraceLogger.cs | 2 +- .../Logging/WarnLogHandler.cs | 46 +++ .../Providers/EventMessageProvider.cs | 2 +- .../Providers/ProviderMetadata.cs | 2 +- .../Providers/RegistryProvider.cs | 3 +- .../Readers/EventLogInformation.cs | 1 - .../Readers/EventLogReader.cs | 1 - .../Readers/EventLogSession.cs | 1 - .../Readers/EventLogWatcher.cs | 1 - .../{Helpers => Readers}/LogNames.cs | 2 +- .../Services/BannerServiceTests.cs | 2 +- .../Services/DatabaseServiceTests.cs | 2 +- .../Services/DeploymentServiceTests.cs | 2 +- .../Services/GitHubServiceTests.cs | 2 +- .../Services/UpdateServiceTests.cs | 2 +- .../Store/EventLog/EventLogEffectsTests.cs | 2 +- .../TestUtils/DatabaseSeedUtils.cs | 2 +- .../TestUtils/LoggerUtils.cs | 2 +- src/EventLogExpert.UI/LogNameMethods.cs | 2 +- .../Services/ApplicationRestartService.cs | 2 +- .../Services/BannerService.cs | 2 +- .../Services/DatabaseService.cs | 2 +- .../Services/DebugLogService.cs | 2 +- .../Services/DeploymentService.cs | 2 +- .../Services/GitHubService.cs | 2 +- .../Services/UpdateService.cs | 2 +- .../Store/EventLog/EventLogEffects.cs | 2 +- .../Store/EventLog/LiveLogWatcherService.cs | 2 +- .../Store/LoggingMiddleware.cs | 2 +- src/EventLogExpert/App.xaml.cs | 2 +- .../Layout/UnhandledExceptionHandler.razor.cs | 2 +- .../Components/Sections/DetailsPane.razor.cs | 2 +- .../Components/Sections/EventTable.razor.cs | 2 +- src/EventLogExpert/MainPage.xaml.cs | 2 +- src/EventLogExpert/MauiProgram.cs | 2 +- .../Services/ClipboardService.cs | 2 +- .../Services/MauiMenuActionService.cs | 2 +- src/EventLogExpert/_Imports.razor | 1 - 87 files changed, 445 insertions(+), 411 deletions(-) delete mode 100644 src/EventLogExpert.Eventing/Helpers/TraceInterpolatedStringHandler.cs create mode 100644 src/EventLogExpert.Eventing/Logging/CriticalLogHandler.cs create mode 100644 src/EventLogExpert.Eventing/Logging/DebugLogHandler.cs create mode 100644 src/EventLogExpert.Eventing/Logging/ErrorLogHandler.cs rename src/EventLogExpert.Eventing/{Helpers => Logging}/ITraceLogger.cs (94%) create mode 100644 src/EventLogExpert.Eventing/Logging/InfoLogHandler.cs create mode 100644 src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs create mode 100644 src/EventLogExpert.Eventing/Logging/TraceLogHandler.cs rename src/EventLogExpert.Eventing/{Helpers => Logging}/TraceLogger.cs (97%) create mode 100644 src/EventLogExpert.Eventing/Logging/WarnLogHandler.cs rename src/EventLogExpert.Eventing/{Helpers => Readers}/LogNames.cs (93%) diff --git a/src/EventLogExpert.Components.Tests/BannerHostTests.cs b/src/EventLogExpert.Components.Tests/BannerHostTests.cs index 9df1a10d..a2026c1a 100644 --- a/src/EventLogExpert.Components.Tests/BannerHostTests.cs +++ b/src/EventLogExpert.Components.Tests/BannerHostTests.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using Bunit; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs b/src/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs index a328123b..3a82ee22 100644 --- a/src/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs +++ b/src/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs @@ -3,7 +3,7 @@ using Bunit; using EventLogExpert.Components.Database; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs b/src/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs index f1a21cb2..8f19b9ac 100644 --- a/src/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs +++ b/src/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs @@ -3,7 +3,7 @@ using Bunit; using EventLogExpert.Components.Database; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs b/src/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs index 3f8eb06c..795d183c 100644 --- a/src/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs +++ b/src/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs @@ -3,7 +3,7 @@ using Bunit; using EventLogExpert.Components.Database; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.Extensions.DependencyInjection; diff --git a/src/EventLogExpert.Components/BannerHost.razor.cs b/src/EventLogExpert.Components/BannerHost.razor.cs index 65feac67..6a877e43 100644 --- a/src/EventLogExpert.Components/BannerHost.razor.cs +++ b/src/EventLogExpert.Components/BannerHost.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs index a9faeb11..c25a097d 100644 --- a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Base; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components; diff --git a/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs b/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs index 86895d8c..338cad93 100644 --- a/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components; diff --git a/src/EventLogExpert.Components/Database/SettingsUpgradeProgressBanner.razor.cs b/src/EventLogExpert.Components/Database/SettingsUpgradeProgressBanner.razor.cs index 41483fe8..e3fb6336 100644 --- a/src/EventLogExpert.Components/Database/SettingsUpgradeProgressBanner.razor.cs +++ b/src/EventLogExpert.Components/Database/SettingsUpgradeProgressBanner.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components; diff --git a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs index ae531901..835b0317 100644 --- a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs b/src/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs index d2fbb0ef..058e5f5c 100644 --- a/src/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs +++ b/src/EventLogExpert.EventDbTool.Tests/DiffDatabaseCommandTests.cs @@ -4,7 +4,7 @@ using EventLogExpert.EventDbTool.Tests.TestUtils; using EventLogExpert.EventDbTool.Tests.TestUtils.Constants; using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using NSubstitute; namespace EventLogExpert.EventDbTool.Tests; diff --git a/src/EventLogExpert.EventDbTool.Tests/MergeDatabaseCommandTests.cs b/src/EventLogExpert.EventDbTool.Tests/MergeDatabaseCommandTests.cs index 47de5a57..afafa34a 100644 --- a/src/EventLogExpert.EventDbTool.Tests/MergeDatabaseCommandTests.cs +++ b/src/EventLogExpert.EventDbTool.Tests/MergeDatabaseCommandTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.EventDbTool.Tests.TestUtils; using EventLogExpert.EventDbTool.Tests.TestUtils.Constants; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using NSubstitute; namespace EventLogExpert.EventDbTool.Tests; diff --git a/src/EventLogExpert.EventDbTool.Tests/ProviderSourceTests.cs b/src/EventLogExpert.EventDbTool.Tests/ProviderSourceTests.cs index 60155db4..2176830d 100644 --- a/src/EventLogExpert.EventDbTool.Tests/ProviderSourceTests.cs +++ b/src/EventLogExpert.EventDbTool.Tests/ProviderSourceTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.EventDbTool.Tests.TestUtils; using EventLogExpert.EventDbTool.Tests.TestUtils.Constants; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using NSubstitute; namespace EventLogExpert.EventDbTool.Tests; diff --git a/src/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs b/src/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs index a2a4f6a5..10d4c42b 100644 --- a/src/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs +++ b/src/EventLogExpert.EventDbTool.Tests/UpgradeDatabaseCommandTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.EventDbTool.Tests.TestUtils; using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using NSubstitute; namespace EventLogExpert.EventDbTool.Tests; diff --git a/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs index c23781d2..8fd4312c 100644 --- a/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs +++ b/src/EventLogExpert.EventDbTool/CreateDatabaseCommand.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using Microsoft.Extensions.DependencyInjection; using System.CommandLine; diff --git a/src/EventLogExpert.EventDbTool/DbToolCommand.cs b/src/EventLogExpert.EventDbTool/DbToolCommand.cs index db165b71..07e3a12b 100644 --- a/src/EventLogExpert.EventDbTool/DbToolCommand.cs +++ b/src/EventLogExpert.EventDbTool/DbToolCommand.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Readers; using System.Text.RegularExpressions; diff --git a/src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs index 7e572bc6..410c3ff8 100644 --- a/src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs +++ b/src/EventLogExpert.EventDbTool/DiffDatabaseCommand.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using Microsoft.Extensions.DependencyInjection; using System.CommandLine; diff --git a/src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs index b34d5e44..07b42d2b 100644 --- a/src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs +++ b/src/EventLogExpert.EventDbTool/MergeDatabaseCommand.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/EventLogExpert.EventDbTool/MtaProviderSource.cs b/src/EventLogExpert.EventDbTool/MtaProviderSource.cs index e237158b..fe765002 100644 --- a/src/EventLogExpert.EventDbTool/MtaProviderSource.cs +++ b/src/EventLogExpert.EventDbTool/MtaProviderSource.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Readers; using System.Text.RegularExpressions; diff --git a/src/EventLogExpert.EventDbTool/Program.cs b/src/EventLogExpert.EventDbTool/Program.cs index 776a5c4f..d450e053 100644 --- a/src/EventLogExpert.EventDbTool/Program.cs +++ b/src/EventLogExpert.EventDbTool/Program.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.CommandLine; diff --git a/src/EventLogExpert.EventDbTool/ProviderSource.cs b/src/EventLogExpert.EventDbTool/ProviderSource.cs index df07b879..0865e69e 100644 --- a/src/EventLogExpert.EventDbTool/ProviderSource.cs +++ b/src/EventLogExpert.EventDbTool/ProviderSource.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using Microsoft.EntityFrameworkCore; using System.Data.Common; diff --git a/src/EventLogExpert.EventDbTool/RegexHelper.cs b/src/EventLogExpert.EventDbTool/RegexHelper.cs index 3aac00ff..f534fbbb 100644 --- a/src/EventLogExpert.EventDbTool/RegexHelper.cs +++ b/src/EventLogExpert.EventDbTool/RegexHelper.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using System.Text.RegularExpressions; namespace EventLogExpert.EventDbTool; diff --git a/src/EventLogExpert.EventDbTool/ShowCommand.cs b/src/EventLogExpert.EventDbTool/ShowCommand.cs index e8d68698..b4783281 100644 --- a/src/EventLogExpert.EventDbTool/ShowCommand.cs +++ b/src/EventLogExpert.EventDbTool/ShowCommand.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using Microsoft.Extensions.DependencyInjection; using System.CommandLine; diff --git a/src/EventLogExpert.EventDbTool/UpgradeDatabaseCommand.cs b/src/EventLogExpert.EventDbTool/UpgradeDatabaseCommand.cs index 5881bd9d..81a39b17 100644 --- a/src/EventLogExpert.EventDbTool/UpgradeDatabaseCommand.cs +++ b/src/EventLogExpert.EventDbTool/UpgradeDatabaseCommand.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using Microsoft.Extensions.DependencyInjection; using System.CommandLine; using System.Data.Common; diff --git a/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs b/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs index 00f64e4f..3a089d77 100644 --- a/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using Microsoft.Data.Sqlite; diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventProviderDatabaseEventResolverTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventProviderDatabaseEventResolverTests.cs index 2f663149..512e0cef 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventProviderDatabaseEventResolverTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventProviderDatabaseEventResolverTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils; diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs index fe40a900..69b68184 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs @@ -2,8 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils; diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/LocalProviderEventResolverTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/LocalProviderEventResolverTests.cs index 6fb03bf0..2883e4cc 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/LocalProviderEventResolverTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/LocalProviderEventResolverTests.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Tests.TestUtils; using EventLogExpert.Eventing.Tests.TestUtils.Constants; diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs index c2f0f32d..500408aa 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils; diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs b/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs index ff058a3e..8f92ca4b 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; namespace EventLogExpert.Eventing.Tests.Helpers; diff --git a/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs index addf7241..1ab03ac9 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; diff --git a/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs index 2dcc370b..e4eaaca1 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; diff --git a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs index 011cc3a3..e0e31516 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; using NSubstitute; diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs index 7f8c5fbd..5821567f 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs index fc71d29b..e6a8dbe2 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs index 29649aa0..bd8a3b14 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs index 5b5ab15b..0cd72df5 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs b/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs index 831011ee..3c6fb89f 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs +++ b/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using Microsoft.EntityFrameworkCore; diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventResolver.cs b/src/EventLogExpert.Eventing/EventResolvers/EventResolver.cs index b283064c..8f0fa39c 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventResolver.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventResolver.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using Microsoft.EntityFrameworkCore; diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs b/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs index 177c6851..b0412767 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Readers; diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs index c651fd99..1be13ce7 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; diff --git a/src/EventLogExpert.Eventing/Helpers/TraceInterpolatedStringHandler.cs b/src/EventLogExpert.Eventing/Helpers/TraceInterpolatedStringHandler.cs deleted file mode 100644 index 0b4d8792..00000000 --- a/src/EventLogExpert.Eventing/Helpers/TraceInterpolatedStringHandler.cs +++ /dev/null @@ -1,332 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using Microsoft.Extensions.Logging; -using System.Runtime.CompilerServices; -using System.Text; - -namespace EventLogExpert.Eventing.Helpers; - -internal struct LogHandlerCore -{ - [ThreadStatic] private static StringBuilder? t_pooledBuilder; - - private const int MaxPooledCapacity = 4096; - - private StringBuilder? _builder; - - internal bool IsEnabled; - - internal void Init(int literalLength, int formattedCount, ITraceLogger logger, LogLevel level, out bool isEnabled) - { - IsEnabled = isEnabled = level >= logger.MinimumLevel; - - if (!isEnabled) { return; } - - var pooled = t_pooledBuilder; - - if (pooled is not null) - { - t_pooledBuilder = null; - _builder = pooled; - } - else - { - _builder = new StringBuilder(literalLength + (formattedCount * 8)); - } - } - - internal void AppendLiteral(string s) => _builder!.Append(s); - - internal void AppendFormatted(T value) => _builder!.Append(value); - - internal void AppendFormatted(string? value) => _builder!.Append(value); - - internal void AppendFormatted(int value) => _builder!.Append(value); - - internal void AppendFormatted(long value) => _builder!.Append(value); - - internal void AppendFormatted(double value) => _builder!.Append(value); - - internal void AppendFormatted(T value, string? format) - { - if (format is null) { _builder!.Append(value); return; } - if (value is IFormattable formattable) { _builder!.Append(formattable.ToString(format, null)); return; } - _builder!.Append(value); - } - - internal void AppendFormatted(T value, int alignment) => AppendAligned(value, alignment, null); - - internal void AppendFormatted(T value, int alignment, string? format) => AppendAligned(value, alignment, format); - - internal void AppendFormatted(ReadOnlySpan value) => _builder!.Append(value); - - internal void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => - _builder!.Append(value); - - public override readonly string ToString() => _builder?.ToString() ?? string.Empty; - - internal string ToStringAndClear() - { - if (_builder is null) { return string.Empty; } - - string result = _builder.ToString(); - var consumed = _builder; - _builder = null; - consumed.Clear(); - if (consumed.Capacity <= MaxPooledCapacity) { t_pooledBuilder = consumed; } - - return result; - } - - private void AppendAligned(T value, int alignment, string? format) - { - string formatted = value switch - { - null => string.Empty, - IFormattable f when format is not null => f.ToString(format, null) ?? string.Empty, - _ => value.ToString() ?? string.Empty - }; - - int width = alignment == int.MinValue ? int.MaxValue : Math.Abs(alignment); - int padding = width - formatted.Length; - if (padding <= 0) { _builder!.Append(formatted); return; } - - if (alignment >= 0) { _builder!.Append(' ', padding).Append(formatted); } - else { _builder!.Append(formatted).Append(' ', padding); } - } -} - -[InterpolatedStringHandler] -public struct TraceLogHandler -{ - private LogHandlerCore _core; - - public readonly bool IsEnabled => _core.IsEnabled; - - public TraceLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => - _core.Init(literalLength, formattedCount, logger, LogLevel.Trace, out isEnabled); - - public void AppendLiteral(string s) => _core.AppendLiteral(s); - - public void AppendFormatted(T value) => _core.AppendFormatted(value); - - public void AppendFormatted(string? value) => _core.AppendFormatted(value); - - public void AppendFormatted(int value) => _core.AppendFormatted(value); - - public void AppendFormatted(long value) => _core.AppendFormatted(value); - - public void AppendFormatted(double value) => _core.AppendFormatted(value); - - public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); - - public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); - - public void AppendFormatted(T value, int alignment, string? format) => - _core.AppendFormatted(value, alignment, format); - - public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); - - public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => - _core.AppendFormatted(value, alignment, format); - - public override readonly string ToString() => _core.ToString(); - - public string ToStringAndClear() => _core.ToStringAndClear(); -} - -[InterpolatedStringHandler] -public struct DebugLogHandler -{ - private LogHandlerCore _core; - - public readonly bool IsEnabled => _core.IsEnabled; - - public DebugLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => - _core.Init(literalLength, formattedCount, logger, LogLevel.Debug, out isEnabled); - - public void AppendLiteral(string s) => _core.AppendLiteral(s); - - public void AppendFormatted(T value) => _core.AppendFormatted(value); - - public void AppendFormatted(string? value) => _core.AppendFormatted(value); - - public void AppendFormatted(int value) => _core.AppendFormatted(value); - - public void AppendFormatted(long value) => _core.AppendFormatted(value); - - public void AppendFormatted(double value) => _core.AppendFormatted(value); - - public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); - - public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); - - public void AppendFormatted(T value, int alignment, string? format) => - _core.AppendFormatted(value, alignment, format); - - public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); - - public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => - _core.AppendFormatted(value, alignment, format); - - public override readonly string ToString() => _core.ToString(); - - public string ToStringAndClear() => _core.ToStringAndClear(); -} - -[InterpolatedStringHandler] -public struct InfoLogHandler -{ - private LogHandlerCore _core; - - public readonly bool IsEnabled => _core.IsEnabled; - - public InfoLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => - _core.Init(literalLength, formattedCount, logger, LogLevel.Information, out isEnabled); - - public void AppendLiteral(string s) => _core.AppendLiteral(s); - - public void AppendFormatted(T value) => _core.AppendFormatted(value); - - public void AppendFormatted(string? value) => _core.AppendFormatted(value); - - public void AppendFormatted(int value) => _core.AppendFormatted(value); - - public void AppendFormatted(long value) => _core.AppendFormatted(value); - - public void AppendFormatted(double value) => _core.AppendFormatted(value); - - public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); - - public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); - - public void AppendFormatted(T value, int alignment, string? format) => - _core.AppendFormatted(value, alignment, format); - - public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); - - public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => - _core.AppendFormatted(value, alignment, format); - - public override readonly string ToString() => _core.ToString(); - - public string ToStringAndClear() => _core.ToStringAndClear(); -} - -[InterpolatedStringHandler] -public struct WarnLogHandler -{ - private LogHandlerCore _core; - - public readonly bool IsEnabled => _core.IsEnabled; - - public WarnLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => - _core.Init(literalLength, formattedCount, logger, LogLevel.Warning, out isEnabled); - - public void AppendLiteral(string s) => _core.AppendLiteral(s); - - public void AppendFormatted(T value) => _core.AppendFormatted(value); - - public void AppendFormatted(string? value) => _core.AppendFormatted(value); - - public void AppendFormatted(int value) => _core.AppendFormatted(value); - - public void AppendFormatted(long value) => _core.AppendFormatted(value); - - public void AppendFormatted(double value) => _core.AppendFormatted(value); - - public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); - - public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); - - public void AppendFormatted(T value, int alignment, string? format) => - _core.AppendFormatted(value, alignment, format); - - public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); - - public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => - _core.AppendFormatted(value, alignment, format); - - public override readonly string ToString() => _core.ToString(); - - public string ToStringAndClear() => _core.ToStringAndClear(); -} - -[InterpolatedStringHandler] -public struct ErrorLogHandler -{ - private LogHandlerCore _core; - - public readonly bool IsEnabled => _core.IsEnabled; - - public ErrorLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => - _core.Init(literalLength, formattedCount, logger, LogLevel.Error, out isEnabled); - - public void AppendLiteral(string s) => _core.AppendLiteral(s); - - public void AppendFormatted(T value) => _core.AppendFormatted(value); - - public void AppendFormatted(string? value) => _core.AppendFormatted(value); - - public void AppendFormatted(int value) => _core.AppendFormatted(value); - - public void AppendFormatted(long value) => _core.AppendFormatted(value); - - public void AppendFormatted(double value) => _core.AppendFormatted(value); - - public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); - - public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); - - public void AppendFormatted(T value, int alignment, string? format) => - _core.AppendFormatted(value, alignment, format); - - public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); - - public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => - _core.AppendFormatted(value, alignment, format); - - public override readonly string ToString() => _core.ToString(); - - public string ToStringAndClear() => _core.ToStringAndClear(); -} - -[InterpolatedStringHandler] -public struct CriticalLogHandler -{ - private LogHandlerCore _core; - - public readonly bool IsEnabled => _core.IsEnabled; - - public CriticalLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => - _core.Init(literalLength, formattedCount, logger, LogLevel.Critical, out isEnabled); - - public void AppendLiteral(string s) => _core.AppendLiteral(s); - - public void AppendFormatted(T value) => _core.AppendFormatted(value); - - public void AppendFormatted(string? value) => _core.AppendFormatted(value); - - public void AppendFormatted(int value) => _core.AppendFormatted(value); - - public void AppendFormatted(long value) => _core.AppendFormatted(value); - - public void AppendFormatted(double value) => _core.AppendFormatted(value); - - public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); - - public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); - - public void AppendFormatted(T value, int alignment, string? format) => - _core.AppendFormatted(value, alignment, format); - - public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); - - public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => - _core.AppendFormatted(value, alignment, format); - - public override readonly string ToString() => _core.ToString(); - - public string ToStringAndClear() => _core.ToStringAndClear(); -} diff --git a/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs index 1d2fff3f..ba2dd505 100644 --- a/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using Microsoft.Win32.SafeHandles; diff --git a/src/EventLogExpert.Eventing/Logging/CriticalLogHandler.cs b/src/EventLogExpert.Eventing/Logging/CriticalLogHandler.cs new file mode 100644 index 00000000..01e498d5 --- /dev/null +++ b/src/EventLogExpert.Eventing/Logging/CriticalLogHandler.cs @@ -0,0 +1,46 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; + +namespace EventLogExpert.Eventing.Logging; + +[InterpolatedStringHandler] +public struct CriticalLogHandler +{ + private LogHandlerCore _core; + + public readonly bool IsEnabled => _core.IsEnabled; + + public CriticalLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => + _core.Init(literalLength, formattedCount, logger, LogLevel.Critical, out isEnabled); + + public void AppendLiteral(string s) => _core.AppendLiteral(s); + + public void AppendFormatted(T value) => _core.AppendFormatted(value); + + public void AppendFormatted(string? value) => _core.AppendFormatted(value); + + public void AppendFormatted(int value) => _core.AppendFormatted(value); + + public void AppendFormatted(long value) => _core.AppendFormatted(value); + + public void AppendFormatted(double value) => _core.AppendFormatted(value); + + public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); + + public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); + + public void AppendFormatted(T value, int alignment, string? format) => + _core.AppendFormatted(value, alignment, format); + + public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); + + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => + _core.AppendFormatted(value, alignment, format); + + public override readonly string ToString() => _core.ToString(); + + public string ToStringAndClear() => _core.ToStringAndClear(); +} diff --git a/src/EventLogExpert.Eventing/Logging/DebugLogHandler.cs b/src/EventLogExpert.Eventing/Logging/DebugLogHandler.cs new file mode 100644 index 00000000..8172a1c3 --- /dev/null +++ b/src/EventLogExpert.Eventing/Logging/DebugLogHandler.cs @@ -0,0 +1,46 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; + +namespace EventLogExpert.Eventing.Logging; + +[InterpolatedStringHandler] +public struct DebugLogHandler +{ + private LogHandlerCore _core; + + public readonly bool IsEnabled => _core.IsEnabled; + + public DebugLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => + _core.Init(literalLength, formattedCount, logger, LogLevel.Debug, out isEnabled); + + public void AppendLiteral(string s) => _core.AppendLiteral(s); + + public void AppendFormatted(T value) => _core.AppendFormatted(value); + + public void AppendFormatted(string? value) => _core.AppendFormatted(value); + + public void AppendFormatted(int value) => _core.AppendFormatted(value); + + public void AppendFormatted(long value) => _core.AppendFormatted(value); + + public void AppendFormatted(double value) => _core.AppendFormatted(value); + + public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); + + public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); + + public void AppendFormatted(T value, int alignment, string? format) => + _core.AppendFormatted(value, alignment, format); + + public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); + + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => + _core.AppendFormatted(value, alignment, format); + + public override readonly string ToString() => _core.ToString(); + + public string ToStringAndClear() => _core.ToStringAndClear(); +} diff --git a/src/EventLogExpert.Eventing/Logging/ErrorLogHandler.cs b/src/EventLogExpert.Eventing/Logging/ErrorLogHandler.cs new file mode 100644 index 00000000..9ae2a7ad --- /dev/null +++ b/src/EventLogExpert.Eventing/Logging/ErrorLogHandler.cs @@ -0,0 +1,46 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; + +namespace EventLogExpert.Eventing.Logging; + +[InterpolatedStringHandler] +public struct ErrorLogHandler +{ + private LogHandlerCore _core; + + public readonly bool IsEnabled => _core.IsEnabled; + + public ErrorLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => + _core.Init(literalLength, formattedCount, logger, LogLevel.Error, out isEnabled); + + public void AppendLiteral(string s) => _core.AppendLiteral(s); + + public void AppendFormatted(T value) => _core.AppendFormatted(value); + + public void AppendFormatted(string? value) => _core.AppendFormatted(value); + + public void AppendFormatted(int value) => _core.AppendFormatted(value); + + public void AppendFormatted(long value) => _core.AppendFormatted(value); + + public void AppendFormatted(double value) => _core.AppendFormatted(value); + + public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); + + public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); + + public void AppendFormatted(T value, int alignment, string? format) => + _core.AppendFormatted(value, alignment, format); + + public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); + + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => + _core.AppendFormatted(value, alignment, format); + + public override readonly string ToString() => _core.ToString(); + + public string ToStringAndClear() => _core.ToStringAndClear(); +} diff --git a/src/EventLogExpert.Eventing/Helpers/ITraceLogger.cs b/src/EventLogExpert.Eventing/Logging/ITraceLogger.cs similarity index 94% rename from src/EventLogExpert.Eventing/Helpers/ITraceLogger.cs rename to src/EventLogExpert.Eventing/Logging/ITraceLogger.cs index 70b0dfac..24f574c7 100644 --- a/src/EventLogExpert.Eventing/Helpers/ITraceLogger.cs +++ b/src/EventLogExpert.Eventing/Logging/ITraceLogger.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Logging; public interface ITraceLogger { diff --git a/src/EventLogExpert.Eventing/Logging/InfoLogHandler.cs b/src/EventLogExpert.Eventing/Logging/InfoLogHandler.cs new file mode 100644 index 00000000..de6b83bb --- /dev/null +++ b/src/EventLogExpert.Eventing/Logging/InfoLogHandler.cs @@ -0,0 +1,46 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; + +namespace EventLogExpert.Eventing.Logging; + +[InterpolatedStringHandler] +public struct InfoLogHandler +{ + private LogHandlerCore _core; + + public readonly bool IsEnabled => _core.IsEnabled; + + public InfoLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => + _core.Init(literalLength, formattedCount, logger, LogLevel.Information, out isEnabled); + + public void AppendLiteral(string s) => _core.AppendLiteral(s); + + public void AppendFormatted(T value) => _core.AppendFormatted(value); + + public void AppendFormatted(string? value) => _core.AppendFormatted(value); + + public void AppendFormatted(int value) => _core.AppendFormatted(value); + + public void AppendFormatted(long value) => _core.AppendFormatted(value); + + public void AppendFormatted(double value) => _core.AppendFormatted(value); + + public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); + + public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); + + public void AppendFormatted(T value, int alignment, string? format) => + _core.AppendFormatted(value, alignment, format); + + public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); + + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => + _core.AppendFormatted(value, alignment, format); + + public override readonly string ToString() => _core.ToString(); + + public string ToStringAndClear() => _core.ToStringAndClear(); +} diff --git a/src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs b/src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs new file mode 100644 index 00000000..b31b0c14 --- /dev/null +++ b/src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs @@ -0,0 +1,100 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using System.Text; + +namespace EventLogExpert.Eventing.Logging; + +internal struct LogHandlerCore +{ + [ThreadStatic] private static StringBuilder? t_pooledBuilder; + + private const int MaxPooledCapacity = 4096; + + private StringBuilder? _builder; + + internal bool IsEnabled; + + internal void Init(int literalLength, int formattedCount, ITraceLogger logger, LogLevel level, out bool isEnabled) + { + IsEnabled = isEnabled = level >= logger.MinimumLevel; + + if (!isEnabled) { return; } + + var pooled = t_pooledBuilder; + + if (pooled is not null) + { + t_pooledBuilder = null; + _builder = pooled; + } + else + { + _builder = new StringBuilder(literalLength + (formattedCount * 8)); + } + } + + internal void AppendLiteral(string s) => _builder!.Append(s); + + internal void AppendFormatted(T value) => _builder!.Append(value); + + internal void AppendFormatted(string? value) => _builder!.Append(value); + + internal void AppendFormatted(int value) => _builder!.Append(value); + + internal void AppendFormatted(long value) => _builder!.Append(value); + + internal void AppendFormatted(double value) => _builder!.Append(value); + + internal void AppendFormatted(T value, string? format) + { + if (format is null) { _builder!.Append(value); return; } + if (value is IFormattable formattable) { _builder!.Append(formattable.ToString(format, null)); return; } + _builder!.Append(value); + } + + internal void AppendFormatted(T value, int alignment) => AppendAligned(value, alignment, null); + + internal void AppendFormatted(T value, int alignment, string? format) => AppendAligned(value, alignment, format); + + internal void AppendFormatted(ReadOnlySpan value) => _builder!.Append(value); + + internal void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => + _builder!.Append(value); + + public override readonly string ToString() => _builder?.ToString() ?? string.Empty; + + internal string ToStringAndClear() + { + if (_builder is null) { return string.Empty; } + + string result = _builder.ToString(); + var consumed = _builder; + _builder = null; + + consumed.Clear(); + + if (consumed.Capacity <= MaxPooledCapacity) { t_pooledBuilder = consumed; } + + return result; + } + + private void AppendAligned(T value, int alignment, string? format) + { + string formatted = value switch + { + null => string.Empty, + IFormattable f when format is not null => f.ToString(format, null) ?? string.Empty, + _ => value.ToString() ?? string.Empty + }; + + int width = alignment == int.MinValue ? int.MaxValue : Math.Abs(alignment); + int padding = width - formatted.Length; + + if (padding <= 0) { _builder!.Append(formatted); return; } + + if (alignment >= 0) { _builder!.Append(' ', padding).Append(formatted); } + else { _builder!.Append(formatted).Append(' ', padding); } + } +} diff --git a/src/EventLogExpert.Eventing/Logging/TraceLogHandler.cs b/src/EventLogExpert.Eventing/Logging/TraceLogHandler.cs new file mode 100644 index 00000000..f07635cf --- /dev/null +++ b/src/EventLogExpert.Eventing/Logging/TraceLogHandler.cs @@ -0,0 +1,46 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; + +namespace EventLogExpert.Eventing.Logging; + +[InterpolatedStringHandler] +public struct TraceLogHandler +{ + private LogHandlerCore _core; + + public readonly bool IsEnabled => _core.IsEnabled; + + public TraceLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => + _core.Init(literalLength, formattedCount, logger, LogLevel.Trace, out isEnabled); + + public void AppendLiteral(string s) => _core.AppendLiteral(s); + + public void AppendFormatted(T value) => _core.AppendFormatted(value); + + public void AppendFormatted(string? value) => _core.AppendFormatted(value); + + public void AppendFormatted(int value) => _core.AppendFormatted(value); + + public void AppendFormatted(long value) => _core.AppendFormatted(value); + + public void AppendFormatted(double value) => _core.AppendFormatted(value); + + public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); + + public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); + + public void AppendFormatted(T value, int alignment, string? format) => + _core.AppendFormatted(value, alignment, format); + + public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); + + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => + _core.AppendFormatted(value, alignment, format); + + public override readonly string ToString() => _core.ToString(); + + public string ToStringAndClear() => _core.ToStringAndClear(); +} diff --git a/src/EventLogExpert.Eventing/Helpers/TraceLogger.cs b/src/EventLogExpert.Eventing/Logging/TraceLogger.cs similarity index 97% rename from src/EventLogExpert.Eventing/Helpers/TraceLogger.cs rename to src/EventLogExpert.Eventing/Logging/TraceLogger.cs index d4cb868e..70a09d92 100644 --- a/src/EventLogExpert.Eventing/Helpers/TraceLogger.cs +++ b/src/EventLogExpert.Eventing/Logging/TraceLogger.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging; -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Logging; public class TraceLogger(LogLevel loggingLevel) : ITraceLogger { diff --git a/src/EventLogExpert.Eventing/Logging/WarnLogHandler.cs b/src/EventLogExpert.Eventing/Logging/WarnLogHandler.cs new file mode 100644 index 00000000..97258d9b --- /dev/null +++ b/src/EventLogExpert.Eventing/Logging/WarnLogHandler.cs @@ -0,0 +1,46 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; + +namespace EventLogExpert.Eventing.Logging; + +[InterpolatedStringHandler] +public struct WarnLogHandler +{ + private LogHandlerCore _core; + + public readonly bool IsEnabled => _core.IsEnabled; + + public WarnLogHandler(int literalLength, int formattedCount, ITraceLogger logger, out bool isEnabled) => + _core.Init(literalLength, formattedCount, logger, LogLevel.Warning, out isEnabled); + + public void AppendLiteral(string s) => _core.AppendLiteral(s); + + public void AppendFormatted(T value) => _core.AppendFormatted(value); + + public void AppendFormatted(string? value) => _core.AppendFormatted(value); + + public void AppendFormatted(int value) => _core.AppendFormatted(value); + + public void AppendFormatted(long value) => _core.AppendFormatted(value); + + public void AppendFormatted(double value) => _core.AppendFormatted(value); + + public void AppendFormatted(T value, string? format) => _core.AppendFormatted(value, format); + + public void AppendFormatted(T value, int alignment) => _core.AppendFormatted(value, alignment); + + public void AppendFormatted(T value, int alignment, string? format) => + _core.AppendFormatted(value, alignment, format); + + public void AppendFormatted(ReadOnlySpan value) => _core.AppendFormatted(value); + + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => + _core.AppendFormatted(value, alignment, format); + + public override readonly string ToString() => _core.ToString(); + + public string ToStringAndClear() => _core.ToStringAndClear(); +} diff --git a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs index 2a359c2a..9a25cab1 100644 --- a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using System.Runtime.InteropServices; diff --git a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs index c82a82b3..84f66062 100644 --- a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs +++ b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs @@ -1,8 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using System.Collections.ObjectModel; diff --git a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs index f654f315..2c3fa053 100644 --- a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs @@ -1,7 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.Readers; using Microsoft.Win32; namespace EventLogExpert.Eventing.Providers; diff --git a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs index 97d5f21f..41bb31f6 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; using System.Runtime.InteropServices; diff --git a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs index a6e6a0d0..16e95443 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using System.Buffers; diff --git a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs index 55be3f82..645448af 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; using System.Runtime.InteropServices; diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index cd97f7a8..e1485b6a 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -1,7 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; using System.Runtime.InteropServices; diff --git a/src/EventLogExpert.Eventing/Helpers/LogNames.cs b/src/EventLogExpert.Eventing/Readers/LogNames.cs similarity index 93% rename from src/EventLogExpert.Eventing/Helpers/LogNames.cs rename to src/EventLogExpert.Eventing/Readers/LogNames.cs index 74996b29..5cdd64e8 100644 --- a/src/EventLogExpert.Eventing/Helpers/LogNames.cs +++ b/src/EventLogExpert.Eventing/Readers/LogNames.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.Eventing.Helpers; +namespace EventLogExpert.Eventing.Readers; public static class LogNames { diff --git a/src/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs b/src/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs index 77db1a10..95025611 100644 --- a/src/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs b/src/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs index 48333fc6..d3580b67 100644 --- a/src/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Options; diff --git a/src/EventLogExpert.UI.Tests/Services/DeploymentServiceTests.cs b/src/EventLogExpert.UI.Tests/Services/DeploymentServiceTests.cs index 6f24114b..fefecd8c 100644 --- a/src/EventLogExpert.UI.Tests/Services/DeploymentServiceTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/DeploymentServiceTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert.UI.Tests/Services/GitHubServiceTests.cs b/src/EventLogExpert.UI.Tests/Services/GitHubServiceTests.cs index 428a1a85..cc5cc109 100644 --- a/src/EventLogExpert.UI.Tests/Services/GitHubServiceTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/GitHubServiceTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI.Tests/Services/UpdateServiceTests.cs b/src/EventLogExpert.UI.Tests/Services/UpdateServiceTests.cs index 00ee0c02..22f524b4 100644 --- a/src/EventLogExpert.UI.Tests/Services/UpdateServiceTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/UpdateServiceTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs index faf25f60..00178deb 100644 --- a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs +++ b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs b/src/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs index e3dcd295..2dddfbf2 100644 --- a/src/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs +++ b/src/EventLogExpert.UI.Tests/TestUtils/DatabaseSeedUtils.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventProviderDatabase; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using Microsoft.Data.Sqlite; namespace EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI.Tests/TestUtils/LoggerUtils.cs b/src/EventLogExpert.UI.Tests/TestUtils/LoggerUtils.cs index 3492a388..6c0dc7e1 100644 --- a/src/EventLogExpert.UI.Tests/TestUtils/LoggerUtils.cs +++ b/src/EventLogExpert.UI.Tests/TestUtils/LoggerUtils.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using Microsoft.Extensions.Logging; namespace EventLogExpert.UI.Tests.TestUtils; diff --git a/src/EventLogExpert.UI/LogNameMethods.cs b/src/EventLogExpert.UI/LogNameMethods.cs index 02e6524c..75e75c94 100644 --- a/src/EventLogExpert.UI/LogNameMethods.cs +++ b/src/EventLogExpert.UI/LogNameMethods.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; namespace EventLogExpert.UI; diff --git a/src/EventLogExpert.UI/Services/ApplicationRestartService.cs b/src/EventLogExpert.UI/Services/ApplicationRestartService.cs index 0d1cf659..2cb55a41 100644 --- a/src/EventLogExpert.UI/Services/ApplicationRestartService.cs +++ b/src/EventLogExpert.UI/Services/ApplicationRestartService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using Windows.ApplicationModel.Core; diff --git a/src/EventLogExpert.UI/Services/BannerService.cs b/src/EventLogExpert.UI/Services/BannerService.cs index 8d64632b..3ee4024d 100644 --- a/src/EventLogExpert.UI/Services/BannerService.cs +++ b/src/EventLogExpert.UI/Services/BannerService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using System.Collections.Immutable; diff --git a/src/EventLogExpert.UI/Services/DatabaseService.cs b/src/EventLogExpert.UI/Services/DatabaseService.cs index 4338680e..bddb8e92 100644 --- a/src/EventLogExpert.UI/Services/DatabaseService.cs +++ b/src/EventLogExpert.UI/Services/DatabaseService.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.EventProviderDatabase; using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Options; diff --git a/src/EventLogExpert.UI/Services/DebugLogService.cs b/src/EventLogExpert.UI/Services/DebugLogService.cs index 82a99584..2678b837 100644 --- a/src/EventLogExpert.UI/Services/DebugLogService.cs +++ b/src/EventLogExpert.UI/Services/DebugLogService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using Microsoft.Extensions.Logging; diff --git a/src/EventLogExpert.UI/Services/DeploymentService.cs b/src/EventLogExpert.UI/Services/DeploymentService.cs index 1685bf47..904a76e3 100644 --- a/src/EventLogExpert.UI/Services/DeploymentService.cs +++ b/src/EventLogExpert.UI/Services/DeploymentService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using System.Reflection; diff --git a/src/EventLogExpert.UI/Services/GitHubService.cs b/src/EventLogExpert.UI/Services/GitHubService.cs index ffefe60f..f3981b84 100644 --- a/src/EventLogExpert.UI/Services/GitHubService.cs +++ b/src/EventLogExpert.UI/Services/GitHubService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Models; using System.Text.Json; diff --git a/src/EventLogExpert.UI/Services/UpdateService.cs b/src/EventLogExpert.UI/Services/UpdateService.cs index 35a67ab4..2f31de49 100644 --- a/src/EventLogExpert.UI/Services/UpdateService.cs +++ b/src/EventLogExpert.UI/Services/UpdateService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs index 2f2da333..bb87a458 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs +++ b/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert.UI/Store/EventLog/LiveLogWatcherService.cs b/src/EventLogExpert.UI/Store/EventLog/LiveLogWatcherService.cs index 08b7bdfa..fdc0f7ec 100644 --- a/src/EventLogExpert.UI/Store/EventLog/LiveLogWatcherService.cs +++ b/src/EventLogExpert.UI/Store/EventLog/LiveLogWatcherService.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; using Fluxor; using Microsoft.Extensions.DependencyInjection; diff --git a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs b/src/EventLogExpert.UI/Store/LoggingMiddleware.cs index 1bb573e3..2b3bbaed 100644 --- a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs +++ b/src/EventLogExpert.UI/Store/LoggingMiddleware.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; diff --git a/src/EventLogExpert/App.xaml.cs b/src/EventLogExpert/App.xaml.cs index 1046e709..e811c091 100644 --- a/src/EventLogExpert/App.xaml.cs +++ b/src/EventLogExpert/App.xaml.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Services; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert/Components/Layout/UnhandledExceptionHandler.razor.cs b/src/EventLogExpert/Components/Layout/UnhandledExceptionHandler.razor.cs index 8988b34c..9df2fba0 100644 --- a/src/EventLogExpert/Components/Layout/UnhandledExceptionHandler.razor.cs +++ b/src/EventLogExpert/Components/Layout/UnhandledExceptionHandler.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; diff --git a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs index 6df2af59..65644ec4 100644 --- a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert/Components/Sections/EventTable.razor.cs b/src/EventLogExpert/Components/Sections/EventTable.razor.cs index c7407ccf..c254b5e6 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor.cs +++ b/src/EventLogExpert/Components/Sections/EventTable.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert/MainPage.xaml.cs b/src/EventLogExpert/MainPage.xaml.cs index d278562f..e24aaf58 100644 --- a/src/EventLogExpert/MainPage.xaml.cs +++ b/src/EventLogExpert/MainPage.xaml.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; using EventLogExpert.Services; using EventLogExpert.UI; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 21bb36b9..4f55690f 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -3,7 +3,7 @@ using EventLogExpert.Components.Modals.Alerts; using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Services; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; diff --git a/src/EventLogExpert/Services/ClipboardService.cs b/src/EventLogExpert/Services/ClipboardService.cs index b8e01fbc..78997430 100644 --- a/src/EventLogExpert/Services/ClipboardService.cs +++ b/src/EventLogExpert/Services/ClipboardService.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.EventResolvers; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Models; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index f381b5d5..14f8124e 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -3,7 +3,7 @@ using EventLogExpert.Components.Modals; using EventLogExpert.Components.Modals.Filters; -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; using EventLogExpert.Platforms.Windows; using EventLogExpert.UI; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index bfb0d2fc..53052354 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -17,7 +17,6 @@ @using EventLogExpert.Components.Modals.Alerts @using EventLogExpert.Components.Modals.Filters @using EventLogExpert.Components.Sections -@using EventLogExpert.Eventing.Helpers @using EventLogExpert.Eventing.Readers @using EventLogExpert.UI @using EventLogExpert.UI.Models From 7b29a4641a7db0960389effb10851f81f1888706 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 11:50:14 -0500 Subject: [PATCH 08/24] Internalize Eventing surface and seal EventXmlResolver --- .../EventResolvers/EventXmlResolverTests.cs | 127 ++++++++++-------- .../CompressedJsonValueConverter.cs | 2 +- .../JsonValueConverter.cs | 24 ---- .../EventResolvers/EventXmlResolver.cs | 48 ++++--- .../Providers/EventMessageProvider.cs | 2 +- .../Providers/RegistryProvider.cs | 2 +- 6 files changed, 106 insertions(+), 99 deletions(-) delete mode 100644 src/EventLogExpert.Eventing/EventProviderDatabase/JsonValueConverter.cs diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs index c16b9fd9..616d9b04 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs @@ -12,22 +12,24 @@ public sealed class EventXmlResolverTests [Fact] public async Task ClearAll_RemovesEveryEntry() { - var resolver = new TrackingResolver(_ => ""); + var resolver = CreateTrackingResolver(_ => "", out var getResolveCallCount); var evt = CreateEvent(recordId: 1); await resolver.GetXmlAsync(evt, TestContext.Current.CancellationToken); - Assert.Equal(1, resolver.ResolveCallCount); + Assert.Equal(1, getResolveCallCount()); resolver.ClearAll(); await resolver.GetXmlAsync(evt, TestContext.Current.CancellationToken); - Assert.Equal(2, resolver.ResolveCallCount); + Assert.Equal(2, getResolveCallCount()); } [Fact] public async Task ClearLog_RemovesEntriesForThatLogOnly() { - var resolver = new TrackingResolver(key => $""); + var resolver = CreateTrackingResolver( + key => $"", + out var getResolveCallCount); var evtA1 = CreateEvent(recordId: 1, owningLog: "A"); var evtA2 = CreateEvent(recordId: 2, owningLog: "A"); @@ -36,25 +38,29 @@ public async Task ClearLog_RemovesEntriesForThatLogOnly() await resolver.GetXmlAsync(evtA1, TestContext.Current.CancellationToken); await resolver.GetXmlAsync(evtA2, TestContext.Current.CancellationToken); await resolver.GetXmlAsync(evtB1, TestContext.Current.CancellationToken); - Assert.Equal(3, resolver.ResolveCallCount); + Assert.Equal(3, getResolveCallCount()); resolver.ClearLog("A"); // B is untouched. await resolver.GetXmlAsync(evtB1, TestContext.Current.CancellationToken); - Assert.Equal(3, resolver.ResolveCallCount); + Assert.Equal(3, getResolveCallCount()); // A entries were evicted; both are re-resolved. await resolver.GetXmlAsync(evtA1, TestContext.Current.CancellationToken); await resolver.GetXmlAsync(evtA2, TestContext.Current.CancellationToken); - Assert.Equal(5, resolver.ResolveCallCount); + Assert.Equal(5, getResolveCallCount()); } [Fact] public async Task GetXmlAsync_AtCapacity_EvictsLeastRecentlyUsed() { // Initial = max = 2, so eviction kicks in once we exceed 2 entries. - var resolver = new BoundedTrackingResolver(initialCapacity: 2, maxCapacity: 2); + var resolver = CreateBoundedTrackingResolver( + key => $"", + out var getResolveCallCount, + initialCapacity: 2, + maxCapacity: 2); var evtA = CreateEvent(recordId: 1, owningLog: "A"); var evtB = CreateEvent(recordId: 2, owningLog: "B"); @@ -69,23 +75,25 @@ public async Task GetXmlAsync_AtCapacity_EvictsLeastRecentlyUsed() // A and C should still be cached (B was evicted as LRU); A re-request must not re-resolve. await resolver.GetXmlAsync(evtA, TestContext.Current.CancellationToken); await resolver.GetXmlAsync(evtC, TestContext.Current.CancellationToken); - Assert.Equal(3, resolver.ResolveCallCount); + Assert.Equal(3, getResolveCallCount()); // Re-requesting B triggers another resolve since it was evicted. await resolver.GetXmlAsync(evtB, TestContext.Current.CancellationToken); - Assert.Equal(4, resolver.ResolveCallCount); + Assert.Equal(4, getResolveCallCount()); } [Fact] public async Task GetXmlAsync_ConcurrentRequestsForSameKey_ResolveOnce() { using var gate = new ManualResetEventSlim(initialState: false); - var resolver = new TrackingResolver(_ => - { - gate.Wait(TimeSpan.FromSeconds(5)); + var resolver = CreateTrackingResolver( + _ => + { + gate.Wait(TimeSpan.FromSeconds(5)); - return ""; - }); + return ""; + }, + out var getResolveCallCount); var evt = CreateEvent(recordId: 7); @@ -97,13 +105,15 @@ public async Task GetXmlAsync_ConcurrentRequestsForSameKey_ResolveOnce() var results = await Task.WhenAll(t1, t2, t3); Assert.All(results, r => Assert.Equal("", r)); - Assert.Equal(1, resolver.ResolveCallCount); + Assert.Equal(1, getResolveCallCount()); } [Fact] public async Task GetXmlAsync_OnCacheMiss_ResolvesAndCachesResult() { - var resolver = new TrackingResolver(key => $""); + var resolver = CreateTrackingResolver( + key => $"", + out var getResolveCallCount); var evt = CreateEvent(recordId: 42); var first = await resolver.GetXmlAsync(evt, TestContext.Current.CancellationToken); @@ -111,55 +121,57 @@ public async Task GetXmlAsync_OnCacheMiss_ResolvesAndCachesResult() Assert.Equal("", first); Assert.Equal("", second); - Assert.Equal(1, resolver.ResolveCallCount); + Assert.Equal(1, getResolveCallCount()); } [Fact] public async Task GetXmlAsync_WhenEventHasPreRenderedXml_ReturnsItWithoutResolving() { - var resolver = new TrackingResolver(_ => "should-not-be-called"); + var resolver = CreateTrackingResolver(_ => "should-not-be-called", out var getResolveCallCount); var evt = CreateEvent(recordId: 1) with { Xml = "" }; var result = await resolver.GetXmlAsync(evt, TestContext.Current.CancellationToken); Assert.Equal("", result); - Assert.Equal(0, resolver.ResolveCallCount); + Assert.Equal(0, getResolveCallCount()); } [Fact] public async Task GetXmlAsync_WhenOwningLogIsEmpty_ReturnsEmptyWithoutResolving() { - var resolver = new TrackingResolver(_ => "x"); + var resolver = CreateTrackingResolver(_ => "x", out var getResolveCallCount); var evt = CreateEvent(recordId: 1, owningLog: string.Empty); var result = await resolver.GetXmlAsync(evt, TestContext.Current.CancellationToken); Assert.Equal(string.Empty, result); - Assert.Equal(0, resolver.ResolveCallCount); + Assert.Equal(0, getResolveCallCount()); } [Fact] public async Task GetXmlAsync_WhenRecordIdIsNull_ReturnsEmptyWithoutResolving() { - var resolver = new TrackingResolver(_ => "x"); + var resolver = CreateTrackingResolver(_ => "x", out var getResolveCallCount); var evt = CreateEvent(recordId: null); var result = await resolver.GetXmlAsync(evt, TestContext.Current.CancellationToken); Assert.Equal(string.Empty, result); - Assert.Equal(0, resolver.ResolveCallCount); + Assert.Equal(0, getResolveCallCount()); } [Fact] public async Task GetXmlAsync_WhenResolveThrows_EvictsEntryAndAllowsRetry() { int callCount = 0; - var resolver = new TrackingResolver(_ => - { - callCount++; + var resolver = CreateTrackingResolver( + _ => + { + callCount++; - return callCount == 1 ? throw new InvalidOperationException("boom") : ""; - }); + return callCount == 1 ? throw new InvalidOperationException("boom") : ""; + }, + out _); var evt = CreateEvent(recordId: 99); @@ -171,6 +183,27 @@ await Assert.ThrowsAsync( Assert.Equal(2, callCount); } + private static EventXmlResolver CreateBoundedTrackingResolver( + Func resolve, + out Func getResolveCallCount, + int initialCapacity, + int maxCapacity) + { + int resolveCount = 0; + + getResolveCallCount = () => Volatile.Read(ref resolveCount); + + return new EventXmlResolver( + (owningLog, recordId, pathType) => + { + Interlocked.Increment(ref resolveCount); + + return resolve(new ResolveKey(owningLog, recordId, pathType)); + }, + initialCapacity, + maxCapacity); + } + private static DisplayEventModel CreateEvent(long? recordId, string owningLog = "TestLog") => new(owningLog, PathType.LogName) { @@ -178,34 +211,22 @@ private static DisplayEventModel CreateEvent(long? recordId, string owningLog = Xml = string.Empty }; - private sealed class BoundedTrackingResolver(int initialCapacity, int maxCapacity) - : EventXmlResolver(initialCapacity, maxCapacity) + private static EventXmlResolver CreateTrackingResolver( + Func resolve, + out Func getResolveCallCount) { - private int _resolveCallCount; + int resolveCount = 0; - public int ResolveCallCount => Volatile.Read(ref _resolveCallCount); + getResolveCallCount = () => Volatile.Read(ref resolveCount); - protected override string ResolveXml(string owningLog, long recordId, PathType pathType) - { - Interlocked.Increment(ref _resolveCallCount); - - return $""; - } - } - - private class TrackingResolver(Func resolve) : EventXmlResolver - { - private int _resolveCallCount; - - public int ResolveCallCount => Volatile.Read(ref _resolveCallCount); - - protected override string ResolveXml(string owningLog, long recordId, PathType pathType) - { - Interlocked.Increment(ref _resolveCallCount); + return new EventXmlResolver( + (owningLog, recordId, pathType) => + { + Interlocked.Increment(ref resolveCount); - return resolve(new ResolveKey(owningLog, recordId, pathType)); - } + return resolve(new ResolveKey(owningLog, recordId, pathType)); + }); } - private sealed record ResolveKey(string OwningLog, long RecordId, PathType PathType); + private readonly record struct ResolveKey(string OwningLog, long RecordId, PathType PathType); } diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/CompressedJsonValueConverter.cs b/src/EventLogExpert.Eventing/EventProviderDatabase/CompressedJsonValueConverter.cs index e668eaad..ff31f2b7 100644 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/CompressedJsonValueConverter.cs +++ b/src/EventLogExpert.Eventing/EventProviderDatabase/CompressedJsonValueConverter.cs @@ -7,7 +7,7 @@ namespace EventLogExpert.Eventing.EventProviderDatabase; -public class CompressedJsonValueConverter() : ValueConverter(v => ConvertToCompressedJson(v), +internal sealed class CompressedJsonValueConverter() : ValueConverter(v => ConvertToCompressedJson(v), v => ConvertFromCompressedJson(v)) where T : class { diff --git a/src/EventLogExpert.Eventing/EventProviderDatabase/JsonValueConverter.cs b/src/EventLogExpert.Eventing/EventProviderDatabase/JsonValueConverter.cs deleted file mode 100644 index 8cf43839..00000000 --- a/src/EventLogExpert.Eventing/EventProviderDatabase/JsonValueConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using System.Text.Json; - -namespace EventLogExpert.Eventing.EventProviderDatabase; - -public class JsonValueConverter : ValueConverter where T : class -{ - public JsonValueConverter() : - base(v => ConvertToJson(v), v => ConvertFromJson(v)) - { } - - private static string ConvertToJson(T value) - { - return JsonSerializer.Serialize(value); - } - - private static T ConvertFromJson(string value) - { - return JsonSerializer.Deserialize(value)!; - } -} diff --git a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs index 1be13ce7..264ba890 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs @@ -14,25 +14,36 @@ namespace EventLogExpert.Eventing.EventResolvers; /// per cache entry. Eviction is silent: a request for an evicted key triggers a fresh /// resolve and re-cache. /// -public class EventXmlResolver : IEventXmlResolver +public sealed class EventXmlResolver : IEventXmlResolver { private const int DefaultInitialCapacity = 256; private const int DefaultMaxCapacity = 4096; + private static readonly Func s_defaultResolveStrategyFunc = DefaultResolveStrategy; + private readonly Dictionary> _cache; private readonly Lock _cacheLock = new(); private readonly LinkedList _lruOrder = []; private readonly int _maxCapacity; + private readonly Func _resolveStrategy; private int _capacity; - public EventXmlResolver() : this(DefaultInitialCapacity, DefaultMaxCapacity) { } + public EventXmlResolver() : this(s_defaultResolveStrategyFunc) { } + + internal EventXmlResolver(Func resolveStrategy) + : this(resolveStrategy, DefaultInitialCapacity, DefaultMaxCapacity) { } - protected EventXmlResolver(int initialCapacity, int maxCapacity) + internal EventXmlResolver( + Func resolveStrategy, + int initialCapacity, + int maxCapacity) { + ArgumentNullException.ThrowIfNull(resolveStrategy); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialCapacity); ArgumentOutOfRangeException.ThrowIfLessThan(maxCapacity, initialCapacity); + _resolveStrategy = resolveStrategy; _capacity = initialCapacity; _maxCapacity = maxCapacity; _cache = new Dictionary>(initialCapacity); @@ -91,8 +102,20 @@ public ValueTask GetXmlAsync(DisplayEventModel evt, CancellationToken ca return AwaitLazyAsync(lazy, cancellationToken); } - /// Performs the actual EvtQuery / EvtNext / RenderEventXml work. Virtual for testability. - protected virtual string ResolveXml(string owningLog, long recordId, PathType pathType) + private static async ValueTask AwaitLazyAsync(Lazy> lazy, CancellationToken cancellationToken) + { + try + { + return await lazy.Value.WaitAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Caller-side cancel: leave the entry in place so other waiters can observe the result. + throw; + } + } + + private static string DefaultResolveStrategy(string owningLog, long recordId, PathType pathType) { using EvtHandle handle = NativeMethods.EvtQuery( EventLogSession.GlobalSession.Handle, @@ -116,19 +139,6 @@ protected virtual string ResolveXml(string owningLog, long recordId, PathType pa return NativeMethods.RenderEventXml(eventHandle) ?? string.Empty; } - private static async ValueTask AwaitLazyAsync(Lazy> lazy, CancellationToken cancellationToken) - { - try - { - return await lazy.Value.WaitAsync(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Caller-side cancel: leave the entry in place so other waiters can observe the result. - throw; - } - } - private void EnsureCapacity() { if (_cache.Count <= _capacity) { return; } @@ -191,7 +201,7 @@ private async Task ResolveAndEvictOnFailureAsync(XmlCacheKey key) { try { - return await Task.Run(() => ResolveXml(key.OwningLog, key.RecordId, key.PathType)).ConfigureAwait(false); + return await Task.Run(() => _resolveStrategy(key.OwningLog, key.RecordId, key.PathType)).ConfigureAwait(false); } catch { diff --git a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs index 9a25cab1..aa015f62 100644 --- a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs @@ -26,7 +26,7 @@ public sealed class EventMessageProvider( private RegistryProvider? _registryProvider; - public static List GetMessages( + internal static List GetMessages( IEnumerable legacyProviderFiles, string providerName, ITraceLogger? logger = null) diff --git a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs index 2c3fa053..7011d558 100644 --- a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs @@ -7,7 +7,7 @@ namespace EventLogExpert.Eventing.Providers; -public class RegistryProvider(ITraceLogger? logger = null) +internal sealed class RegistryProvider(ITraceLogger? logger = null) { private readonly ITraceLogger? _logger = logger; From e9ac30515aabf5ecede9d7677ad59f0c884abb5d Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 12:06:54 -0500 Subject: [PATCH 09/24] Migrate Eventing InternalsVisibleTo to csproj --- src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj | 4 ++++ src/EventLogExpert.Eventing/Properties/AssemblyInfo.cs | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 src/EventLogExpert.Eventing/Properties/AssemblyInfo.cs diff --git a/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj b/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj index a7a8991e..fb0b03dd 100644 --- a/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj +++ b/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/src/EventLogExpert.Eventing/Properties/AssemblyInfo.cs b/src/EventLogExpert.Eventing/Properties/AssemblyInfo.cs deleted file mode 100644 index 51095226..00000000 --- a/src/EventLogExpert.Eventing/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("EventLogExpert.Eventing.Tests")] From c380b4b962821587f714d7a62dec4c5435fe3c7b Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 12:19:58 -0500 Subject: [PATCH 10/24] Move Eventing.Tests Helpers tests into Interop and Readers --- .../{Helpers => Interop}/NativeErrorResolverTests.cs | 2 +- .../EventMethodsTests.cs => Interop/NativeMethodsEvtTests.cs} | 4 ++-- .../{Helpers => Interop}/NativeMethodsTests.cs | 2 +- .../{Helpers => Readers}/LogNamesTests.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/EventLogExpert.Eventing.Tests/{Helpers => Interop}/NativeErrorResolverTests.cs (97%) rename src/EventLogExpert.Eventing.Tests/{Helpers/EventMethodsTests.cs => Interop/NativeMethodsEvtTests.cs} (99%) rename src/EventLogExpert.Eventing.Tests/{Helpers => Interop}/NativeMethodsTests.cs (96%) rename src/EventLogExpert.Eventing.Tests/{Helpers => Readers}/LogNamesTests.cs (95%) diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/NativeErrorResolverTests.cs b/src/EventLogExpert.Eventing.Tests/Interop/NativeErrorResolverTests.cs similarity index 97% rename from src/EventLogExpert.Eventing.Tests/Helpers/NativeErrorResolverTests.cs rename to src/EventLogExpert.Eventing.Tests/Interop/NativeErrorResolverTests.cs index 26ce5bef..c50f9fcf 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/NativeErrorResolverTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Interop/NativeErrorResolverTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.Interop; -namespace EventLogExpert.Eventing.Tests.Helpers; +namespace EventLogExpert.Eventing.Tests.Interop; public sealed class NativeErrorResolverTests { diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs similarity index 99% rename from src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs rename to src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs index b5bf8e4e..062ebd08 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs @@ -5,9 +5,9 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace EventLogExpert.Eventing.Tests.Helpers; +namespace EventLogExpert.Eventing.Tests.Interop; -public sealed class EventMethodsTests +public sealed class NativeMethodsEvtTests { [Fact] public void ConvertVariant_WhenAnsiString_ShouldReturnString() diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsTests.cs similarity index 96% rename from src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs rename to src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsTests.cs index c0b5c9c4..8c83a31c 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.Interop; -namespace EventLogExpert.Eventing.Tests.Helpers; +namespace EventLogExpert.Eventing.Tests.Interop; public sealed class NativeMethodsTests { diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs similarity index 95% rename from src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs rename to src/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs index 8f92ca4b..47d0aa7a 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.Readers; -namespace EventLogExpert.Eventing.Tests.Helpers; +namespace EventLogExpert.Eventing.Tests.Readers; public sealed class LogNamesTests { From 85f2a16cbc3f2843582921d4f91e19946b095213 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 13:34:42 -0500 Subject: [PATCH 11/24] Remove tautological and vacuous tests from Eventing.Tests --- .../Providers/ProviderMetadataTests.cs | 99 ------------------- .../Readers/EventLogInformationTests.cs | 65 ------------ .../Readers/EventLogSessionTests.cs | 44 --------- 3 files changed, 208 deletions(-) diff --git a/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs index e4eaaca1..772390d6 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs @@ -2,7 +2,6 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; using NSubstitute; @@ -99,20 +98,6 @@ public void Channels_WhenValidProvider_ShouldContainData() Assert.NotEmpty(channels); } - [Fact] - public void Channels_WhenValidProvider_ShouldReturnDictionary() - { - // Arrange - var metadata = ProviderMetadata.Create(Constants.SecurityAuditingLogName); - - // Act - var channels = metadata?.Channels; - - // Assert - Assert.NotNull(channels); - Assert.IsAssignableFrom>(channels); - } - [Fact] public void Channels_WhenValidProvider_ShouldReturnReadOnlyDictionary() { @@ -127,19 +112,6 @@ public void Channels_WhenValidProvider_ShouldReturnReadOnlyDictionary() Assert.IsAssignableFrom>(channels); } - [Fact] - public void Create_WhenCalledConcurrently_ShouldHandleMultipleInstances() - { - // Arrange & Act - var metadata1 = ProviderMetadata.Create(Constants.SecurityAuditingLogName); - var metadata2 = ProviderMetadata.Create(Constants.SecurityAuditingLogName); - - // Assert - Assert.NotNull(metadata1); - Assert.NotNull(metadata2); - Assert.NotSame(metadata1, metadata2); - } - [Theory] [InlineData(Constants.SecurityAuditingLogName)] [InlineData(Constants.KernelGeneralLogName)] @@ -275,21 +247,6 @@ public void Error_WhenInvalidProvider_ShouldContainErrorMessage() .Debug(Arg.Is(h => !string.IsNullOrEmpty(h.ToString()))); } - [Fact] - public void Events_WhenCalledTwice_ShouldReturnDifferentEnumerableInstances() - { - // Arrange - var metadata = ProviderMetadata.Create(Constants.SecurityAuditingLogName); - - // Act - var events1 = metadata?.Events; - var events2 = metadata?.Events; - - // Assert - Assert.NotNull(events1); - Assert.NotNull(events2); - } - [Fact] public void Events_WhenProviderHasEvents_ShouldHaveValidEventMetadata() { @@ -345,20 +302,6 @@ public void Events_WhenValidProvider_ShouldContainEvents() Assert.NotEmpty(events); } - [Fact] - public void Events_WhenValidProvider_ShouldReturnEnumerable() - { - // Arrange - var metadata = ProviderMetadata.Create(Constants.SecurityAuditingLogName); - - // Act - var events = metadata?.Events; - - // Assert - Assert.NotNull(events); - Assert.IsAssignableFrom>(events); - } - [Fact] public async Task Keywords_WhenAccessedConcurrently_ShouldReturnValidData() { @@ -452,20 +395,6 @@ public void Keywords_WhenProviderHasNoDuplicateKeywordValues_ShouldNotLoseData() Assert.Equal(keywords.Count, uniqueValues); } - [Fact] - public void Keywords_WhenValidProvider_ShouldReturnDictionary() - { - // Arrange - var metadata = ProviderMetadata.Create(Constants.SecurityAuditingLogName); - - // Act - var keywords = metadata?.Keywords; - - // Assert - Assert.NotNull(keywords); - Assert.IsAssignableFrom>(keywords); - } - [Fact] public void Keywords_WhenValidProvider_ShouldReturnReadOnlyDictionary() { @@ -640,20 +569,6 @@ public void Opcodes_WhenValidProvider_ShouldContainData() Assert.NotEmpty(opcodes); } - [Fact] - public void Opcodes_WhenValidProvider_ShouldReturnDictionary() - { - // Arrange - var metadata = ProviderMetadata.Create(Constants.SecurityAuditingLogName); - - // Act - var opcodes = metadata?.Opcodes; - - // Assert - Assert.NotNull(opcodes); - Assert.IsAssignableFrom>(opcodes); - } - [Fact] public void Opcodes_WhenValidProvider_ShouldReturnReadOnlyDictionary() { @@ -800,20 +715,6 @@ public void Tasks_WhenValidProvider_ShouldContainData() Assert.NotEmpty(tasks); } - [Fact] - public void Tasks_WhenValidProvider_ShouldReturnDictionary() - { - // Arrange - var metadata = ProviderMetadata.Create(Constants.SecurityAuditingLogName); - - // Act - var tasks = metadata?.Tasks; - - // Assert - Assert.NotNull(tasks); - Assert.IsAssignableFrom>(tasks); - } - [Fact] public void Tasks_WhenValidProvider_ShouldReturnReadOnlyDictionary() { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs index 5821567f..55335662 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs @@ -8,20 +8,6 @@ namespace EventLogExpert.Eventing.Tests.Readers; public sealed class EventLogInformationTests { - [Fact] - public void Attributes_WhenApplicationLog_ShouldBeValidInteger() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); - - // Assert - Assert.NotNull(logInfo.Attributes); - Assert.IsType(logInfo.Attributes.Value); - } - [Fact] public void Constructor_WhenApplicationLog_ShouldPopulateAllProperties() { @@ -39,27 +25,6 @@ public void Constructor_WhenApplicationLog_ShouldPopulateAllProperties() Assert.NotNull(logInfo.RecordCount); } - [Fact] - public void Constructor_WhenCalledMultipleTimes_ShouldCreateIndependentInstances() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act - var logInfo1 = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); - var logInfo2 = new EventLogInformation(session, Constants.SystemLogName, PathType.LogName); - - // Assert - Assert.NotNull(logInfo1); - Assert.NotNull(logInfo2); - Assert.NotSame(logInfo1, logInfo2); - - // Different logs should have potentially different record counts - // (though we can't guarantee they're different) - Assert.NotNull(logInfo1.RecordCount); - Assert.NotNull(logInfo2.RecordCount); - } - [Theory] [InlineData(Constants.ApplicationLogName)] [InlineData(Constants.SystemLogName)] @@ -314,22 +279,6 @@ public void FileSize_WhenMultipleLogs_ShouldVaryByLog() Assert.True(sysLogInfo.FileSize > 0); } - [Fact] - public void GetLogInformation_WhenCalledTwice_ShouldReturnDifferentInstances() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act - var logInfo1 = session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); - var logInfo2 = session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); - - // Assert - Assert.NotNull(logInfo1); - Assert.NotNull(logInfo2); - Assert.NotSame(logInfo1, logInfo2); - } - [Fact] public void GetLogInformation_WhenCalledTwice_ShouldReturnSimilarData() { @@ -364,20 +313,6 @@ public void IsLogFull_WhenApplicationLog_ShouldBeFalse() Assert.False(logInfo.IsLogFull.Value); } - [Fact] - public void IsLogFull_WhenValidLog_ShouldReturnBoolean() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act - var logInfo = new EventLogInformation(session, Constants.ApplicationLogName, PathType.LogName); - - // Assert - Assert.NotNull(logInfo.IsLogFull); - Assert.IsType(logInfo.IsLogFull.Value); - } - [Fact] public void LastAccessTime_WhenApplicationLog_ShouldBeValidOrNull() { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs index bd8a3b14..768a585c 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs @@ -22,22 +22,6 @@ public void GetLogInformation_WhenApplicationLog_ShouldReturnInformation() Assert.NotNull(logInfo.RecordCount); } - [Fact] - public void GetLogInformation_WhenCalledMultipleTimes_ShouldReturnDifferentInstances() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act - var logInfo1 = session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); - var logInfo2 = session.GetLogInformation(Constants.ApplicationLogName, PathType.LogName); - - // Assert - Assert.NotNull(logInfo1); - Assert.NotNull(logInfo2); - Assert.NotSame(logInfo1, logInfo2); - } - [Theory] [InlineData(Constants.ApplicationLogName)] [InlineData(Constants.SystemLogName)] @@ -177,20 +161,6 @@ public void GetLogNames_WhenCalled_ShouldReturnAtLeast50Logs() Assert.True(logNames.Count >= 50, $"Expected at least 50 logs, but got {logNames.Count}"); } - [Fact] - public void GetLogNames_WhenCalled_ShouldReturnEnumerable() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act - var logNames = session.GetLogNames(); - - // Assert - Assert.NotNull(logNames); - Assert.IsAssignableFrom>(logNames); - } - [Fact] public void GetLogNames_WhenCalled_ShouldReturnLogNames() { @@ -328,20 +298,6 @@ public void GetProviderNames_WhenCalled_ShouldNotContainDuplicates() Assert.Equal(providersList.Count, uniqueProviders.Count); } - [Fact] - public void GetProviderNames_WhenCalled_ShouldReturnHashSet() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act - var providers = session.GetProviderNames(); - - // Assert - Assert.NotNull(providers); - Assert.IsAssignableFrom>(providers); - } - [Fact] public void GetProviderNames_WhenCalled_ShouldReturnManyProviders() { From 31ef59903fa4355aa6e21f56a97615827c41ce76 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 14:17:42 -0500 Subject: [PATCH 12/24] Replace EventLogWatcher test sleeps with signals and cancellable delays --- .../Readers/EventLogWatcherTests.cs | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs index 0cd72df5..04aed6d1 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs @@ -133,8 +133,14 @@ public void Dispose_AfterDispose_ShouldNotReceiveEvents() // Arrange var watcher = new EventLogWatcher(Constants.ApplicationLogName); var receivedEvents = new List(); + var eventReceived = new ManualResetEventSlim(false); + + watcher.EventRecordWritten += (sender, record) => + { + receivedEvents.Add(record); + eventReceived.Set(); + }; - watcher.EventRecordWritten += (sender, record) => receivedEvents.Add(record); watcher.Enabled = true; // Act @@ -144,9 +150,10 @@ public void Dispose_AfterDispose_ShouldNotReceiveEvents() eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event after dispose", EventLogEntryType.Information); - Thread.Sleep(200); // Wait to ensure event would have been received if subscription was active + bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken); // Assert + Assert.False(received, "Should not have received any event after dispose"); Assert.Empty(receivedEvents); } @@ -378,16 +385,27 @@ public void EventRecordWritten_AfterUnsubscribe_ShouldStopReceivingEvents() // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); var receivedEvents = new List(); + var eventReceived = new ManualResetEventSlim(false); + + watcher.EventRecordWritten += (sender, record) => + { + receivedEvents.Add(record); + eventReceived.Set(); + }; - watcher.EventRecordWritten += (sender, record) => receivedEvents.Add(record); watcher.Enabled = true; // Act watcher.Enabled = false; int countBeforeWait = receivedEvents.Count; - Thread.Sleep(100); // Brief wait to ensure no new events arrive + + // Safe: EventLogWatcher.Unsubscribe blocks until in-flight callbacks complete, + // so no handler can fire between the count capture and the Reset/Wait below. + eventReceived.Reset(); + bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken); // Assert + Assert.False(received, "Should not have received any event after unsubscribe"); Assert.Equal(countBeforeWait, receivedEvents.Count); } @@ -543,7 +561,7 @@ public void EventRecordWritten_ShouldProvideCorrectSender() } [Fact] - public void EventRecordWritten_ShouldReceiveEventsInOrder() + public async Task EventRecordWritten_ShouldReceiveEventsInOrder() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); @@ -568,9 +586,9 @@ public void EventRecordWritten_ShouldReceiveEventsInOrder() var startTime = DateTime.UtcNow; eventLog.WriteEntry("Event A", EventLogEntryType.Information); - Thread.Sleep(50); // Small delay between events + await Task.Delay(TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); eventLog.WriteEntry("Event B", EventLogEntryType.Information); - Thread.Sleep(50); + await Task.Delay(TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); eventLog.WriteEntry("Event C", EventLogEntryType.Information); bool received = countdown.Wait(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); @@ -658,13 +676,19 @@ public void EventRecordWritten_WhenNotSubscribed_ShouldNotReceiveEvents() // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); var receivedEvents = new List(); + var eventReceived = new ManualResetEventSlim(false); - watcher.EventRecordWritten += (sender, record) => receivedEvents.Add(record); + watcher.EventRecordWritten += (sender, record) => + { + receivedEvents.Add(record); + eventReceived.Set(); + }; // Act - Thread.Sleep(100); // Brief wait to ensure no events arrive + bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken); // Assert + Assert.False(received, "Should not have received any event when not subscribed"); Assert.Empty(receivedEvents); } @@ -843,7 +867,7 @@ public void EventRecordWritten_WithMultipleSubscribers_ShouldNotifyAll() } [Fact] - public void EventRecordWritten_WithNoSubscribers_ShouldNotThrow() + public async Task EventRecordWritten_WithNoSubscribers_ShouldNotThrow() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); @@ -855,7 +879,7 @@ public void EventRecordWritten_WithNoSubscribers_ShouldNotThrow() eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event with no subscribers", EventLogEntryType.Information); - Thread.Sleep(200); // Wait for event processing + await Task.Delay(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken); // Assert - No exception means success Assert.True(watcher.Enabled); From aa503bc34ec134b86729c685fc271cbbdab2e29a Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 15:11:45 -0500 Subject: [PATCH 13/24] Fix EventLogInformation invalid-handle check and replace ThrowsAny with specific assertions --- .../CompressedJsonValueConverterTests.cs | 4 +- .../Readers/EventLogInformationTests.cs | 50 ++++++++++++------- .../Readers/EventLogSessionTests.cs | 6 ++- .../Readers/EventLogWatcherTests.cs | 18 +++++-- .../Readers/EventLogInformation.cs | 13 +++++ 5 files changed, 64 insertions(+), 27 deletions(-) diff --git a/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/CompressedJsonValueConverterTests.cs b/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/CompressedJsonValueConverterTests.cs index 366d735f..20ff28ce 100644 --- a/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/CompressedJsonValueConverterTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/CompressedJsonValueConverterTests.cs @@ -35,13 +35,13 @@ public void ConvertFromCompressedJson_WithEmptyCollection_ShouldReturnEmptyColle } [Fact] - public void ConvertFromCompressedJson_WithInvalidData_ShouldThrowException() + public void ConvertFromCompressedJson_WithInvalidData_ShouldThrowInvalidDataException() { // Arrange var invalidData = new byte[] { 0x00, 0x01, 0x02, 0x03 }; // Act & Assert - Assert.ThrowsAny(() => + Assert.Throws(() => CompressedJsonValueConverter.ConvertFromCompressedJson(invalidData)); } diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs index 55335662..0797d617 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs @@ -25,6 +25,20 @@ public void Constructor_WhenApplicationLog_ShouldPopulateAllProperties() Assert.NotNull(logInfo.RecordCount); } + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t")] + public void Constructor_WhenBlankLogName_ShouldThrowArgumentException(string logName) + { + // Arrange + var session = EventLogSession.GlobalSession; + + // Act & Assert + Assert.Throws(() => + new EventLogInformation(session, logName, PathType.LogName)); + } + [Theory] [InlineData(Constants.ApplicationLogName)] [InlineData(Constants.SystemLogName)] @@ -80,17 +94,6 @@ public async Task Constructor_WhenConcurrentAccess_ShouldHandleMultipleThreads() }); } - [Fact] - public void Constructor_WhenEmptyLogName_ShouldThrowException() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act & Assert - Assert.ThrowsAny(() => - new EventLogInformation(session, string.Empty, PathType.LogName)); - } - [Fact] public void Constructor_WhenGlobalSession_ShouldUseCorrectSession() { @@ -103,25 +106,28 @@ public void Constructor_WhenGlobalSession_ShouldUseCorrectSession() } [Fact] - public void Constructor_WhenInvalidLogName_ShouldThrowException() + public void Constructor_WhenInvalidLogName_ShouldThrowFileNotFoundException() { // Arrange var session = EventLogSession.GlobalSession; var invalidLogName = "NonExistentLog_" + Guid.NewGuid(); // Act & Assert - Assert.ThrowsAny(() => + // Surfaces the real EvtOpenLog error (ERROR_EVT_CHANNEL_NOT_FOUND) instead + // of the previous masked UnauthorizedAccessException from a follow-up + // GetLogInfo on an invalid handle. + Assert.Throws(() => new EventLogInformation(session, invalidLogName, PathType.LogName)); } [Fact] - public void Constructor_WhenNullLogName_ShouldThrowException() + public void Constructor_WhenNullLogName_ShouldThrowArgumentNullException() { // Arrange var session = EventLogSession.GlobalSession; // Act & Assert - Assert.ThrowsAny(() => + Assert.Throws(() => new EventLogInformation(session, null!, PathType.LogName)); } @@ -162,15 +168,23 @@ public void Constructor_WhenSecurityLog_MayRequireElevation() } [Fact] - public void Constructor_WhenSpecialCharactersInLogName_ShouldThrowException() + public void Constructor_WhenSpecialCharactersInLogName_ShouldNotMaskAsUnauthorizedAccessException() { // Arrange var session = EventLogSession.GlobalSession; var invalidLogName = "Invalid<>Log|Name"; - // Act & Assert - Assert.ThrowsAny(() => + // Act + var ex = Record.Exception(() => new EventLogInformation(session, invalidLogName, PathType.LogName)); + + // Assert + // Avoid overfitting the specific Win32 mapping (e.g. + // ERROR_EVT_CHANNEL_PATH_INVALID) since it goes through the default switch + // case and may shift across Windows versions. Capture the smell-fix + // invariant: malformed paths must not be masked as UAE. + Assert.NotNull(ex); + Assert.IsNotType(ex); } [Fact] diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs index 768a585c..f4ed274e 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs @@ -63,14 +63,16 @@ public async Task GetLogInformation_WhenConcurrentAccess_ShouldHandleMultipleThr } [Fact] - public void GetLogInformation_WhenInvalidLogName_ShouldThrowException() + public void GetLogInformation_WhenInvalidLogName_ShouldThrowFileNotFoundException() { // Arrange var session = EventLogSession.GlobalSession; var invalidLogName = "NonExistentLog_" + Guid.NewGuid(); // Act & Assert - Assert.ThrowsAny(() => + // Surfaces the real EvtOpenLog error (ERROR_EVT_CHANNEL_NOT_FOUND) instead + // of the previous masked UnauthorizedAccessException. + Assert.Throws(() => session.GetLogInformation(invalidLogName, PathType.LogName)); } diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs index 04aed6d1..d1cc49e9 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs @@ -43,13 +43,13 @@ public void Constructor_WithEmptyLogName_ShouldThrowArgumentException() } [Fact] - public void Constructor_WithInvalidLogName_ShouldThrowException() + public void Constructor_WithInvalidLogName_ShouldThrowFileNotFoundException() { // Arrange var invalidLogName = "NonExistentLog_" + Guid.NewGuid(); // Act & Assert - Assert.ThrowsAny(() => new EventLogWatcher(invalidLogName)); + Assert.Throws(() => new EventLogWatcher(invalidLogName)); } [Fact] @@ -820,14 +820,22 @@ public async Task EventRecordWritten_WithConcurrentEventWrites_ShouldHandleAllEv } [Fact] - public void EventRecordWritten_WithInvalidBookmark_ShouldThrowException() + public void EventRecordWritten_WithInvalidBookmark_ShouldThrowAndNotMaskAsUnauthorizedAccessException() { // Arrange var invalidBookmark = "InvalidBookmarkString"; using var watcher = new EventLogWatcher(Constants.ApplicationLogName, invalidBookmark, renderXml: false); - // Act & Assert - Assert.ThrowsAny(() => watcher.Enabled = true); + // Act + var ex = Record.Exception(() => watcher.Enabled = true); + + // Assert + // The bookmark XML failure flows through ThrowEventLogException and the + // exact Win32 mapping may shift across Windows versions. Capture the + // stable invariants instead of pinning the type: bad bookmarks must + // surface an exception and must not be masked as UAE. + Assert.NotNull(ex); + Assert.IsNotType(ex); } [Fact] diff --git a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs index 41bb31f6..aa102c19 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs @@ -10,8 +10,21 @@ public sealed class EventLogInformation { internal EventLogInformation(EventLogSession session, string logName, PathType pathType) { + ArgumentException.ThrowIfNullOrWhiteSpace(logName); + using EvtHandle handle = NativeMethods.EvtOpenLog(session.Handle, logName, pathType); + int error = Marshal.GetLastWin32Error(); + + // Surface the real EvtOpenLog failure (e.g., FileNotFoundException for a + // missing channel). Without this check, the constructor would call + // GetLogInfo on a NULL handle and the secondary ERROR_INVALID_HANDLE would + // mask the original error as UnauthorizedAccessException. + if (handle.IsInvalid) + { + NativeMethods.ThrowEventLogException(error); + } + Attributes = (int?)(uint?)GetLogInfo(handle, EvtLogPropertyId.Attributes); CreationTime = (DateTime?)GetLogInfo(handle, EvtLogPropertyId.CreationTime); FileSize = (long?)(ulong?)GetLogInfo(handle, EvtLogPropertyId.FileSize); From 927488f12f64ae6227dd251f5e15e7307ff6e6d0 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 16:10:52 -0500 Subject: [PATCH 14/24] Replace conditional logic in test bodies with deterministic fixtures and explicit skips --- .../EventResolvers/EventResolverBaseTests.cs | 6 +- .../Providers/RegistryProviderTests.cs | 69 +++------ .../Readers/EventLogInformationTests.cs | 22 --- .../Readers/EventLogReaderTests.cs | 62 +++----- .../Readers/SmallEvtxFixture.cs | 143 ++++++++++++++---- 5 files changed, 161 insertions(+), 141 deletions(-) diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs index 69b68184..07c39e04 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs @@ -496,10 +496,8 @@ public void ResolveEvent_WithClassicEventIdMatchingWin32ErrorCode_ShouldNotPrepe // Compare against the locale's system-message text for error 2 so the test // catches false matches on non-English Windows too. var win32ErrorTwoMessage = NativeMethods.FormatSystemMessage(2); - if (!string.IsNullOrWhiteSpace(win32ErrorTwoMessage)) - { - Assert.DoesNotContain(win32ErrorTwoMessage, displayEvent.Description); - } + Assert.False(string.IsNullOrWhiteSpace(win32ErrorTwoMessage), "FormatSystemMessage(2) must return text on Windows."); + Assert.DoesNotContain(win32ErrorTwoMessage!, displayEvent.Description); // Property tail is still rendered Assert.Contains("The following information was included with the event:", displayEvent.Description); Assert.Contains("payload-a", displayEvent.Description); diff --git a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs index e0e31516..efd2b71d 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs @@ -284,29 +284,17 @@ public void GetMessageFilesForLegacyProvider_WhenProviderFound_ShouldLogFoundMes var mockLogger = Substitute.For(); var provider = new RegistryProvider(mockLogger); - // Act - Try common providers until we find one - var commonProviders = new[] - { - Constants.ApplicationLogName, - Constants.SystemLogName, - Constants.KernelGeneralLogName, - Constants.PowerShellLogName - }; - - foreach (var providerName in commonProviders) - { - var result = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); - - if (result.Count == 0) { continue; } - - // Found a provider, verify logging - mockLogger.Received() - .Debug(Arg.Is(h => h.ToString().Contains("Found message file for legacy provider"))); - - return; - } - - // If no providers found, skip assertion + // Act — drive the SUT through whichever well-known legacy provider is + // present on this host. The helper internally probes a fixed list and + // returns the first non-empty match. + var foundFiles = FindAnyLegacyProviderFiles(provider); + + // Assert — skip rather than silently pass when the host has no eligible + // legacy providers registered (e.g., a stripped-down container image). + Assert.SkipUnless(foundFiles.Count > 0, + "Test requires at least one common legacy provider with registered message files on this host."); + mockLogger.Received() + .Debug(Arg.Is(h => h.ToString().Contains("Found message file for legacy provider"))); } [Fact] @@ -319,15 +307,14 @@ public void GetMessageFilesForLegacyProvider_WhenReturnedPaths_ShouldBeDllOrExe( var result = FindAnyLegacyProviderFiles(provider); // Assert - if (result.Count != 0) + Assert.SkipUnless(result.Count > 0, + "Test requires at least one common legacy provider with registered message files on this host."); + Assert.All(result, path => { - Assert.All(result, path => - { - var extension = Path.GetExtension(path).ToLower(); - Assert.True(extension is ".dll" or ".exe", - $"Expected .dll or .exe, but got {extension} for path: {path}"); - }); - } + var extension = Path.GetExtension(path).ToLower(); + Assert.True(extension is ".dll" or ".exe", + $"Expected .dll or .exe, but got {extension} for path: {path}"); + }); } [Fact] @@ -340,16 +327,14 @@ public void GetMessageFilesForLegacyProvider_WhenReturnedPaths_ShouldBeFullPaths var result = FindAnyLegacyProviderFiles(provider); // Assert - if (result.Count != 0) + Assert.SkipUnless(result.Count > 0, + "Test requires at least one common legacy provider with registered message files on this host."); + Assert.All(result, path => { - Assert.All(result, path => - { - // Full paths should either start with drive letter or UNC path - Assert.True( - Path.IsPathFullyQualified(path) || path.StartsWith(@"\\"), - $"Expected fully qualified path, but got: {path}"); - }); - } + Assert.True( + Path.IsPathFullyQualified(path) || path.StartsWith(@"\\"), + $"Expected fully qualified path, but got: {path}"); + }); } [Fact] @@ -438,10 +423,6 @@ public void GetMessageFilesForLegacyProvider_WhenWhitespaceProviderName_ShouldRe Assert.Empty(result); } - /// - /// Helper method to find any legacy provider files on the system. - /// Tries common providers and returns the first non-empty result. - /// private static List FindAnyLegacyProviderFiles(RegistryProvider provider) { var commonProviders = new[] diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs index 0797d617..75227a4a 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs @@ -145,28 +145,6 @@ public void Constructor_WhenPathTypeLogName_ShouldSucceed() Assert.NotNull(logInfo.RecordCount); } - [Fact] - public void Constructor_WhenSecurityLog_MayRequireElevation() - { - // Arrange - var session = EventLogSession.GlobalSession; - - // Act & Assert - // Security log typically requires administrator privileges - // This test documents expected behavior rather than testing functionality - try - { - var logInfo = new EventLogInformation(session, Constants.SecurityLogName, PathType.LogName); - // If we can access it (running as admin), verify it works - Assert.NotNull(logInfo); - } - catch (UnauthorizedAccessException ex) - { - // Expected when not running as administrator - Assert.IsType(ex); - } - } - [Fact] public void Constructor_WhenSpecialCharactersInLogName_ShouldNotMaskAsUnauthorizedAccessException() { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs index e6a8dbe2..9a5e8657 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs @@ -224,18 +224,20 @@ public void IsValid_WhenInvalidLogName_ShouldBeFalse() [Fact] public void LastBookmark_AfterTryGetEvents_ShouldBeSet() { - // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + // Use SmallEvtxFixture so the read is deterministic — the live Application + // log may be empty on minimal hosts, which used to silently skip the + // assertions behind an `if (success && events.Length > 0)` guard. + using var fixture = new SmallEvtxFixture(); + using var reader = new EventLogReader(fixture.FilePath, PathType.FilePath); // Act bool success = reader.TryGetEvents(out var events); // Assert - if (success && events.Length > 0) - { - Assert.NotNull(reader.LastBookmark); - Assert.NotEmpty(reader.LastBookmark); - } + Assert.True(success); + Assert.NotEmpty(events); + Assert.NotNull(reader.LastBookmark); + Assert.NotEmpty(reader.LastBookmark); } [Fact] @@ -251,8 +253,10 @@ public void LastBookmark_WhenInitialized_ShouldBeNull() [Fact] public void LastBookmark_WhenMultipleBatches_ShouldUpdateWithEachBatch() { - // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + // Use SmallEvtxFixture (5 events) with batchSize: 1 so two consecutive + // reads are guaranteed to produce two distinct events on every host. + using var fixture = new SmallEvtxFixture(); + using var reader = new EventLogReader(fixture.FilePath, PathType.FilePath); // Act bool success1 = reader.TryGetEvents(out var events1, batchSize: 1); @@ -262,20 +266,15 @@ public void LastBookmark_WhenMultipleBatches_ShouldUpdateWithEachBatch() string? bookmark2 = reader.LastBookmark; // Assert - if (success1 && events1.Length > 0) - { - Assert.NotNull(bookmark1); - } - - if (!success2 || events2.Length == 0) { return; } + Assert.True(success1); + Assert.Single(events1); + Assert.NotNull(bookmark1); + Assert.True(success2); + Assert.Single(events2); Assert.NotNull(bookmark2); - // Bookmarks should be different if we read different events - if (bookmark1 != null && events1.Length > 0 && events2.Length > 0) - { - Assert.NotEqual(bookmark1, bookmark2); - } + Assert.NotEqual(bookmark1, bookmark2); } [Fact] @@ -470,29 +469,6 @@ public void TryGetEvents_WhenDefaultBatchSize_ShouldReturn30OrLess() Assert.True(events.Length <= 30); } - [Fact] - public void TryGetEvents_WhenEventHasError_ShouldSetErrorProperty() - { - // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); - - // Act - bool success = reader.TryGetEvents(out var events, batchSize: 100); - - // Assert - Assert.True(success); - - // Some events might have errors (e.g., corrupt events, missing provider info) - // Just verify that if Error is set, it's a non-empty string - foreach (var evt in events) - { - if (!string.IsNullOrEmpty(evt.Error)) - { - Assert.NotEmpty(evt.Error); - } - } - } - [Fact] public void TryGetEvents_WhenEventsReturned_ShouldHavePathNameSet() { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs b/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs index 90d0e496..36eca189 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs @@ -1,17 +1,24 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Readers; using System.Diagnostics; +using System.Text.RegularExpressions; namespace EventLogExpert.Eventing.Tests.Readers; -/// -/// Creates a small temporary .evtx file by exporting at most 5 events from the local -/// Application log. Used by tests that need to exercise end-of-results behavior without -/// scanning the entire (potentially millions of records) Application log. -/// +/// Creates a small temporary .evtx file by exporting a bounded window of events from the live local Application log, anchored on the most recent EventRecordID. internal sealed class SmallEvtxFixture : IDisposable { + private const int MinimumExpectedEvents = 2; + private const int RequestedEvents = 5; + + private static readonly Regex s_recordIdPattern = new( + @"(\d+)", + RegexOptions.CultureInvariant | RegexOptions.Compiled); + + private static readonly TimeSpan s_wevtutilTimeout = TimeSpan.FromSeconds(30); + public SmallEvtxFixture() { FilePath = Path.Combine(Path.GetTempPath(), $"elx-tests-{Guid.NewGuid():N}.evtx"); @@ -20,54 +27,134 @@ public SmallEvtxFixture() // robust on machines where %PATH% has been customized. var wevtutilPath = Path.Combine(Environment.SystemDirectory, "wevtutil.exe"); - // /q with an EventRecordID range bounds the export to at most 5 events. wevtutil epl - // does not support a /count switch, so XPath is the supported way to cap output. + // Probe the live Application log for its most recent EventRecordID. Hard-coding + // the legacy [1, 5] window only works on hosts where the log has not rolled past + // record 5 -- not the case on long-running dev boxes or persistent CI agents. + long latestId = ProbeLatestRecordId(wevtutilPath); + long oldestId = Math.Max(1, latestId - (RequestedEvents - 1)); + + ExportRecordWindow(wevtutilPath, oldestId, latestId); + VerifyMinimumEvents(); + } + + public string FilePath { get; } + + public void Dispose() + { + try + { + if (File.Exists(FilePath)) { File.Delete(FilePath); } + } + catch + { + // Best-effort cleanup: a temp file left behind should not fail the test. + } + } + + private static long ProbeLatestRecordId(string wevtutilPath) + { + // qe = query events; /c:1 limits to one event; /rd:true reads in reverse-chronological + // order (newest first); /f:RenderedXml ensures EventRecordID is rendered as an XML + // element we can parse without depending on locale-formatted text output. + var (stdout, _) = RunWevtutil( + wevtutilPath, + ["qe", "Application", "/c:1", "/rd:true", "/f:RenderedXml"], + "qe Application"); + + var match = s_recordIdPattern.Match(stdout); + + if (!match.Success || !long.TryParse(match.Groups[1].Value, out long id)) + { + throw new InvalidOperationException( + "Could not determine the most recent EventRecordID from wevtutil.exe output. " + + "The local Application log may be empty."); + } + + return id; + } + + private static (string Stdout, string Stderr) RunWevtutil( + string wevtutilPath, + IEnumerable arguments, + string commandLabel) + { var psi = new ProcessStartInfo { FileName = wevtutilPath, - ArgumentList = - { - "epl", - "Application", - FilePath, - "/q:*[System[EventRecordID>=1 and EventRecordID<=5]]" - }, UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, CreateNoWindow = true }; - using var proc = Process.Start(psi) - ?? throw new InvalidOperationException("Failed to start wevtutil.exe."); + foreach (var arg in arguments) { psi.ArgumentList.Add(arg); } + + using var proc = Process.Start(psi) ?? + throw new InvalidOperationException( + $"Failed to start wevtutil.exe ({commandLabel})."); + + Task stdoutTask = proc.StandardOutput.ReadToEndAsync(); + Task stderrTask = proc.StandardError.ReadToEndAsync(); - if (!proc.WaitForExit(TimeSpan.FromSeconds(30))) + if (!proc.WaitForExit(s_wevtutilTimeout)) { - try { proc.Kill(entireProcessTree: true); } catch { /* best effort */ } + try { proc.Kill(true); } + catch + { /* best effort */ + } - throw new InvalidOperationException("wevtutil.exe did not complete within 30 seconds."); + throw new InvalidOperationException( + $"wevtutil.exe ({commandLabel}) did not complete within {s_wevtutilTimeout.TotalSeconds:0} seconds."); } + // The process has exited so both pipes are now closed; the read tasks complete + // imminently and a brief synchronous wait here cannot hang. + string stdout = stdoutTask.GetAwaiter().GetResult(); + string stderr = stderrTask.GetAwaiter().GetResult(); + if (proc.ExitCode != 0) { - var err = proc.StandardError.ReadToEnd(); - throw new InvalidOperationException( - $"wevtutil.exe exited with code {proc.ExitCode}. Stderr: {err}"); + $"wevtutil.exe ({commandLabel}) exited with code {proc.ExitCode}. Stderr: {stderr}"); } + + return (stdout, stderr); } - public string FilePath { get; } + private void ExportRecordWindow(string wevtutilPath, long oldestId, long latestId) + { + // wevtutil epl does not support a /count switch, so XPath against EventRecordID is the + // supported way to bound the export size. + _ = RunWevtutil( + wevtutilPath, + ["epl", "Application", FilePath, $"/q:*[System[EventRecordID>={oldestId} and EventRecordID<={latestId}]]"], + "epl Application"); + } - public void Dispose() + private void VerifyMinimumEvents() { - try + // Open the freshly-exported file and count records so a host where the Application + // log was just cleared (or where the probed window happened to span gaps) produces + // a clear fixture-level error instead of cryptic failures (e.g. Assert.True(success) + // on ERROR_NO_MORE_ITEMS) inside test bodies. We use the project's own + // EventLogReader rather than System.Diagnostics.Eventing.Reader because that BCL API + // is forbidden in this solution -- the entire EventLogExpert.Eventing project owns + // the EVT P/Invoke layer and bypassing it (even in a fixture) would defeat the point. + using var reader = new EventLogReader(FilePath, PathType.FilePath); + + int total = 0; + + while (reader.TryGetEvents(out var batch) && batch.Length > 0) { - if (File.Exists(FilePath)) { File.Delete(FilePath); } + total += batch.Length; } - catch + + if (total < MinimumExpectedEvents) { - // Best-effort cleanup: a temp file left behind should not fail the test. + throw new InvalidOperationException( + $"SmallEvtxFixture exported only {total} event(s) to '{FilePath}'; " + + $"required at least {MinimumExpectedEvents}. The local Application log " + + "may be empty or recently cleared."); } } } From 419f307a046b3657a6efd552476fca800b3d3aae Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 17:13:30 -0500 Subject: [PATCH 15/24] Split Eventing tests into unit and integration projects --- .github/workflows/PullRequest.yml | 4 +- ...LogExpert.Eventing.IntegrationTests.csproj | 17 + .../GlobalUsings.cs | 1 + .../EventMessageProviderIntegrationTests.cs | 324 ++++++++++++++++++ .../Providers/ProviderMetadataTests.cs | 2 +- .../Providers/RegistryProviderTests.cs | 2 +- .../Readers/EventLogInformationTests.cs | 2 +- .../Readers/EventLogReaderTests.cs | 2 +- .../Readers/EventLogSessionTests.cs | 2 +- .../Readers/EventLogWatcherTests.cs | 2 +- .../Readers/SmallEvtxFixture.cs | 8 +- .../Providers/EventMessageProviderTests.cs | 312 ----------------- .../EventLogExpert.Eventing.csproj | 1 + src/EventLogExpert.slnx | 1 + 14 files changed, 357 insertions(+), 323 deletions(-) create mode 100644 src/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj create mode 100644 src/EventLogExpert.Eventing.IntegrationTests/GlobalUsings.cs create mode 100644 src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs rename src/{EventLogExpert.Eventing.Tests => EventLogExpert.Eventing.IntegrationTests}/Providers/ProviderMetadataTests.cs (99%) rename src/{EventLogExpert.Eventing.Tests => EventLogExpert.Eventing.IntegrationTests}/Providers/RegistryProviderTests.cs (99%) rename src/{EventLogExpert.Eventing.Tests => EventLogExpert.Eventing.IntegrationTests}/Readers/EventLogInformationTests.cs (99%) rename src/{EventLogExpert.Eventing.Tests => EventLogExpert.Eventing.IntegrationTests}/Readers/EventLogReaderTests.cs (99%) rename src/{EventLogExpert.Eventing.Tests => EventLogExpert.Eventing.IntegrationTests}/Readers/EventLogSessionTests.cs (99%) rename src/{EventLogExpert.Eventing.Tests => EventLogExpert.Eventing.IntegrationTests}/Readers/EventLogWatcherTests.cs (99%) rename src/{EventLogExpert.Eventing.Tests => EventLogExpert.Eventing.IntegrationTests}/Readers/SmallEvtxFixture.cs (94%) diff --git a/.github/workflows/PullRequest.yml b/.github/workflows/PullRequest.yml index ced8ebb7..ec37bda6 100644 --- a/.github/workflows/PullRequest.yml +++ b/.github/workflows/PullRequest.yml @@ -32,4 +32,6 @@ jobs: - name: Build run: dotnet build EventLogExpert.slnx --no-restore - name: Test - run: dotnet test EventLogExpert.slnx --no-build --verbosity normal + run: dotnet test EventLogExpert.slnx --no-build --verbosity normal --filter "FullyQualifiedName!~EventLogExpert.Eventing.IntegrationTests" + - name: Integration Test + run: dotnet test EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj --no-build --verbosity normal diff --git a/src/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj b/src/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj new file mode 100644 index 00000000..96299147 --- /dev/null +++ b/src/EventLogExpert.Eventing.IntegrationTests/EventLogExpert.Eventing.IntegrationTests.csproj @@ -0,0 +1,17 @@ + + + + false + true + true + + + + + + + + + + + \ No newline at end of file diff --git a/src/EventLogExpert.Eventing.IntegrationTests/GlobalUsings.cs b/src/EventLogExpert.Eventing.IntegrationTests/GlobalUsings.cs new file mode 100644 index 00000000..c802f448 --- /dev/null +++ b/src/EventLogExpert.Eventing.IntegrationTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs new file mode 100644 index 00000000..0e12584c --- /dev/null +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs @@ -0,0 +1,324 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Interop; +using EventLogExpert.Eventing.Logging; +using EventLogExpert.Eventing.Providers; +using EventLogExpert.Eventing.Readers; +using EventLogExpert.Eventing.Tests.TestUtils.Constants; +using NSubstitute; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace EventLogExpert.Eventing.IntegrationTests.Providers; + +public sealed class EventMessageProviderIntegrationTests +{ + [Fact] + public void GetMessages_WhenBinaryUsesMuiSatellite_ShouldLoadMessagesFromMuiFile() + { + // wevtsvc.dll on Windows keeps its message table in \wevtsvc.dll.mui rather + // than in the binary itself. The previous load path used LOAD_LIBRARY_AS_DATAFILE + // with only the leaf filename and therefore failed with ERROR_RESOURCE_TYPE_NOT_FOUND + // (1813) for binaries like this — including the WMIRegistrationService.exe scenario + // that motivated this fix. The MUI-aware load path should resolve them. + var systemDirectory = Environment.SystemDirectory; + var muiBinary = Path.Combine(systemDirectory, "wevtsvc.dll"); + + Assert.SkipUnless( + File.Exists(muiBinary) && TryFindMuiSatellite(systemDirectory, "wevtsvc.dll", out _), + "Test requires wevtsvc.dll and a matching .mui satellite in the loader's MUI fallback chain (current UI culture or en-US)."); + + // Act + var messages = EventMessageProvider.GetMessages([muiBinary], Constants.TestProviderName); + + // Assert + Assert.NotNull(messages); + Assert.NotEmpty(messages); + Assert.All(messages, m => Assert.Equal(Constants.TestProviderName, m.ProviderName)); + } + + [Fact] + public void GetMessages_WhenFilePathContainsEnvironmentVariable_ShouldHandleCorrectly() + { + // LoadLibraryEx does not expand environment variables. Channel-named providers like + // Microsoft-Windows-AppXDeploymentServer/Operational supply paths such as + // "%SystemRoot%\system32\AppXDeploymentServer.dll" through the publisher manifest; + // EventMessageProvider must expand them defensively before calling LoadLibraryEx, + // otherwise the load fails with ERROR_FILE_NOT_FOUND. We exercise the same path + // here with wevtsvc.dll, which is present on every Windows host. + var systemDirectory = Environment.SystemDirectory; + var muiBinary = Path.Combine(systemDirectory, "wevtsvc.dll"); + + Assert.SkipUnless( + File.Exists(muiBinary) && TryFindMuiSatellite(systemDirectory, "wevtsvc.dll", out _), + "Test requires wevtsvc.dll and a matching .mui satellite in the loader's MUI fallback chain (current UI culture or en-US)."); + + var filesWithEnvVar = new[] { @"%SystemRoot%\System32\wevtsvc.dll" }; + + var messages = EventMessageProvider.GetMessages(filesWithEnvVar, Constants.TestProviderName); + + Assert.NotNull(messages); + Assert.NotEmpty(messages); + Assert.All(messages, m => Assert.Equal(Constants.TestProviderName, m.ProviderName)); + } + + [Fact] + public void LoadProviderDetails_ShouldLogProviderLoadingAttempt() + { + // Arrange + var mockLogger = Substitute.For(); + EventMessageProvider provider = new(Constants.TestProviderName, logger: mockLogger); + + // Act + provider.LoadProviderDetails(); + + // Assert + mockLogger.Received().Debug(Arg.Any()); + } + + [Fact] + public void LoadProviderDetails_WhenCalled_ShouldHaveNonNullCollections() + { + // Arrange + EventMessageProvider provider = new(Constants.TestProviderName); + + // Act + var details = provider.LoadProviderDetails(); + + // Assert + Assert.NotNull(details); + Assert.NotNull(details.Events); + Assert.NotNull(details.Keywords); + Assert.NotNull(details.Opcodes); + Assert.NotNull(details.Tasks); + Assert.NotNull(details.Messages); + Assert.NotNull(details.Parameters); + } + + [Fact] + public void LoadProviderDetails_WhenCalled_ShouldReturnProviderDetails() + { + // Arrange + EventMessageProvider provider = new(Constants.TestProviderName); + + // Act + var details = provider.LoadProviderDetails(); + + // Assert + Assert.NotNull(details); + Assert.Equal(Constants.TestProviderName, details.ProviderName); + } + + [Fact] + public void LoadProviderDetails_WhenCalledMultipleTimes_ShouldReturnConsistentResults() + { + // Arrange + EventMessageProvider provider = new(Constants.TestProviderName); + + // Act + var details1 = provider.LoadProviderDetails(); + var details2 = provider.LoadProviderDetails(); + + // Assert + Assert.NotNull(details1); + Assert.NotNull(details2); + Assert.Equal(details1.ProviderName, details2.ProviderName); + } + + [Fact] + public void LoadProviderDetails_WhenChannelOwningPublisherUnknown_ShouldReturnEmptyDetailsWithoutFallback() + { + // A made-up provider name that is neither a registered publisher nor a registered channel. + // The owning-publisher probe must return false and the resolver must end up with an empty + // ProviderDetails instead of recursing or throwing. + const string MadeUpName = "NonExistent-Publisher-And-Channel/Bogus"; + + EventMessageProvider provider = new(MadeUpName); + + var details = provider.LoadProviderDetails(); + + Assert.NotNull(details); + Assert.True(details.IsEmpty); + Assert.Null(details.ResolvedFromOwningPublisher); + Assert.Equal(MadeUpName, details.ProviderName); + } + + [Fact] + public void LoadProviderDetails_WhenProviderHasNoData_ShouldReturnEmptyCollections() + { + // Arrange + EventMessageProvider provider = new(Constants.TestProviderName); + + // Act + var details = provider.LoadProviderDetails(); + + // Assert + Assert.NotNull(details); + Assert.Empty(details.Messages); + Assert.Empty(details.Parameters); + } + + [Fact] + public void LoadProviderDetails_WhenProviderNameIsActuallyAChannelPath_ShouldFallBackToOwningPublisher() + { + // Some events arrive with a channel path in the ProviderName slot rather than the real + // publisher name (the AppXDeploymentServer/Operational case). We expect the fallback to + // resolve via EvtChannelConfigOwningPublisher and load the real publisher's metadata. + Assert.SkipUnless( + TryFindChannelWithDistinctOwningPublisher(out var channelName, out var owningPublisher), + "Test requires a registered channel whose owning publisher differs from the channel path itself."); + + EventMessageProvider provider = new(channelName!); + + var details = provider.LoadProviderDetails(); + + Assert.NotNull(details); + Assert.False(details.IsEmpty, + $"Expected channel-owner fallback to populate metadata for channel '{channelName}' (owner '{owningPublisher}')."); + Assert.Equal(owningPublisher, details.ResolvedFromOwningPublisher); + } + + [Fact] + public void LoadProviderDetails_WhenProviderNotFound_ShouldReturnDetailsWithProviderName() + { + // Arrange + EventMessageProvider provider = new(Constants.TestProviderName); + + // Act + var details = provider.LoadProviderDetails(); + + // Assert + Assert.NotNull(details); + Assert.Equal(Constants.TestProviderName, details.ProviderName); + } + + private static bool TryFindChannelWithDistinctOwningPublisher(out string? channelName, out string? owningPublisher) + { + foreach (var candidate in EventLogSession.GlobalSession.GetLogNames()) + { + // Only modern channels carry an OwningPublisher distinct from the channel path itself. + if (!candidate.Contains('/')) + { + continue; + } + + using var channelConfig = NativeMethods.EvtOpenChannelConfig( + EventLogSession.GlobalSession.Handle, + candidate, + 0); + + if (channelConfig.IsInvalid) + { + continue; + } + + if (!TryReadOwningPublisher(channelConfig, out var publisher) || string.IsNullOrEmpty(publisher)) + { + continue; + } + + if (string.Equals(publisher, candidate, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + channelName = candidate; + owningPublisher = publisher; + + return true; + } + + channelName = null; + owningPublisher = null; + + return false; + } + + private static bool TryFindMuiSatellite(string systemDirectory, string binaryName, out string? satellitePath) + { + var muiFileName = binaryName + ".mui"; + + // Only probe locales the Win32 MUI loader will actually consult: the current UI culture, + // its parents, and "en-US" as the well-known system fallback that ships with every Windows + // SKU. A satellite present in some other locale subfolder would pass a broad existence + // check but the loader would never select it, so the test could still fail after skipping. + // The loop terminates naturally when culture.Name becomes empty (the invariant culture). + var probeOrder = new List(); + + for (var culture = CultureInfo.CurrentUICulture; !string.IsNullOrEmpty(culture.Name); culture = culture.Parent) + { + if (!probeOrder.Contains(culture.Name, StringComparer.OrdinalIgnoreCase)) + { + probeOrder.Add(culture.Name); + } + } + + if (!probeOrder.Contains("en-US", StringComparer.OrdinalIgnoreCase)) + { + probeOrder.Add("en-US"); + } + + foreach (var cultureName in probeOrder) + { + var candidate = Path.Combine(systemDirectory, cultureName, muiFileName); + + if (!File.Exists(candidate)) { continue; } + + satellitePath = candidate; + + return true; + } + + satellitePath = null; + + return false; + } + + private static bool TryReadOwningPublisher(EvtHandle channelConfig, out string? publisher) + { + publisher = null; + + bool success = NativeMethods.EvtGetChannelConfigProperty( + channelConfig, + EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, + 0, + 0, + IntPtr.Zero, + out int bufferSize); + + int error = Marshal.GetLastWin32Error(); + + if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) + { + return false; + } + + IntPtr buffer = Marshal.AllocHGlobal(bufferSize); + + try + { + success = NativeMethods.EvtGetChannelConfigProperty( + channelConfig, + EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, + 0, + bufferSize, + buffer, + out _); + + if (!success) + { + return false; + } + + var variant = Marshal.PtrToStructure(buffer); + publisher = NativeMethods.ConvertVariant(variant) as string; + + return !string.IsNullOrEmpty(publisher); + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } +} diff --git a/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs similarity index 99% rename from src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs index 772390d6..c6e799ef 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs @@ -7,7 +7,7 @@ using NSubstitute; using System.Collections.ObjectModel; -namespace EventLogExpert.Eventing.Tests.Providers; +namespace EventLogExpert.Eventing.IntegrationTests.Providers; public sealed class ProviderMetadataTests { diff --git a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs similarity index 99% rename from src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs index efd2b71d..3120347f 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs @@ -6,7 +6,7 @@ using EventLogExpert.Eventing.Tests.TestUtils.Constants; using NSubstitute; -namespace EventLogExpert.Eventing.Tests.Providers; +namespace EventLogExpert.Eventing.IntegrationTests.Providers; public sealed class RegistryProviderTests { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs similarity index 99% rename from src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs index 75227a4a..2e9e4678 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs @@ -4,7 +4,7 @@ using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.Readers; public sealed class EventLogInformationTests { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs similarity index 99% rename from src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs index 9a5e8657..bd14ad08 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs @@ -4,7 +4,7 @@ using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.Readers; public sealed class EventLogReaderTests { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs similarity index 99% rename from src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs index f4ed274e..bff735db 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs @@ -4,7 +4,7 @@ using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.Readers; public sealed class EventLogSessionTests { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs similarity index 99% rename from src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs index d1cc49e9..30c138e5 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs @@ -6,7 +6,7 @@ using EventLogExpert.Eventing.Tests.TestUtils.Constants; using System.Diagnostics; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.Readers; public sealed class EventLogWatcherTests { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs similarity index 94% rename from src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs index 36eca189..b830a47f 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Text.RegularExpressions; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.Readers; /// Creates a small temporary .evtx file by exporting a bounded window of events from the live local Application log, anchored on the most recent EventRecordID. internal sealed class SmallEvtxFixture : IDisposable @@ -137,9 +137,9 @@ private void VerifyMinimumEvents() // log was just cleared (or where the probed window happened to span gaps) produces // a clear fixture-level error instead of cryptic failures (e.g. Assert.True(success) // on ERROR_NO_MORE_ITEMS) inside test bodies. We use the project's own - // EventLogReader rather than System.Diagnostics.Eventing.Reader because that BCL API - // is forbidden in this solution -- the entire EventLogExpert.Eventing project owns - // the EVT P/Invoke layer and bypassing it (even in a fixture) would defeat the point. + // EventLogReader rather than the BCL event-log reader because the entire + // EventLogExpert.Eventing project owns the EVT P/Invoke layer and bypassing it + // (even in a fixture) would defeat the point. using var reader = new EventLogReader(FilePath, PathType.FilePath); int total = 0; diff --git a/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs index 1ab03ac9..a834dede 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs @@ -1,14 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; -using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; using NSubstitute; -using System.Globalization; -using System.Runtime.InteropServices; namespace EventLogExpert.Eventing.Tests.Providers; @@ -26,30 +22,6 @@ public void Constructor_WhenDifferentProviderNames_ShouldCreateInstances(string Assert.NotNull(provider); } - [Fact] - public void GetMessages_WhenBinaryUsesMuiSatellite_ShouldLoadMessagesFromMuiFile() - { - // wevtsvc.dll on Windows keeps its message table in \wevtsvc.dll.mui rather - // than in the binary itself. The previous load path used LOAD_LIBRARY_AS_DATAFILE - // with only the leaf filename and therefore failed with ERROR_RESOURCE_TYPE_NOT_FOUND - // (1813) for binaries like this — including the WMIRegistrationService.exe scenario - // that motivated this fix. The MUI-aware load path should resolve them. - var systemDirectory = Environment.SystemDirectory; - var muiBinary = Path.Combine(systemDirectory, "wevtsvc.dll"); - - Assert.SkipUnless( - File.Exists(muiBinary) && TryFindMuiSatellite(systemDirectory, "wevtsvc.dll", out _), - "Test requires wevtsvc.dll and a matching .mui satellite in the loader's MUI fallback chain (current UI culture or en-US)."); - - // Act - var messages = EventMessageProvider.GetMessages([muiBinary], Constants.TestProviderName); - - // Assert - Assert.NotNull(messages); - Assert.NotEmpty(messages); - Assert.All(messages, m => Assert.Equal(Constants.TestProviderName, m.ProviderName)); - } - [Fact] public void GetMessages_WhenDuplicateFiles_ShouldProcessAll() { @@ -96,31 +68,6 @@ public void GetMessages_WhenFileListIsNull_ShouldThrowNullReferenceException() EventMessageProvider.GetMessages(null!, Constants.TestProviderName, mockLogger)); } - [Fact] - public void GetMessages_WhenFilePathContainsEnvironmentVariable_ShouldHandleCorrectly() - { - // LoadLibraryEx does not expand environment variables. Channel-named providers like - // Microsoft-Windows-AppXDeploymentServer/Operational supply paths such as - // "%SystemRoot%\system32\AppXDeploymentServer.dll" through the publisher manifest; - // EventMessageProvider must expand them defensively before calling LoadLibraryEx, - // otherwise the load fails with ERROR_FILE_NOT_FOUND. We exercise the same path - // here with wevtsvc.dll, which is present on every Windows host. - var systemDirectory = Environment.SystemDirectory; - var muiBinary = Path.Combine(systemDirectory, "wevtsvc.dll"); - - Assert.SkipUnless( - File.Exists(muiBinary) && TryFindMuiSatellite(systemDirectory, "wevtsvc.dll", out _), - "Test requires wevtsvc.dll and a matching .mui satellite in the loader's MUI fallback chain (current UI culture or en-US)."); - - var filesWithEnvVar = new[] { @"%SystemRoot%\System32\wevtsvc.dll" }; - - var messages = EventMessageProvider.GetMessages(filesWithEnvVar, Constants.TestProviderName); - - Assert.NotNull(messages); - Assert.NotEmpty(messages); - Assert.All(messages, m => Assert.Equal(Constants.TestProviderName, m.ProviderName)); - } - [Fact] public void GetMessages_WhenFilePathHasMultipleBackslashes_ShouldExtractFileName() { @@ -215,263 +162,4 @@ public void GetMessages_WhenProviderNameProvided_ShouldIncludeInMessages() // Assert Assert.NotNull(messages); } - - [Fact] - public void LoadProviderDetails_ShouldLogProviderLoadingAttempt() - { - // Arrange - var mockLogger = Substitute.For(); - EventMessageProvider provider = new(Constants.TestProviderName, logger: mockLogger); - - // Act - provider.LoadProviderDetails(); - - // Assert - mockLogger.Received().Debug(Arg.Any()); - } - - [Fact] - public void LoadProviderDetails_WhenCalled_ShouldHaveNonNullCollections() - { - // Arrange - EventMessageProvider provider = new(Constants.TestProviderName); - - // Act - var details = provider.LoadProviderDetails(); - - // Assert - Assert.NotNull(details); - Assert.NotNull(details.Events); - Assert.NotNull(details.Keywords); - Assert.NotNull(details.Opcodes); - Assert.NotNull(details.Tasks); - Assert.NotNull(details.Messages); - Assert.NotNull(details.Parameters); - } - - [Fact] - public void LoadProviderDetails_WhenCalled_ShouldReturnProviderDetails() - { - // Arrange - EventMessageProvider provider = new(Constants.TestProviderName); - - // Act - var details = provider.LoadProviderDetails(); - - // Assert - Assert.NotNull(details); - Assert.Equal(Constants.TestProviderName, details.ProviderName); - } - - [Fact] - public void LoadProviderDetails_WhenCalledMultipleTimes_ShouldReturnConsistentResults() - { - // Arrange - EventMessageProvider provider = new(Constants.TestProviderName); - - // Act - var details1 = provider.LoadProviderDetails(); - var details2 = provider.LoadProviderDetails(); - - // Assert - Assert.NotNull(details1); - Assert.NotNull(details2); - Assert.Equal(details1.ProviderName, details2.ProviderName); - } - - [Fact] - public void LoadProviderDetails_WhenChannelOwningPublisherUnknown_ShouldReturnEmptyDetailsWithoutFallback() - { - // A made-up provider name that is neither a registered publisher nor a registered channel. - // The owning-publisher probe must return false and the resolver must end up with an empty - // ProviderDetails instead of recursing or throwing. - const string MadeUpName = "NonExistent-Publisher-And-Channel/Bogus"; - - EventMessageProvider provider = new(MadeUpName); - - var details = provider.LoadProviderDetails(); - - Assert.NotNull(details); - Assert.True(details.IsEmpty); - Assert.Null(details.ResolvedFromOwningPublisher); - Assert.Equal(MadeUpName, details.ProviderName); - } - - [Fact] - public void LoadProviderDetails_WhenProviderHasNoData_ShouldReturnEmptyCollections() - { - // Arrange - EventMessageProvider provider = new(Constants.TestProviderName); - - // Act - var details = provider.LoadProviderDetails(); - - // Assert - Assert.NotNull(details); - Assert.Empty(details.Messages); - Assert.Empty(details.Parameters); - } - - [Fact] - public void LoadProviderDetails_WhenProviderNameIsActuallyAChannelPath_ShouldFallBackToOwningPublisher() - { - // Some events arrive with a channel path in the ProviderName slot rather than the real - // publisher name (the AppXDeploymentServer/Operational case). We expect the fallback to - // resolve via EvtChannelConfigOwningPublisher and load the real publisher's metadata. - Assert.SkipUnless( - TryFindChannelWithDistinctOwningPublisher(out var channelName, out var owningPublisher), - "Test requires a registered channel whose owning publisher differs from the channel path itself."); - - EventMessageProvider provider = new(channelName!); - - var details = provider.LoadProviderDetails(); - - Assert.NotNull(details); - Assert.False(details.IsEmpty, - $"Expected channel-owner fallback to populate metadata for channel '{channelName}' (owner '{owningPublisher}')."); - Assert.Equal(owningPublisher, details.ResolvedFromOwningPublisher); - } - - [Fact] - public void LoadProviderDetails_WhenProviderNotFound_ShouldReturnDetailsWithProviderName() - { - // Arrange - EventMessageProvider provider = new(Constants.TestProviderName); - - // Act - var details = provider.LoadProviderDetails(); - - // Assert - Assert.NotNull(details); - Assert.Equal(Constants.TestProviderName, details.ProviderName); - } - - private static bool TryFindChannelWithDistinctOwningPublisher(out string? channelName, out string? owningPublisher) - { - foreach (var candidate in EventLogSession.GlobalSession.GetLogNames()) - { - // Only modern channels carry an OwningPublisher distinct from the channel path itself. - if (!candidate.Contains('/')) - { - continue; - } - - using var channelConfig = NativeMethods.EvtOpenChannelConfig( - EventLogSession.GlobalSession.Handle, - candidate, - 0); - - if (channelConfig.IsInvalid) - { - continue; - } - - if (!TryReadOwningPublisher(channelConfig, out var publisher) || string.IsNullOrEmpty(publisher)) - { - continue; - } - - if (string.Equals(publisher, candidate, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - channelName = candidate; - owningPublisher = publisher; - - return true; - } - - channelName = null; - owningPublisher = null; - - return false; - } - - private static bool TryFindMuiSatellite(string systemDirectory, string binaryName, out string? satellitePath) - { - var muiFileName = binaryName + ".mui"; - - // Only probe locales the Win32 MUI loader will actually consult: the current UI culture, - // its parents, and "en-US" as the well-known system fallback that ships with every Windows - // SKU. A satellite present in some other locale subfolder would pass a broad existence - // check but the loader would never select it, so the test could still fail after skipping. - // The loop terminates naturally when culture.Name becomes empty (the invariant culture). - var probeOrder = new List(); - - for (var culture = CultureInfo.CurrentUICulture; !string.IsNullOrEmpty(culture.Name); culture = culture.Parent) - { - if (!probeOrder.Contains(culture.Name, StringComparer.OrdinalIgnoreCase)) - { - probeOrder.Add(culture.Name); - } - } - - if (!probeOrder.Contains("en-US", StringComparer.OrdinalIgnoreCase)) - { - probeOrder.Add("en-US"); - } - - foreach (var cultureName in probeOrder) - { - var candidate = Path.Combine(systemDirectory, cultureName, muiFileName); - - if (!File.Exists(candidate)) { continue; } - - satellitePath = candidate; - - return true; - } - - satellitePath = null; - - return false; - } - - private static bool TryReadOwningPublisher(EvtHandle channelConfig, out string? publisher) - { - publisher = null; - - bool success = NativeMethods.EvtGetChannelConfigProperty( - channelConfig, - EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, - 0, - 0, - IntPtr.Zero, - out int bufferSize); - - int error = Marshal.GetLastWin32Error(); - - if (!success && error != Win32ErrorCodes.ERROR_INSUFFICIENT_BUFFER) - { - return false; - } - - IntPtr buffer = Marshal.AllocHGlobal(bufferSize); - - try - { - success = NativeMethods.EvtGetChannelConfigProperty( - channelConfig, - EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, - 0, - bufferSize, - buffer, - out _); - - if (!success) - { - return false; - } - - var variant = Marshal.PtrToStructure(buffer); - publisher = NativeMethods.ConvertVariant(variant) as string; - - return !string.IsNullOrEmpty(publisher); - } - finally - { - Marshal.FreeHGlobal(buffer); - } - } } diff --git a/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj b/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj index fb0b03dd..089c31d8 100644 --- a/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj +++ b/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj @@ -12,6 +12,7 @@ + diff --git a/src/EventLogExpert.slnx b/src/EventLogExpert.slnx index bb5e4a76..1eaf083c 100644 --- a/src/EventLogExpert.slnx +++ b/src/EventLogExpert.slnx @@ -9,6 +9,7 @@ + From eacaa223811e7c6e7bf17526fd5b81967ad3251a Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 18:20:03 -0500 Subject: [PATCH 16/24] Tighten RegistryProvider integration test probes --- .../Providers/RegistryProviderTests.cs | 299 ++++++++++++------ 1 file changed, 195 insertions(+), 104 deletions(-) diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs index 3120347f..2730493c 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs @@ -3,13 +3,25 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; +using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; +using Microsoft.Win32; using NSubstitute; namespace EventLogExpert.Eventing.IntegrationTests.Providers; public sealed class RegistryProviderTests { + private static readonly string[] s_commonLegacyProviderNames = + [ + Constants.ApplicationLogName, + Constants.SystemLogName, + Constants.KernelGeneralLogName, + Constants.PowerShellLogName, + Constants.SecurityAuditingLogName, + Constants.ServiceControlManagerLogName + ]; + [Fact] public void Constructor_WhenCalledWithNullLogger_ShouldNotThrow() { @@ -37,24 +49,22 @@ public void GetMessageFilesForLegacyProvider_WhenCalled_ShouldNotIncludeSysFiles // Arrange var provider = new RegistryProvider(); - // Act - var allResults = new List(); - var commonProviders = new[] { Constants.ApplicationLogName, Constants.SystemLogName }; - - foreach (var providerName in commonProviders) - { - allResults.AddRange(provider.GetMessageFilesForLegacyProvider(providerName)); - } + // Act — drive the SUT through whichever well-known legacy provider is + // present on this host. The original test probed Application+System log + // names (which the SUT treats as provider names that produce no EMF on + // a standard Windows install); switch to the helper so the .sys filter + // is actually exercised against real provider data. + var result = FindAnyLegacyProviderFiles(provider); // Assert - if (allResults.Count != 0) - { - Assert.All(allResults, path => + Assert.NotEmpty(result); + + Assert.All(result, + path => { var extension = Path.GetExtension(path).ToLower(); Assert.NotEqual(".sys", extension); }); - } } [Fact] @@ -70,7 +80,9 @@ public void GetMessageFilesForLegacyProvider_WhenCalledWithLogger_ShouldLogTrace // Assert mockLogger.Received(1).Debug( - Arg.Is(h => h.ToString().Contains("GetMessageFilesForLegacyProvider called") && h.ToString().Contains(providerName))); + Arg.Is(h => + h.ToString().Contains("GetMessageFilesForLegacyProvider called") && + h.ToString().Contains(providerName))); } [Fact] @@ -107,11 +119,13 @@ public void GetMessageFilesForLegacyProvider_WhenCommonWindowsProvider_ShouldRet // Arrange var provider = new RegistryProvider(); - // Act - var result = provider.GetMessageFilesForLegacyProvider(Constants.ApplicationLogName).ToList(); + // Act — probe well-known legacy provider names (the original test passed + // a log name where the SUT expects a provider name, which produces no + // results on a standard Windows install). + var result = FindAnyLegacyProviderFiles(provider); // Assert - Assert.NotNull(result); + Assert.NotEmpty(result); } [Fact] @@ -149,50 +163,39 @@ public void GetMessageFilesForLegacyProvider_WhenLocalComputer_ShouldExpandEnvir // Arrange var provider = new RegistryProvider(); - // Act - Try to find any legacy provider + // Act var result = FindAnyLegacyProviderFiles(provider); // Assert - if (result.Count != 0) - { - // Verify paths are expanded (no %SystemRoot% or other variables) - Assert.All(result, path => + Assert.NotEmpty(result); + + Assert.All(result, + path => { Assert.DoesNotContain("%", path); }); - } } - /// - /// Tests that when a provider has multiple message files (including CategoryMessageFile), - /// they are all returned. This test is environment-dependent and will pass vacuously - /// if no providers with multiple message files exist on the system. - /// [Fact] public void GetMessageFilesForLegacyProvider_WhenMultipleMessageFilesExist_ShouldReturnAll() { // Arrange var provider = new RegistryProvider(); - var commonProviders = new[] { Constants.ApplicationLogName, Constants.SystemLogName }; - // Act - Find a provider with multiple message files - foreach (var providerName in commonProviders) + // Act + foreach (var providerName in s_commonLegacyProviderNames) { var result = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); - // Assert - If we found multiple files, verify the collection is valid if (result.Count > 1) { - Assert.NotEmpty(result); + // Assert Assert.All(result, file => Assert.False(string.IsNullOrWhiteSpace(file))); - return; // Test passed with actual validation + return; } } - // No providers with multiple message files found on this system. - // This is environment-dependent - the test passes vacuously but logs the condition. - // To make this test meaningful, run on a system with providers that have - // both EventMessageFile and CategoryMessageFile registry entries. + Assert.Skip("No common legacy provider on this host has multiple message files registered."); } [Fact] @@ -212,11 +215,12 @@ public async Task GetMessageFilesForLegacyProvider_WhenMultipleProviders_ShouldH await Task.WhenAll(tasks); // Assert - Assert.All(tasks, task => - { - Assert.NotNull(task.Result); - Assert.True(task.IsCompletedSuccessfully); - }); + Assert.All(tasks, + task => + { + Assert.NotNull(task.Result); + Assert.True(task.IsCompletedSuccessfully); + }); } [Fact] @@ -225,13 +229,20 @@ public void GetMessageFilesForLegacyProvider_WhenMultipleSemicolonSeparatedPaths // Arrange var provider = new RegistryProvider(); - // Act - Try to find any provider with multiple files - var result = FindAnyLegacyProviderFiles(provider); + // Act - Scan the registry for a legacy provider whose EventMessageFile + // value literally contains semicolon-separated paths and yields multiple + // .dll/.exe entries through the SUT's split + extension filter. + var (providerName, files) = FindLegacyProviderWithSemicolonSplitFiles(provider); - // Assert - // This test verifies the semicolon splitting logic works - // Even if we only get one file, the test passes as it shows the method works - Assert.NotNull(result); + // Assert - genuinely environment-dependent; not every host has a legacy + // provider registered with semicolon-separated EventMessageFile entries. + Assert.SkipUnless(providerName is not null, + "Test requires a legacy provider with semicolon-separated EventMessageFile entries on this host."); + + Assert.True(files.Count > 1, + $"Expected multiple files after semicolon split for provider '{providerName}', got {files.Count}."); + + Assert.All(files, file => Assert.False(string.IsNullOrWhiteSpace(file))); } [Fact] @@ -247,7 +258,9 @@ public void GetMessageFilesForLegacyProvider_WhenNoSubkeyFound_ShouldLogTerminal // Assert mockLogger.Received().Debug( - Arg.Is(h => h.ToString().Contains("No legacy EventMessageFile found for provider") && h.ToString().Contains(providerName))); + Arg.Is(h => + h.ToString().Contains("No legacy EventMessageFile found for provider") && + h.ToString().Contains(providerName))); } [Fact] @@ -280,21 +293,29 @@ public void GetMessageFilesForLegacyProvider_WhenProviderExists_ShouldReturnEnum [Fact] public void GetMessageFilesForLegacyProvider_WhenProviderFound_ShouldLogFoundMessage() { - // Arrange + // Arrange — discover a known-good provider name on this host first so + // the mock-logger assertion can be bound to that exact name. Without + // this binding, a stray "Found" debug emitted while probing an + // unrelated provider could vacuously satisfy the contract. + var probe = new RegistryProvider(); + var providerName = FindAnyLegacyProviderName(probe); + Assert.NotNull(providerName); + var mockLogger = Substitute.For(); var provider = new RegistryProvider(mockLogger); - // Act — drive the SUT through whichever well-known legacy provider is - // present on this host. The helper internally probes a fixed list and - // returns the first non-empty match. - var foundFiles = FindAnyLegacyProviderFiles(provider); - - // Assert — skip rather than silently pass when the host has no eligible - // legacy providers registered (e.g., a stripped-down container image). - Assert.SkipUnless(foundFiles.Count > 0, - "Test requires at least one common legacy provider with registered message files on this host."); + // Act — drive the SUT through the provider name we just confirmed + // yields a non-empty result on this host. + var foundFiles = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); + + // Assert — bind the "Found message file" debug emission to the specific + // provider name we asked for so a stray emission cannot mask a regression. + Assert.NotEmpty(foundFiles); + mockLogger.Received() - .Debug(Arg.Is(h => h.ToString().Contains("Found message file for legacy provider"))); + .Debug(Arg.Is(h => + h.ToString().Contains("Found message file for legacy provider") && + h.ToString().Contains(providerName))); } [Fact] @@ -307,14 +328,16 @@ public void GetMessageFilesForLegacyProvider_WhenReturnedPaths_ShouldBeDllOrExe( var result = FindAnyLegacyProviderFiles(provider); // Assert - Assert.SkipUnless(result.Count > 0, - "Test requires at least one common legacy provider with registered message files on this host."); - Assert.All(result, path => - { - var extension = Path.GetExtension(path).ToLower(); - Assert.True(extension is ".dll" or ".exe", - $"Expected .dll or .exe, but got {extension} for path: {path}"); - }); + Assert.NotEmpty(result); + + Assert.All(result, + path => + { + var extension = Path.GetExtension(path).ToLower(); + + Assert.True(extension is ".dll" or ".exe", + $"Expected .dll or .exe, but got {extension} for path: {path}"); + }); } [Fact] @@ -327,14 +350,15 @@ public void GetMessageFilesForLegacyProvider_WhenReturnedPaths_ShouldBeFullPaths var result = FindAnyLegacyProviderFiles(provider); // Assert - Assert.SkipUnless(result.Count > 0, - "Test requires at least one common legacy provider with registered message files on this host."); - Assert.All(result, path => - { - Assert.True( - Path.IsPathFullyQualified(path) || path.StartsWith(@"\\"), - $"Expected fully qualified path, but got: {path}"); - }); + Assert.NotEmpty(result); + + Assert.All(result, + path => + { + Assert.True( + Path.IsPathFullyQualified(path) || path.StartsWith(@"\\"), + $"Expected fully qualified path, but got: {path}"); + }); } [Fact] @@ -347,13 +371,13 @@ public void GetMessageFilesForLegacyProvider_WhenReturnedPaths_ShouldNotBeEmpty( var result = FindAnyLegacyProviderFiles(provider); // Assert - if (result.Count != 0) - { - Assert.All(result, path => + Assert.NotEmpty(result); + + Assert.All(result, + path => { Assert.False(string.IsNullOrWhiteSpace(path)); }); - } } [Fact] @@ -366,31 +390,31 @@ public void GetMessageFilesForLegacyProvider_WhenReturnedPaths_ShouldNotContainD var result = FindAnyLegacyProviderFiles(provider); // Assert - if (result.Any()) - { - var uniquePaths = result.Distinct().Count(); - Assert.Equal(result.Count, uniquePaths); - } + Assert.NotEmpty(result); + var uniquePaths = result.Distinct().Count(); + Assert.Equal(result.Count, uniquePaths); } [Fact] public void GetMessageFilesForLegacyProvider_WhenSameProviderMultipleTimes_ShouldReturnConsistentResults() { - // Arrange + // Arrange — probe for a known-good provider name (Application/System are + // log names, not provider names — the SUT returns empty for them). var provider = new RegistryProvider(); + var providerName = FindAnyLegacyProviderName(provider); + Assert.NotNull(providerName); // Act - var result1 = provider.GetMessageFilesForLegacyProvider(Constants.ApplicationLogName).ToList(); - var result2 = provider.GetMessageFilesForLegacyProvider(Constants.ApplicationLogName).ToList(); + var result1 = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); + var result2 = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); // Assert + Assert.NotEmpty(result1); Assert.Equal(result1.Count, result2.Count); - if (result1.Any()) + + for (int i = 0; i < result1.Count; i++) { - for (int i = 0; i < result1.Count; i++) - { - Assert.Equal(result1[i], result2[i]); - } + Assert.Equal(result1[i], result2[i]); } } @@ -425,17 +449,7 @@ public void GetMessageFilesForLegacyProvider_WhenWhitespaceProviderName_ShouldRe private static List FindAnyLegacyProviderFiles(RegistryProvider provider) { - var commonProviders = new[] - { - Constants.ApplicationLogName, - Constants.SystemLogName, - Constants.KernelGeneralLogName, - Constants.PowerShellLogName, - Constants.SecurityAuditingLogName, - Constants.ServiceControlManagerLogName - }; - - foreach (var providerName in commonProviders) + foreach (var providerName in s_commonLegacyProviderNames) { var result = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); @@ -447,4 +461,81 @@ private static List FindAnyLegacyProviderFiles(RegistryProvider provider return []; } + + private static string? FindAnyLegacyProviderName(RegistryProvider provider) + { + foreach (var providerName in s_commonLegacyProviderNames) + { + if (provider.GetMessageFilesForLegacyProvider(providerName).Any()) + { + return providerName; + } + } + + return null; + } + + private static (string? Name, List Files) FindLegacyProviderWithSemicolonSplitFiles( + RegistryProvider provider) + { + using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default); + using var eventLogKey = hklm.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\EventLog"); + + if (eventLogKey is null) + { + return (null, []); + } + + // Track provider names whose first non-empty EMF occurrence has already + // been evaluated. This mirrors the SUT, which returns from the first + // log subtree where a given provider name has a non-empty EMF. + var seenWithEmf = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var logName in eventLogKey.GetSubKeyNames()) + { + if (LogNames.AdminOnlyLiveLogNames.Contains(logName)) + { + continue; + } + + using var logSubKey = eventLogKey.OpenSubKey(logName); + + if (logSubKey is null) + { + continue; + } + + foreach (var providerName in logSubKey.GetSubKeyNames()) + { + using var providerSubKey = logSubKey.OpenSubKey(providerName); + + if (providerSubKey?.GetValue("EventMessageFile") is not string emf || string.IsNullOrEmpty(emf)) + { + continue; + } + + // First non-empty EMF wins. If this occurrence lacks ';', the + // SUT will not return semicolon-split content for this name on + // this host regardless of any later log subtree. + if (!seenWithEmf.Add(providerName)) + { + continue; + } + + if (!emf.Contains(';')) + { + continue; + } + + var files = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); + + if (files.Count > 1) + { + return (providerName, files); + } + } + } + + return (null, []); + } } From 67cf05b05f35bd83d3c102e0e8ee819651528eb5 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 18:54:07 -0500 Subject: [PATCH 17/24] Tighten thread-safety and vacuity in EventLogWatcherTests --- .../Readers/EventLogWatcherTests.cs | 311 ++++++++++++------ 1 file changed, 208 insertions(+), 103 deletions(-) diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs index 30c138e5..c3da4741 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs @@ -4,6 +4,7 @@ using EventLogExpert.Eventing.Models; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; +using System.Collections.Concurrent; using System.Diagnostics; namespace EventLogExpert.Eventing.IntegrationTests.Readers; @@ -132,29 +133,37 @@ public void Dispose_AfterDispose_ShouldNotReceiveEvents() { // Arrange var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); + int eventCount = 0; var eventReceived = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); + Interlocked.Increment(ref eventCount); eventReceived.Set(); }; watcher.Enabled = true; - // Act + // Act — dispose, then write a stimulus event to prove the handler is + // no longer wired up. EventLogWatcher.Dispose drains in-flight + // callbacks before returning. Snapshot the count and reset the signal + // *after* the drain so any ambient callbacks delivered during the + // live subscription window (between Enabled=true and Dispose) cannot + // bleed into the post-stimulus assertion. watcher.Dispose(); + int countBefore = Volatile.Read(ref eventCount); + eventReceived.Reset(); using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event after dispose", EventLogEntryType.Information); - bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken); + bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // Assert + int actual = Volatile.Read(ref eventCount); Assert.False(received, "Should not have received any event after dispose"); - Assert.Empty(receivedEvents); + Assert.Equal(countBefore, actual); } [Fact] @@ -351,19 +360,22 @@ public void EventRecordWritten_AfterResubscribe_ShouldReceiveEvents() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); + int eventCount = 0; var eventReceived = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); + Interlocked.Increment(ref eventCount); eventReceived.Set(); }; - // Act + // Act — toggle the watcher off then on. Safe to reset the counter + // and signal between Disable and the second Enable: Unsubscribe + // blocks until in-flight callbacks complete, so no handler can fire + // between the count clear and the Reset below. watcher.Enabled = true; watcher.Enabled = false; - receivedEvents.Clear(); + Interlocked.Exchange(ref eventCount, 0); eventReceived.Reset(); watcher.Enabled = true; @@ -375,8 +387,9 @@ public void EventRecordWritten_AfterResubscribe_ShouldReceiveEvents() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + int actual = Volatile.Read(ref eventCount); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotEmpty(receivedEvents); + Assert.True(actual > 0, $"Expected at least one event after resubscribe, but got {actual}."); } [Fact] @@ -384,29 +397,40 @@ public void EventRecordWritten_AfterUnsubscribe_ShouldStopReceivingEvents() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); + int eventCount = 0; var eventReceived = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); + Interlocked.Increment(ref eventCount); eventReceived.Set(); }; watcher.Enabled = true; - // Act + // Act — disable the watcher, then snapshot the post-action count and + // clear the signal accumulated during the populate phase. Safe: + // EventLogWatcher.Unsubscribe blocks until in-flight callbacks + // complete, so no handler can fire between the count capture and the + // Reset below. watcher.Enabled = false; - int countBeforeWait = receivedEvents.Count; - - // Safe: EventLogWatcher.Unsubscribe blocks until in-flight callbacks complete, - // so no handler can fire between the count capture and the Reset/Wait below. + int countBefore = Volatile.Read(ref eventCount); eventReceived.Reset(); - bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken); + + // The stimulus that proves Disable worked: write an event AFTER + // Disable and verify the handler does not fire. Without the stimulus + // the test would vacuously pass because no event would arrive in the + // wait window regardless of whether Disable did anything. + using var eventLog = new EventLog(Constants.ApplicationLogName); + eventLog.Source = Constants.ApplicationLogName; + eventLog.WriteEntry("Test event after unsubscribe", EventLogEntryType.Information); + + bool fired = eventReceived.Wait(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // Assert - Assert.False(received, "Should not have received any event after unsubscribe"); - Assert.Equal(countBeforeWait, receivedEvents.Count); + int actual = Volatile.Read(ref eventCount); + Assert.False(fired, "Should not have received any event after unsubscribe"); + Assert.Equal(countBefore, actual); } [Fact] @@ -419,7 +443,13 @@ public void EventRecordWritten_ShouldHaveRecordId() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + // Volatile.Write provides cross-thread visibility for the + // reference assignment; the test thread reads via Volatile.Read + // after the signal Wait. Last-event-wins semantics: a later + // ambient callback may overwrite this value before the test + // reads it, which is acceptable here because the assertion is + // an "any watcher event has this invariant" check. + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -433,10 +463,11 @@ public void EventRecordWritten_ShouldHaveRecordId() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + var snapshot = Volatile.Read(ref capturedEvent); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotNull(capturedEvent); - Assert.NotNull(capturedEvent.RecordId); - Assert.True(capturedEvent.RecordId > 0); + Assert.NotNull(snapshot); + Assert.NotNull(snapshot.RecordId); + Assert.True(snapshot.RecordId > 0); } [Fact] @@ -449,7 +480,7 @@ public void EventRecordWritten_ShouldHaveValidTimeCreated() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -466,11 +497,12 @@ public void EventRecordWritten_ShouldHaveValidTimeCreated() var afterWrite = DateTime.UtcNow.AddSeconds(1); // Add buffer for processing // Assert + var snapshot = Volatile.Read(ref capturedEvent); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotNull(capturedEvent); - Assert.NotEqual(default(DateTime), capturedEvent.TimeCreated); - Assert.True(capturedEvent.TimeCreated >= beforeWrite.AddSeconds(-1)); - Assert.True(capturedEvent.TimeCreated <= afterWrite); + Assert.NotNull(snapshot); + Assert.NotEqual(default(DateTime), snapshot.TimeCreated); + Assert.True(snapshot.TimeCreated >= beforeWrite.AddSeconds(-1)); + Assert.True(snapshot.TimeCreated <= afterWrite); } [Fact] @@ -483,7 +515,7 @@ public void EventRecordWritten_ShouldIncludePathName() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -497,9 +529,10 @@ public void EventRecordWritten_ShouldIncludePathName() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + var snapshot = Volatile.Read(ref capturedEvent); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotNull(capturedEvent); - Assert.Equal(Constants.ApplicationLogName, capturedEvent.PathName); + Assert.NotNull(snapshot); + Assert.Equal(Constants.ApplicationLogName, snapshot.PathName); } [Fact] @@ -512,7 +545,7 @@ public void EventRecordWritten_ShouldIncludeProperties() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -526,9 +559,10 @@ public void EventRecordWritten_ShouldIncludeProperties() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + var snapshot = Volatile.Read(ref capturedEvent); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotNull(capturedEvent); - Assert.NotNull(capturedEvent.Properties); + Assert.NotNull(snapshot); + Assert.NotNull(snapshot.Properties); } [Fact] @@ -541,7 +575,7 @@ public void EventRecordWritten_ShouldProvideCorrectSender() watcher.EventRecordWritten += (sender, record) => { - capturedSender = sender; + Volatile.Write(ref capturedSender, sender); eventReceived.Set(); }; @@ -555,9 +589,10 @@ public void EventRecordWritten_ShouldProvideCorrectSender() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + var snapshot = Volatile.Read(ref capturedSender); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotNull(capturedSender); - Assert.Same(watcher, capturedSender); + Assert.NotNull(snapshot); + Assert.Same(watcher, snapshot); } [Fact] @@ -565,17 +600,24 @@ public async Task EventRecordWritten_ShouldReceiveEventsInOrder() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); - var countdown = new CountdownEvent(3); + const int expectedCount = 3; + var receivedEvents = new ConcurrentQueue(); + int signalCount = 0; + var countdown = new CountdownEvent(expectedCount); watcher.EventRecordWritten += (sender, record) => { - lock (receivedEvents) + receivedEvents.Enqueue(record); + + // Guard CountdownEvent.Signal: ambient callbacks past the + // expected count would throw InvalidOperationException on the + // callback thread. + int count = Interlocked.Increment(ref signalCount); + + if (count <= expectedCount) { - receivedEvents.Add(record); + countdown.Signal(); } - - countdown.Signal(); }; watcher.Enabled = true; @@ -584,7 +626,6 @@ public async Task EventRecordWritten_ShouldReceiveEventsInOrder() using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; - var startTime = DateTime.UtcNow; eventLog.WriteEntry("Event A", EventLogEntryType.Information); await Task.Delay(TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); eventLog.WriteEntry("Event B", EventLogEntryType.Information); @@ -594,13 +635,17 @@ public async Task EventRecordWritten_ShouldReceiveEventsInOrder() bool received = countdown.Wait(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); // Assert + // Snapshot the queue: ConcurrentQueue.ToArray returns a moment-in- + // time copy in enqueue order. Late ambient callbacks may continue + // to enqueue after this point, but they will not affect the snapshot. + var snapshot = receivedEvents.ToArray(); Assert.True(received, "Did not receive all events within timeout period"); - Assert.True(receivedEvents.Count >= 3); + Assert.True(snapshot.Length >= expectedCount, $"Expected at least {expectedCount} events in snapshot, but got {snapshot.Length}."); // Verify events are in chronological order (by TimeCreated) - for (int i = 1; i < Math.Min(3, receivedEvents.Count); i++) + for (int i = 1; i < Math.Min(expectedCount, snapshot.Length); i++) { - Assert.True(receivedEvents[i].TimeCreated >= receivedEvents[i - 1].TimeCreated, + Assert.True(snapshot[i].TimeCreated >= snapshot[i - 1].TimeCreated, $"Event {i} was received before event {i - 1}"); } } @@ -610,12 +655,12 @@ public void EventRecordWritten_WhenErrorOccurs_ShouldReceiveEventRecordWithError { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); + var receivedEvents = new ConcurrentQueue(); var eventReceived = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); + receivedEvents.Enqueue(record); if (!eventReceived.IsSet) { @@ -633,10 +678,13 @@ public void EventRecordWritten_WhenErrorOccurs_ShouldReceiveEventRecordWithError bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + var snapshot = receivedEvents.ToArray(); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotEmpty(receivedEvents); - // Normal events should not have errors - Assert.All(receivedEvents, evt => Assert.Null(evt.Error)); + Assert.NotEmpty(snapshot); + // Assert only the first signaled record (the stimulus). Late ambient + // callbacks queued before ToArray could otherwise broaden the check + // beyond what the test stimulus controls. + Assert.Null(snapshot[0].Error); } [Fact] @@ -644,13 +692,21 @@ public void EventRecordWritten_WhenMultipleEventsWritten_ShouldReceiveAll() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); - var countdown = new CountdownEvent(3); + const int expectedCount = 3; + int eventCount = 0; + var countdown = new CountdownEvent(expectedCount); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); - countdown.Signal(); + // Guard CountdownEvent.Signal: the shared Application log can + // produce ambient callbacks that, if signaled past zero, throw + // InvalidOperationException on the callback thread. + int count = Interlocked.Increment(ref eventCount); + + if (count <= expectedCount) + { + countdown.Signal(); + } }; watcher.Enabled = true; @@ -666,30 +722,40 @@ public void EventRecordWritten_WhenMultipleEventsWritten_ShouldReceiveAll() bool received = countdown.Wait(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); // Assert - Assert.True(received, "Did not receive all events within timeout period"); - Assert.True(receivedEvents.Count >= 3, $"Expected at least 3 events, but got {receivedEvents.Count}"); + int actual = Volatile.Read(ref eventCount); + Assert.True(received, $"Did not receive all events within timeout period. Got {actual}."); + Assert.True(actual >= expectedCount, $"Expected at least {expectedCount} events, but got {actual}."); } [Fact] public void EventRecordWritten_WhenNotSubscribed_ShouldNotReceiveEvents() { - // Arrange + // Arrange — handler is attached via += but watcher.Enabled is never + // set to true, so no native subscription should be active. using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); + int eventCount = 0; var eventReceived = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); + Interlocked.Increment(ref eventCount); eventReceived.Set(); }; - // Act - bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken); + // Act — write an event to prove the negative case. Without this + // stimulus the test would vacuously pass because no event would + // arrive in the wait window regardless of whether the watcher was + // actually subscribed. + using var eventLog = new EventLog(Constants.ApplicationLogName); + eventLog.Source = Constants.ApplicationLogName; + eventLog.WriteEntry("Test event with watcher not enabled", EventLogEntryType.Information); + + bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // Assert - Assert.False(received, "Should not have received any event when not subscribed"); - Assert.Empty(receivedEvents); + int actual = Volatile.Read(ref eventCount); + Assert.False(received, "Should not have received any event when watcher is not Enabled"); + Assert.Equal(0, actual); } [Fact] @@ -697,12 +763,17 @@ public void EventRecordWritten_WhenSubscribed_ShouldReceiveEvents() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); + int eventCount = 0; + EventRecord? capturedEvent = null; var eventReceived = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); + // Capture the latest record for the per-record non-null + // invariant. Volatile.Write pairs with Volatile.Read in the + // assertion to guarantee cross-thread visibility. + Volatile.Write(ref capturedEvent, record); + Interlocked.Increment(ref eventCount); eventReceived.Set(); }; @@ -717,9 +788,11 @@ public void EventRecordWritten_WhenSubscribed_ShouldReceiveEvents() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + int actual = Volatile.Read(ref eventCount); + var snapshot = Volatile.Read(ref capturedEvent); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotEmpty(receivedEvents); - Assert.All(receivedEvents, evt => Assert.NotNull(evt)); + Assert.True(actual > 0, $"Expected at least one event, but got {actual}."); + Assert.NotNull(snapshot); } [Fact] @@ -732,12 +805,12 @@ public void EventRecordWritten_WithBookmark_ShouldReceiveNewEvents() var bookmark = reader.LastBookmark; using var watcher = new EventLogWatcher(Constants.ApplicationLogName, bookmark, renderXml: false); - var receivedEvents = new List(); + int eventCount = 0; var eventReceived = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); + Interlocked.Increment(ref eventCount); eventReceived.Set(); }; @@ -752,8 +825,9 @@ public void EventRecordWritten_WithBookmark_ShouldReceiveNewEvents() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + int actual = Volatile.Read(ref eventCount); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotEmpty(receivedEvents); + Assert.True(actual > 0, $"Expected at least one event after bookmark, but got {actual}."); } [Fact] @@ -761,17 +835,21 @@ public async Task EventRecordWritten_WithConcurrentEventWrites_ShouldHandleAllEv { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); - var countdown = new CountdownEvent(5); + const int expectedCount = 5; + int eventCount = 0; + var countdown = new CountdownEvent(expectedCount); watcher.EventRecordWritten += (sender, record) => { - lock (receivedEvents) + // Guard CountdownEvent.Signal: ambient callbacks past the + // expected count would throw InvalidOperationException on the + // callback thread. + int count = Interlocked.Increment(ref eventCount); + + if (count <= expectedCount) { - receivedEvents.Add(record); + countdown.Signal(); } - - countdown.Signal(); }; watcher.Enabled = true; @@ -815,8 +893,9 @@ public async Task EventRecordWritten_WithConcurrentEventWrites_ShouldHandleAllEv bool received = countdown.Wait(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); // Assert - Assert.True(received, $"Did not receive all events within timeout period. Got {receivedEvents.Count} events"); - Assert.True(receivedEvents.Count >= 5, $"Expected at least 5 events, but got {receivedEvents.Count}"); + int actual = Volatile.Read(ref eventCount); + Assert.True(received, $"Did not receive all events within timeout period. Got {actual} events."); + Assert.True(actual >= expectedCount, $"Expected at least {expectedCount} events, but got {actual}."); } [Fact] @@ -843,20 +922,30 @@ public void EventRecordWritten_WithMultipleSubscribers_ShouldNotifyAll() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents1 = new List(); - var receivedEvents2 = new List(); + const int expectedCountPerSubscriber = 1; + int eventCount1 = 0; + int eventCount2 = 0; var countdown = new CountdownEvent(2); watcher.EventRecordWritten += (sender, record) => { - receivedEvents1.Add(record); - countdown.Signal(); + // Guard CountdownEvent.Signal against ambient over-signal. + int count = Interlocked.Increment(ref eventCount1); + + if (count <= expectedCountPerSubscriber) + { + countdown.Signal(); + } }; watcher.EventRecordWritten += (sender, record) => { - receivedEvents2.Add(record); - countdown.Signal(); + int count = Interlocked.Increment(ref eventCount2); + + if (count <= expectedCountPerSubscriber) + { + countdown.Signal(); + } }; watcher.Enabled = true; @@ -869,9 +958,11 @@ public void EventRecordWritten_WithMultipleSubscribers_ShouldNotifyAll() bool received = countdown.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + int actual1 = Volatile.Read(ref eventCount1); + int actual2 = Volatile.Read(ref eventCount2); Assert.True(received, "Did not receive events in all subscribers within timeout period"); - Assert.NotEmpty(receivedEvents1); - Assert.NotEmpty(receivedEvents2); + Assert.True(actual1 > 0, $"Subscriber 1 expected at least one event, but got {actual1}."); + Assert.True(actual2 > 0, $"Subscriber 2 expected at least one event, but got {actual2}."); } [Fact] @@ -903,7 +994,7 @@ public void EventRecordWritten_WithRenderXmlFalse_ShouldNotIncludeXml() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -917,9 +1008,10 @@ public void EventRecordWritten_WithRenderXmlFalse_ShouldNotIncludeXml() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + var snapshot = Volatile.Read(ref capturedEvent); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotNull(capturedEvent); - Assert.Null(capturedEvent.Xml); + Assert.NotNull(snapshot); + Assert.Null(snapshot.Xml); } [Fact] @@ -932,7 +1024,7 @@ public void EventRecordWritten_WithRenderXmlTrue_ShouldIncludeXml() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -946,9 +1038,10 @@ public void EventRecordWritten_WithRenderXmlTrue_ShouldIncludeXml() bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + var snapshot = Volatile.Read(ref capturedEvent); Assert.True(received, "Did not receive event within timeout period"); - Assert.NotNull(capturedEvent); - Assert.False(string.IsNullOrWhiteSpace(capturedEvent.Xml)); + Assert.NotNull(snapshot); + Assert.False(string.IsNullOrWhiteSpace(snapshot.Xml)); } [Fact] @@ -974,20 +1067,30 @@ public void Multiple_Watchers_OnSameLog_ShouldAllReceiveEvents() using var watcher1 = new EventLogWatcher(Constants.ApplicationLogName); using var watcher2 = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents1 = new List(); - var receivedEvents2 = new List(); + const int expectedCountPerWatcher = 1; + int eventCount1 = 0; + int eventCount2 = 0; var countdown = new CountdownEvent(2); watcher1.EventRecordWritten += (sender, record) => { - receivedEvents1.Add(record); - countdown.Signal(); + // Guard CountdownEvent.Signal against ambient over-signal. + int count = Interlocked.Increment(ref eventCount1); + + if (count <= expectedCountPerWatcher) + { + countdown.Signal(); + } }; watcher2.EventRecordWritten += (sender, record) => { - receivedEvents2.Add(record); - countdown.Signal(); + int count = Interlocked.Increment(ref eventCount2); + + if (count <= expectedCountPerWatcher) + { + countdown.Signal(); + } }; watcher1.Enabled = true; @@ -1001,9 +1104,11 @@ public void Multiple_Watchers_OnSameLog_ShouldAllReceiveEvents() bool received = countdown.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert + int actual1 = Volatile.Read(ref eventCount1); + int actual2 = Volatile.Read(ref eventCount2); Assert.True(received, "Did not receive events in all watchers within timeout period"); - Assert.NotEmpty(receivedEvents1); - Assert.NotEmpty(receivedEvents2); + Assert.True(actual1 > 0, $"Watcher 1 expected at least one event, but got {actual1}."); + Assert.True(actual2 > 0, $"Watcher 2 expected at least one event, but got {actual2}."); } [Fact] From 98c0854f827e7c678ae8d313a8f46deb4c7515ca Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 19:33:14 -0500 Subject: [PATCH 18/24] Strengthen resolver fallback tests and convert provider loop to Theory --- .../EventResolvers/EventResolverBaseTests.cs | 76 ++++++++++ .../VersatileEventResolverTests.cs | 136 +++++++++++++++--- 2 files changed, 192 insertions(+), 20 deletions(-) diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs index 07c39e04..4a8e52ec 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs @@ -2613,6 +2613,82 @@ public void ResolveEvent_WithPopulatedProviderButUnknownEventAndMultipleProperti Assert.DoesNotContain("Failed to resolve", displayEvent.Description); } + [Fact] + public void ResolveEvent_WithPrimaryAndSupplementalDetailsButUnmappedEventId_ShouldReturnNoMatchingMessage() + { + // Arrange — both primary and supplemental have populated metadata with mapped + // legacy messages, but neither has a message matching the requested EventId. + // This exercises the fallback path where supplemental IS loaded (because + // primary's GetMessagesByShortId count == 0 for this Id) and supplemental + // also fails to match — descriptionDetails stays on primary, supplemental + // is forwarded into CreateEventModel as a present-but-unhelpful argument. + // Distinct from WithPopulatedProviderButUnknownEvent... which uses + // TestEventResolver (supplemental is always null). + // + // To prove supplemental was actually loaded AND forwarded into CreateEventModel + // (rather than the test passing trivially via the primary-only no-match path), + // we seed a task ONLY in supplemental and set eventRecord.Task to match. The + // ResolveTaskName fallback only consults supplemental when primary has no entry; + // observing the supplemental task name in displayEvent.TaskCategory therefore + // requires both supplemental lookup and supplemental forwarding into the model. + const string SupplementalOnlyTaskName = "SupplementalOnlyTask"; + const int SharedTaskId = 42; + + var primaryDetails = new ProviderDetails + { + ProviderName = Constants.TestProviderName, + Events = [], + Messages = + [ + new MessageModel { ProviderName = Constants.TestProviderName, ShortId = 100, Text = "Primary event 100" } + ], + Parameters = [], + Keywords = new Dictionary(), + Opcodes = new Dictionary(), + Tasks = new Dictionary() + }; + + var supplementalDetails = new ProviderDetails + { + ProviderName = Constants.TestProviderName, + Events = [], + Messages = + [ + new MessageModel { ProviderName = Constants.TestProviderName, ShortId = 200, Text = "Supplemental event 200" } + ], + Parameters = [], + Keywords = new Dictionary(), + Opcodes = new Dictionary(), + Tasks = new Dictionary { { SharedTaskId, SupplementalOnlyTaskName } } + }; + + var resolver = new SupplementalTestResolver([primaryDetails], supplementalDetails); + + var eventRecord = new EventRecord + { + ProviderName = Constants.TestProviderName, + Id = 9999, + Task = SharedTaskId, + Properties = [] + }; + + // Act + var displayEvent = resolver.ResolveEvent(eventRecord); + + // Assert + Assert.NotNull(displayEvent); + Assert.Contains("No matching message", displayEvent.Description); + Assert.DoesNotContain("No matching provider", displayEvent.Description); + Assert.DoesNotContain("Failed to resolve", displayEvent.Description); + // Neither primary nor supplemental message text should leak into the description. + Assert.DoesNotContain("Primary event 100", displayEvent.Description); + Assert.DoesNotContain("Supplemental event 200", displayEvent.Description); + // Proves supplemental was loaded AND forwarded into CreateEventModel: the + // supplemental-only task name only appears via the supplemental fallback in + // ResolveTaskName. + Assert.Equal(SupplementalOnlyTaskName, displayEvent.TaskCategory); + } + [Fact] public void ResolveEvent_WithPrimaryAndSupplementalTask_ShouldPreferPrimaryTaskName() { diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs index 500408aa..67e9b920 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs @@ -345,32 +345,94 @@ public void ResolveEvent_WithLocalResolver_ShouldResolveEvent() } [Fact] - public void ResolveEvent_WithMultipleProviders_ShouldResolveAll() + public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseTextToDbProvider() { - // Arrange - using var resolver = new EventResolver(); + // Documents that a single resolver instance cleanly handles both DB-resolved + // and locally-resolved providers. The DB-loaded provider's seeded message text + // must not leak into the description for an unrelated unknown provider — + // that would indicate a per-provider cache-isolation bug in EventResolver. + const string DBProviderName = "MixedScenarioDbProvider"; + const string DBMessageText = "Resolved from test database for mixed scenario"; + const ushort EventId = 1000; + + string unknownProviderName = $"NotInDb-{Guid.NewGuid()}"; + string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); - var providers = new[] + try { - Constants.ApplicationLogName, - Constants.SystemLogName, - Constants.TestProviderName - }; + using (var context = new EventProviderDbContext(dbPath, false)) + { + context.ProviderDetails.Add(new ProviderDetails + { + ProviderName = DBProviderName, + Messages = + [ + new MessageModel + { + ProviderName = DBProviderName, + ShortId = (short)EventId, + Text = DBMessageText + } + ] + }); - // Act & Assert - foreach (var provider in providers) - { - var eventRecord = new EventRecord + context.SaveChanges(); + } + + var dbCollection = Substitute.For(); + dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); + + DisplayEventModel dbDisplayEvent; + DisplayEventModel unknownDisplayEvent; + + using (var resolver = new EventResolver(dbCollection)) { - ProviderName = provider, - Id = 1000, - ComputerName = "TestComputer", - LogName = Constants.ApplicationLogName - }; + var dbEventRecord = new EventRecord + { + ProviderName = DBProviderName, + Id = EventId, + ComputerName = "TestComputer", + LogName = Constants.ApplicationLogName + }; - var displayEvent = resolver.ResolveEvent(eventRecord); - Assert.NotNull(displayEvent); - Assert.Equal(provider, displayEvent.Source); + var unknownEventRecord = new EventRecord + { + ProviderName = unknownProviderName, + Id = EventId, + ComputerName = "TestComputer", + LogName = Constants.ApplicationLogName + }; + + // Act — same resolver instance resolves both. The DB-known provider + // hits TryResolveFromDatabase; the unknown provider falls through to + // ResolveFromLocalProvider. + dbDisplayEvent = resolver.ResolveEvent(dbEventRecord); + unknownDisplayEvent = resolver.ResolveEvent(unknownEventRecord); + } + + // Assert + Assert.NotNull(dbDisplayEvent); + Assert.Equal(DBProviderName, dbDisplayEvent.Source); + Assert.Contains(DBMessageText, dbDisplayEvent.Description); + + Assert.NotNull(unknownDisplayEvent); + Assert.Equal(unknownProviderName, unknownDisplayEvent.Source); + Assert.DoesNotContain(DBMessageText, unknownDisplayEvent.Description); + // Unknown provider must hit a fallback "No matching ..." path (provider-level + // or message-level) rather than the "Failed to resolve" exception path or + // accidentally inheriting the DB-known provider's description. + Assert.Contains("No matching", unknownDisplayEvent.Description); + Assert.DoesNotContain("Failed to resolve", unknownDisplayEvent.Description); + } + finally + { + if (File.Exists(dbPath)) + { + SqliteConnection.ClearAllPools(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + File.Delete(dbPath); + } } } @@ -396,6 +458,40 @@ public void ResolveEvent_WithNonExistentProvider_ShouldReturnDefaultDescription( Assert.Contains("No matching", displayEvent.Description); } + [Theory] + [InlineData(Constants.ApplicationLogName)] + [InlineData(Constants.SystemLogName)] + [InlineData(Constants.TestProviderName)] + public void ResolveEvent_WithProvider_ShouldReturnDisplayEventWithMatchingFields(string providerName) + { + // Verifies model-construction round-trip: per-record fields (Id, ComputerName) + // and the requested ProviderName flow through CreateEventModel into the + // DisplayEventModel for representative provider names. This is intentionally + // a model-propagation test, not a provider-resolution-behavior test — none of + // the provider names are seeded with metadata, so each row exercises the same + // local-fallback path. The Theory shape over a foreach loop gives per-case + // diagnostics if one provider name regresses while others continue to pass. + // Arrange + using var resolver = new EventResolver(); + + var eventRecord = new EventRecord + { + ProviderName = providerName, + Id = 1000, + ComputerName = "TestComputer", + LogName = Constants.ApplicationLogName + }; + + // Act + var displayEvent = resolver.ResolveEvent(eventRecord); + + // Assert + Assert.NotNull(displayEvent); + Assert.Equal(providerName, displayEvent.Source); + Assert.Equal(eventRecord.Id, displayEvent.Id); + Assert.Equal(eventRecord.ComputerName, displayEvent.ComputerName); + } + [Fact] public void ResolveProviderDetails_CalledTwice_ShouldHandleCorrectly() { From be201acd0d4868a816effc9b5921eebb1e96bc11 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Thu, 7 May 2026 20:34:57 -0500 Subject: [PATCH 19/24] Isolate handler exceptions and reject reentrant stop in EventLogWatcher --- .../Readers/EventLogWatcherTests.cs | 314 +++++++++++++++++- .../Readers/EventLogWatcher.cs | 311 ++++++++++++----- 2 files changed, 528 insertions(+), 97 deletions(-) diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs index c3da4741..c051f8fa 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Tests.TestUtils.Constants; using System.Collections.Concurrent; using System.Diagnostics; +using System.Reflection; namespace EventLogExpert.Eventing.IntegrationTests.Readers; @@ -177,6 +178,42 @@ public void Dispose_BeforeSubscribe_ShouldNotThrow() watcher.Dispose(); } + [Fact] + public void Dispose_ShouldReleaseUnderlyingWaitHandle() + { + // Arrange + var watcher = new EventLogWatcher(Constants.ApplicationLogName); + watcher.Enabled = true; + + // Reach into the private AutoResetEvent the watcher owns so we can + // assert the kernel handle is released — the leak this test guards + // against (per Copilot review on PR #516) was discovered precisely + // because there was no externally-observable signal of disposal. + var newEventsField = typeof(EventLogWatcher).GetField( + "_newEvents", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.True( + newEventsField is not null, + "Private field _newEvents not found on EventLogWatcher — was it renamed? Update Dispose_ShouldReleaseUnderlyingWaitHandle."); + + var newEvents = (AutoResetEvent?)newEventsField.GetValue(watcher); + Assert.NotNull(newEvents); + + // Act + watcher.Dispose(); + + // Assert + Assert.True( + newEvents.SafeWaitHandle.IsClosed, + "AutoResetEvent.SafeWaitHandle.IsClosed should be true after Dispose; otherwise the OS event handle leaks across watcher lifetimes."); + + // Idempotency: the field-level Dispose unconditionally calls + // _newEvents.Dispose(), so a second top-level Dispose() must be + // gated by the _disposed fast-path to avoid a second call. + watcher.Dispose(); + Assert.True(newEvents.SafeWaitHandle.IsClosed); + } + [Fact] public void Dispose_WhenCalled_ShouldUnsubscribe() { @@ -191,6 +228,54 @@ public void Dispose_WhenCalled_ShouldUnsubscribe() Assert.False(watcher.Enabled); } + [Fact] + public void Dispose_WhenCalledFromHandler_ShouldThrowInvalidOperationException() + { + // Arrange + using var watcher = new EventLogWatcher(Constants.ApplicationLogName); + Exception? observed = null; + var observedSet = new ManualResetEventSlim(false); + + watcher.EventRecordWritten += (sender, record) => + { + // Capture the exception inside the handler — the SUT isolates + // subscriber exceptions per its public contract, so an uncaught + // throw here would be silently swallowed and the test would hang. + try + { + ((EventLogWatcher)sender!).Dispose(); + } + catch (Exception ex) + { + Volatile.Write(ref observed, ex); + } + finally + { + observedSet.Set(); + } + }; + + watcher.Enabled = true; + + // Act + using var eventLog = new EventLog(Constants.ApplicationLogName); + eventLog.Source = Constants.ApplicationLogName; + eventLog.WriteEntry("Test event for reentrant Dispose", EventLogEntryType.Information); + + bool received = observedSet.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + + // Assert + Assert.True(received, "Handler was never invoked, so reentrancy was not exercised"); + var captured = Volatile.Read(ref observed); + Assert.NotNull(captured); + var ex = Assert.IsType(captured); + Assert.Contains("cannot be stopped from within", ex.Message); + + // Cleanup happens via `using var watcher` from the test thread, which + // is not the ThreadPool callback thread, so the reentrancy guard does + // not trigger and Dispose completes normally. + } + [Fact] public void Dispose_WhenCalledMultipleTimes_ShouldNotThrow() { @@ -256,6 +341,45 @@ public void Enabled_WhenCreated_ShouldBeFalse() Assert.False(watcher.Enabled); } + [Fact] + public async Task Enabled_WhenRacingFromMultipleThreads_ShouldNotHangOrCorruptState() + { + // Arrange + using var watcher = new EventLogWatcher(Constants.ApplicationLogName); + + // Act: race many Subscribe/Unsubscribe pairs to maximize the window + // for Subscribe to mutate _subscriptionHandle/_waitHandle while + // another thread is in Unsubscribe. Without the lifecycle lock, two + // Enabled=false callers can both pass the _waitHandle null check and + // both call RegisteredWaitHandle.Unregister; the second call returns + // false (already unregistered) and does NOT signal its wait object, + // so its WaitOne blocks indefinitely. Separately, an Enabled=true + // racing with Enabled=false can leave _subscriptionHandle disposed + // mid-Subscribe, making the next ProcessNewEvents invocation crash + // on a closed native handle. + var tasks = new List(capacity: 100); + var ct = TestContext.Current.CancellationToken; + + for (int i = 0; i < 50; i++) + { + tasks.Add(Task.Run(() => + { + try { watcher.Enabled = true; } + catch (InvalidOperationException) { /* loser of a Subscribe race — expected */ } + }, ct)); + tasks.Add(Task.Run(() => watcher.Enabled = false, ct)); + } + + var allDone = Task.WhenAll(tasks); + + // Assert: 30s budget far exceeds any legitimate Subscribe/Unsubscribe + // drain across 100 toggles. A TimeoutException here means the + // RegisteredWaitHandle race regression has returned. Any other + // exception (e.g., ObjectDisposedException from a torn-down handle) + // surfaces via the await. + await allDone.WaitAsync(TimeSpan.FromSeconds(30), ct); + } + [Fact] public void Enabled_WhenSetToFalse_ShouldUnsubscribe() { @@ -270,6 +394,50 @@ public void Enabled_WhenSetToFalse_ShouldUnsubscribe() Assert.False(watcher.Enabled); } + [Fact] + public void Enabled_WhenSetToFalseFromHandler_ShouldThrowInvalidOperationException() + { + // Arrange + using var watcher = new EventLogWatcher(Constants.ApplicationLogName); + Exception? observed = null; + var observedSet = new ManualResetEventSlim(false); + + watcher.EventRecordWritten += (sender, record) => + { + // Same reentrancy contract as Dispose: Enabled = false also calls + // Unsubscribe, which blocks waiting for in-flight callbacks. Calling + // it from a callback would self-deadlock, so the SUT throws. + try + { + ((EventLogWatcher)sender!).Enabled = false; + } + catch (Exception ex) + { + Volatile.Write(ref observed, ex); + } + finally + { + observedSet.Set(); + } + }; + + watcher.Enabled = true; + + // Act + using var eventLog = new EventLog(Constants.ApplicationLogName); + eventLog.Source = Constants.ApplicationLogName; + eventLog.WriteEntry("Test event for reentrant Enabled=false", EventLogEntryType.Information); + + bool received = observedSet.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + + // Assert + Assert.True(received, "Handler was never invoked, so reentrancy was not exercised"); + var captured = Volatile.Read(ref observed); + Assert.NotNull(captured); + var ex = Assert.IsType(captured); + Assert.Contains("cannot be stopped from within", ex.Message); + } + [Fact] public void Enabled_WhenSetToFalseTwice_ShouldNotThrow() { @@ -308,6 +476,21 @@ public void Enabled_WhenSetToTrue_ShouldSubscribe() Assert.True(watcher.Enabled); } + [Fact] + public void Enabled_WhenSetToTrueAfterDispose_ShouldThrowObjectDisposedException() + { + // Arrange + var watcher = new EventLogWatcher(Constants.ApplicationLogName); + watcher.Dispose(); + + // Act & Assert: post-dispose Subscribe is gated by an explicit + // _disposed check inside the lifecycle lock so callers see a clean + // ObjectDisposedException instead of a confusing native-handle + // failure (e.g., from _newEvents.SafeWaitHandle access after + // _newEvents has itself been disposed). + Assert.Throws(() => watcher.Enabled = true); + } + [Fact] public void Enabled_WhenSetToTrueTwice_ShouldNotThrow() { @@ -433,6 +616,48 @@ public void EventRecordWritten_AfterUnsubscribe_ShouldStopReceivingEvents() Assert.Equal(countBefore, actual); } + [Fact] + public void EventRecordWritten_ForNormalEvent_ShouldHaveNullError() + { + // Arrange + using var watcher = new EventLogWatcher(Constants.ApplicationLogName); + var receivedEvents = new ConcurrentQueue(); + var eventReceived = new ManualResetEventSlim(false); + + watcher.EventRecordWritten += (sender, record) => + { + receivedEvents.Enqueue(record); + + if (!eventReceived.IsSet) + { + eventReceived.Set(); + } + }; + + watcher.Enabled = true; + + // Act — Write a normal event. EventRecord.Error is populated by + // ProcessNewEvents only when EvtRender throws, which is not reachable + // through the public API for a well-formed log entry. This test pins + // the happy-path invariant (Error is null on a normal event); see + // WithInvalidBookmark_ShouldThrowAndNotMaskAsUnauthorizedAccessException + // for an exercise of the failure path. + using var eventLog = new EventLog(Constants.ApplicationLogName); + eventLog.Source = Constants.ApplicationLogName; + eventLog.WriteEntry("Test event for normal-path null Error invariant", EventLogEntryType.Information); + + bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + + // Assert + var snapshot = receivedEvents.ToArray(); + Assert.True(received, "Did not receive event within timeout period"); + Assert.NotEmpty(snapshot); + // Assert only the first signaled record (the stimulus). Late ambient + // callbacks queued before ToArray could otherwise broaden the check + // beyond what the test stimulus controls. + Assert.Null(snapshot[0].Error); + } + [Fact] public void EventRecordWritten_ShouldHaveRecordId() { @@ -651,40 +876,53 @@ public async Task EventRecordWritten_ShouldReceiveEventsInOrder() } [Fact] - public void EventRecordWritten_WhenErrorOccurs_ShouldReceiveEventRecordWithError() + public void EventRecordWritten_WhenHandlerThrows_ShouldStillDeliverFutureEvents() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new ConcurrentQueue(); - var eventReceived = new ManualResetEventSlim(false); + int attempts = 0; + var firstObserved = new ManualResetEventSlim(false); + var laterObserved = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Enqueue(record); - - if (!eventReceived.IsSet) + // Signal "first invocation observed" BEFORE throwing so the + // stimulus thread can release the second write only after the + // throw has happened. Per-subscriber isolation in the SUT + // (introduced alongside this test) is what lets later invocations + // fire at all — without it the throw would short-circuit the + // multicast invoke and bubble into the ThreadPool worker. + int n = Interlocked.Increment(ref attempts); + + if (n == 1) { - eventReceived.Set(); + firstObserved.Set(); + throw new InvalidOperationException("simulated handler failure"); } + + laterObserved.Set(); }; watcher.Enabled = true; - // Act - Write a normal event (error handling is internal, hard to trigger externally) using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; - eventLog.WriteEntry("Test event for error case", EventLogEntryType.Information); - bool received = eventReceived.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + // Act — write the stimulus, wait for the first (throwing) invocation, + // then write a second event to prove the watcher still delivers. + eventLog.WriteEntry("Test event 1 (handler throws on first invocation)", EventLogEntryType.Information); + bool firstReceived = firstObserved.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + Assert.True(firstReceived, "First handler invocation was not observed"); + + eventLog.WriteEntry("Test event 2 (handler should still fire)", EventLogEntryType.Information); + bool laterReceived = laterObserved.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Assert - var snapshot = receivedEvents.ToArray(); - Assert.True(received, "Did not receive event within timeout period"); - Assert.NotEmpty(snapshot); - // Assert only the first signaled record (the stimulus). Late ambient - // callbacks queued before ToArray could otherwise broaden the check - // beyond what the test stimulus controls. - Assert.Null(snapshot[0].Error); + int actual = Volatile.Read(ref attempts); + Assert.True(laterReceived, + $"Handler did not fire again after throwing on first invocation; attempts={actual}"); + Assert.True(actual >= 2, + $"Expected at least 2 handler invocations after recovery, got {actual}"); } [Fact] @@ -758,6 +996,48 @@ public void EventRecordWritten_WhenNotSubscribed_ShouldNotReceiveEvents() Assert.Equal(0, actual); } + [Fact] + public void EventRecordWritten_WhenOneOfMultipleHandlersThrows_ShouldStillNotifyOthers() + { + // Arrange + using var watcher = new EventLogWatcher(Constants.ApplicationLogName); + int failingCount = 0; + int succeedingCount = 0; + var succeedingObserved = new ManualResetEventSlim(false); + + // First subscriber always throws — simulates a poorly written consumer. + // Per-subscriber isolation must prevent it from blocking the second. + watcher.EventRecordWritten += (sender, record) => + { + Interlocked.Increment(ref failingCount); + throw new InvalidOperationException("subscriber A always fails"); + }; + + // Second subscriber must still be invoked despite the first throwing. + watcher.EventRecordWritten += (sender, record) => + { + Interlocked.Increment(ref succeedingCount); + succeedingObserved.Set(); + }; + + watcher.Enabled = true; + + // Act + using var eventLog = new EventLog(Constants.ApplicationLogName); + eventLog.Source = Constants.ApplicationLogName; + eventLog.WriteEntry("Test event for multi-subscriber isolation", EventLogEntryType.Information); + + bool received = succeedingObserved.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + + // Assert + int failed = Volatile.Read(ref failingCount); + int succeeded = Volatile.Read(ref succeedingCount); + Assert.True(received, + $"Second subscriber was never invoked despite first subscriber throwing; failed={failed}, succeeded={succeeded}"); + Assert.True(failed >= 1, $"Expected first (throwing) subscriber to be invoked at least once, got {failed}"); + Assert.True(succeeded >= 1, $"Expected second (succeeding) subscriber to be invoked at least once, got {succeeded}"); + } + [Fact] public void EventRecordWritten_WhenSubscribed_ShouldReceiveEvents() { diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index e1485b6a..c539296a 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -3,17 +3,38 @@ using EventLogExpert.Eventing.Interop; using EventLogExpert.Eventing.Models; +using System.Collections.Concurrent; using System.Runtime.InteropServices; namespace EventLogExpert.Eventing.Readers; -public sealed partial class EventLogWatcher : IDisposable +public sealed class EventLogWatcher : IDisposable { private readonly string? _bookmark; + // Tracks ManagedThreadIds currently inside ProcessNewEvents. Used by + // Dispose() and the Enabled=false path to detect a handler trying to stop + // the watcher from inside its own callback, which would self-deadlock on + // Unsubscribe's drain wait. Non-disposable so it stays safe across + // idempotent Dispose calls and finalizer paths. + private readonly ConcurrentDictionary _callbackThreadIds = new(); private readonly AutoResetEvent _newEvents = new(false); private readonly string _path; private readonly EvtHandle _queryHandle; private readonly bool _renderXml; + // Serializes lifecycle transitions: Subscribe (via Enabled=true), Unsubscribe + // (via Enabled=false or Dispose), and the disposed-state check that gates + // Subscribe. Without this, two concurrent threads can both pass the + // _waitHandle null check in Unsubscribe and both call + // RegisteredWaitHandle.Unregister; the second call returns false (already + // unregistered) and does NOT signal its wait object, so WaitOne blocks + // indefinitely. Beyond that race, Subscribe and Unsubscribe both mutate + // _subscriptionHandle/_waitHandle/_isSubscribed; without the lock, + // Enabled=true racing with Enabled=false (or Dispose) can leave + // _subscriptionHandle disposed but ProcessNewEvents already running over + // it. The lock also makes the loser's Unsubscribe wait for the winner's + // drain to complete — required so both callers honor the "no callback + // fires after Unsubscribe returns" contract. + private readonly Lock _lifecycleLock = new(); private int _disposed; private bool _isSubscribed; @@ -42,11 +63,31 @@ public EventLogWatcher(string path, bool renderXml = false) : this(path, null, r ~EventLogWatcher() { - Dispose(disposing: false); + Dispose(false); } + /// + /// Raised when one or more new event records arrive from the underlying Windows EventLog subscription; subscriber + /// exceptions are caught and isolated, and synchronously calling or setting + /// to false from inside a handler throws + /// unless another thread has already begun disposing or unsubscribing the watcher (in which case the call is a + /// silent no-op for IDisposable idempotency); schedule the stop on a different thread to avoid the throw entirely. + /// public event EventHandler? EventRecordWritten; + /// + /// Gets or sets whether the watcher is actively subscribed to the log; setting false tears down the native + /// subscription and blocks until any in-flight callbacks complete. + /// + /// + /// Thrown when the setter is invoked with a value of false from inside an + /// handler while the watcher is still subscribed; the unsubscribe path waits + /// for in-flight callbacks to complete and would self-deadlock the calling thread. If another thread has already + /// unsubscribed or disposed the watcher, the call is a silent no-op. + /// + /// + /// Thrown when the setter is invoked with a value of true after the watcher has been disposed. + /// public bool Enabled { get @@ -61,20 +102,52 @@ public bool Enabled Subscribe(); break; case false when _isSubscribed: + ThrowIfCurrentCallbackWouldDeadlock(); Unsubscribe(); break; } } } + /// + /// Releases the native subscription and query handles; idempotent and safe to call multiple times from any + /// non-callback thread. + /// + /// + /// Thrown when invoked from inside an handler while the watcher is still live. + /// The unsubscribe path waits for in-flight callbacks to complete and would self-deadlock the calling thread. + /// If another thread has already begun disposing the watcher, the call is a silent no-op for IDisposable + /// idempotency. + /// public void Dispose() { - Dispose(disposing: true); + Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { + // Idempotent fast path: if another thread already disposed (or is + // disposing), reentrant calls — including ones that originate from + // inside a callback after the disposing thread has already won the + // CAS — must be a no-op. This preserves the IDisposable.Dispose + // idempotency contract that callers historically relied on. + if (Volatile.Read(ref _disposed) != 0) + { + return; + } + + // Detect reentrancy BEFORE marking _disposed. A handler invoking + // Dispose() on its own watcher would otherwise wedge inside Unsubscribe + // (which blocks on the in-flight callback drain) and leave the watcher + // unrecoverable. Throwing here keeps _disposed at 0 so a subsequent + // Dispose call from another thread (e.g. the test cleanup path) can + // still complete normally. + if (disposing) + { + ThrowIfCurrentCallbackWouldDeadlock(); + } + // Use Interlocked.CompareExchange for atomic check-and-set. // Only one thread will successfully change _disposed from 0 to 1. if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) @@ -85,6 +158,11 @@ private void Dispose(bool disposing) if (disposing) { Unsubscribe(); + // Release the AutoResetEvent's kernel handle. Must happen AFTER + // Unsubscribe completes its drain — in-flight callbacks reference + // _newEvents.SafeWaitHandle via the ThreadPool wait, so disposing + // earlier could crash a callback mid-flight. + _newEvents.Dispose(); } if (_queryHandle is { IsInvalid: false }) @@ -95,116 +173,189 @@ private void Dispose(bool disposing) private void ProcessNewEvents(object? state, bool timedOut) { - bool success; - var buffer = new IntPtr[64]; + int threadId = Environment.CurrentManagedThreadId; - do + _callbackThreadIds[threadId] = 0; + + try { - if (_isSubscribed is false) { break; } + bool success; + var buffer = new IntPtr[64]; - int count = 0; + do + { + if (_isSubscribed is false) { break; } - success = NativeMethods.EvtNext(_subscriptionHandle, buffer.Length, buffer, 0, 0, ref count); + int count = 0; - if (!success) { return; } + success = NativeMethods.EvtNext(_subscriptionHandle, buffer.Length, buffer, 0, 0, ref count); - for (int i = 0; i < count; i++) - { - using var eventHandle = new EvtHandle(buffer[i]); - EventRecord @event; + if (!success) { return; } - try + for (int i = 0; i < count; i++) { - @event = NativeMethods.RenderEvent(eventHandle); - @event.Properties = NativeMethods.RenderEventProperties(eventHandle); + using var eventHandle = new EvtHandle(buffer[i]); + EventRecord @event; - if (_renderXml) + try { - @event.Xml = NativeMethods.RenderEventXml(eventHandle); + @event = NativeMethods.RenderEvent(eventHandle); + @event.Properties = NativeMethods.RenderEventProperties(eventHandle); + + if (_renderXml) + { + @event.Xml = NativeMethods.RenderEventXml(eventHandle); + } } - } - catch (Exception ex) - { - @event = new EventRecord { RecordId = null, Error = ex.Message }; - } + catch (Exception ex) + { + @event = new EventRecord { RecordId = null, Error = ex.Message }; + } + + @event.PathName = _path; - @event.PathName = _path; + // Subscriber failures are isolated by contract: one handler + // must not prevent subsequent handlers in the multicast + // chain or future events from being delivered. Without this + // per-subscriber try/catch, a single throwing subscriber + // would short-circuit the multicast invoke and let the + // exception bubble into the ThreadPool worker. + var subscribers = EventRecordWritten; - EventRecordWritten?.Invoke(this, @event); + if (subscribers is null) { continue; } + + foreach (Delegate handler in subscribers.GetInvocationList()) + { + try + { + ((EventHandler)handler)(this, @event); + } + catch (Exception) + { + // Intentional: per-subscriber isolation. No logger + // is wired into this layer; if one is added later, + // log the exception here. + } + } + } } - } while (success); + while (success); + } + finally + { + _callbackThreadIds.TryRemove(threadId, out _); + } + } + + private void ThrowIfCurrentCallbackWouldDeadlock() + { + if (_callbackThreadIds.ContainsKey(Environment.CurrentManagedThreadId)) + { + throw new InvalidOperationException( + "EventLogWatcher cannot be stopped from within an EventRecordWritten handler."); + } } private void Subscribe() { - if (_isSubscribed) { throw new InvalidOperationException("Already subscribed."); } + // Serialized with Unsubscribe so Enabled=true racing with Enabled=false + // (or Dispose) cannot leave _subscriptionHandle disposed while this + // method is still mutating it. Reentry from inside a callback is + // impossible because the Enabled setter calls + // ThrowIfCurrentCallbackWouldDeadlock before invoking Subscribe; the + // synchronous ProcessNewEvents call below cannot self-recurse because + // it runs before _waitHandle is registered. + lock (_lifecycleLock) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); - EvtSubscribeFlags flag = string.IsNullOrEmpty(_bookmark) ? - EvtSubscribeFlags.ToFutureEvents : - EvtSubscribeFlags.StartAfterBookmark; + if (_isSubscribed) { throw new InvalidOperationException("Already subscribed."); } - EvtHandle bookmarkHandle = string.IsNullOrEmpty(_bookmark) ? - EvtHandle.Zero : - NativeMethods.EvtCreateBookmark(_bookmark); + EvtSubscribeFlags flag = string.IsNullOrEmpty(_bookmark) ? + EvtSubscribeFlags.ToFutureEvents : + EvtSubscribeFlags.StartAfterBookmark; - if (!string.IsNullOrEmpty(_bookmark) && bookmarkHandle.IsInvalid) - { - int error = Marshal.GetLastWin32Error(); - bookmarkHandle.Dispose(); - NativeMethods.ThrowEventLogException(error); - } + EvtHandle bookmarkHandle = string.IsNullOrEmpty(_bookmark) ? + EvtHandle.Zero : + NativeMethods.EvtCreateBookmark(_bookmark); - _subscriptionHandle = NativeMethods.EvtSubscribe( - EventLogSession.GlobalSession.Handle, - _newEvents.SafeWaitHandle, - _path, - null, - bookmarkHandle, - IntPtr.Zero, - IntPtr.Zero, - flag); - - if (!string.IsNullOrEmpty(_bookmark)) - { - bookmarkHandle.Dispose(); - } + if (!string.IsNullOrEmpty(_bookmark) && bookmarkHandle.IsInvalid) + { + int error = Marshal.GetLastWin32Error(); + bookmarkHandle.Dispose(); + NativeMethods.ThrowEventLogException(error); + } - int subscriptionError = Marshal.GetLastWin32Error(); + _subscriptionHandle = NativeMethods.EvtSubscribe( + EventLogSession.GlobalSession.Handle, + _newEvents.SafeWaitHandle, + _path, + null, + bookmarkHandle, + IntPtr.Zero, + IntPtr.Zero, + flag); + + if (!string.IsNullOrEmpty(_bookmark)) + { + bookmarkHandle.Dispose(); + } - if (_subscriptionHandle.IsInvalid) - { - NativeMethods.ThrowEventLogException(subscriptionError); - } + int subscriptionError = Marshal.GetLastWin32Error(); + + if (_subscriptionHandle.IsInvalid) + { + NativeMethods.ThrowEventLogException(subscriptionError); + } - _isSubscribed = true; + _isSubscribed = true; - ProcessNewEvents(null, false); + ProcessNewEvents(null, false); - _waitHandle = ThreadPool.RegisterWaitForSingleObject(_newEvents, ProcessNewEvents, null, -1, false); + _waitHandle = ThreadPool.RegisterWaitForSingleObject(_newEvents, ProcessNewEvents, null, -1, false); + } } private void Unsubscribe() { - _isSubscribed = false; - - if (_waitHandle is not null) + // Serialized with Subscribe (see _lifecycleLock comment) so concurrent + // callers (Dispose racing with Enabled=false, or two Enabled=false calls) + // cannot both attempt Unregister on the same _waitHandle, and so a + // racing Subscribe cannot interleave with the teardown. The lock also + // guarantees the losing caller observes the winner's drain — required + // for the "no callback fires after Unsubscribe returns" contract that + // callers (e.g. DatabaseService delete) rely on. Reentry from inside a + // callback is impossible because both call sites first invoke + // ThrowIfCurrentCallbackWouldDeadlock. + lock (_lifecycleLock) { - // Pass a wait object (not null) so Unregister blocks until all in-flight - // ProcessNewEvents callbacks have completed. Per the .NET docs on - // RegisteredWaitHandle.Unregister, passing null does NOT wait — and our - // callback creates per-event resolver scopes that hold pooled SQLite - // connections, so callers (e.g., DatabaseService delete) need a hard - // guarantee that no callback can fire after Unregister returns. - using var unregisterSignal = new ManualResetEvent(false); - - _waitHandle.Unregister(unregisterSignal); - unregisterSignal.WaitOne(); - _waitHandle = null; - } + _isSubscribed = false; - if (!_subscriptionHandle.IsInvalid) - { - _subscriptionHandle.Dispose(); + if (_waitHandle is not null) + { + // Pass a wait object (not null) so Unregister blocks until all in-flight + // ProcessNewEvents callbacks have completed. Per the .NET docs on + // RegisteredWaitHandle.Unregister, passing null does NOT wait — and our + // callback creates per-event resolver scopes that hold pooled SQLite + // connections, so callers (e.g., DatabaseService delete) need a hard + // guarantee that no callback can fire after Unregister returns. + using var unregisterSignal = new ManualResetEvent(false); + + _waitHandle.Unregister(unregisterSignal); + unregisterSignal.WaitOne(); + _waitHandle = null; + } + + // Use IsClosed (not IsInvalid) — SafeHandle.IsInvalid is based on + // the underlying handle value and does NOT flip after Dispose, so + // checking IsInvalid would let a serialized loser call Dispose a + // second time. SafeHandle.Dispose is idempotent today, but the + // explicit IsClosed check makes the intent clear and removes the + // dependency on that idempotency. + if (!_subscriptionHandle.IsClosed) + { + _subscriptionHandle.Dispose(); + } } } } From 5e8481e6d0eb2910fc757d780fd26c01e2a7e78e Mon Sep 17 00:00:00 2001 From: jschick04 Date: Fri, 8 May 2026 08:53:51 -0500 Subject: [PATCH 20/24] Strip overlong comments and tighten EventLogWatcher XML doc/impl alignment --- .../EventMessageProviderIntegrationTests.cs | 27 +--- .../Providers/RegistryProviderTests.cs | 33 +--- .../Readers/EventLogInformationTests.cs | 9 +- .../Readers/EventLogReaderTests.cs | 14 +- .../Readers/EventLogWatcherTests.cs | 141 +++--------------- .../Readers/SmallEvtxFixture.cs | 19 +-- .../EventResolvers/EventResolverBaseTests.cs | 21 +-- .../VersatileEventResolverTests.cs | 19 +-- .../Readers/EventLogInformation.cs | 5 +- .../Readers/EventLogWatcher.cs | 115 ++------------ 10 files changed, 63 insertions(+), 340 deletions(-) diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs index 0e12584c..5ec53796 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs @@ -17,11 +17,7 @@ public sealed class EventMessageProviderIntegrationTests [Fact] public void GetMessages_WhenBinaryUsesMuiSatellite_ShouldLoadMessagesFromMuiFile() { - // wevtsvc.dll on Windows keeps its message table in \wevtsvc.dll.mui rather - // than in the binary itself. The previous load path used LOAD_LIBRARY_AS_DATAFILE - // with only the leaf filename and therefore failed with ERROR_RESOURCE_TYPE_NOT_FOUND - // (1813) for binaries like this — including the WMIRegistrationService.exe scenario - // that motivated this fix. The MUI-aware load path should resolve them. + // wevtsvc.dll keeps its message table in the .mui satellite — the loader must follow MUI fallback. var systemDirectory = Environment.SystemDirectory; var muiBinary = Path.Combine(systemDirectory, "wevtsvc.dll"); @@ -41,12 +37,7 @@ public void GetMessages_WhenBinaryUsesMuiSatellite_ShouldLoadMessagesFromMuiFile [Fact] public void GetMessages_WhenFilePathContainsEnvironmentVariable_ShouldHandleCorrectly() { - // LoadLibraryEx does not expand environment variables. Channel-named providers like - // Microsoft-Windows-AppXDeploymentServer/Operational supply paths such as - // "%SystemRoot%\system32\AppXDeploymentServer.dll" through the publisher manifest; - // EventMessageProvider must expand them defensively before calling LoadLibraryEx, - // otherwise the load fails with ERROR_FILE_NOT_FOUND. We exercise the same path - // here with wevtsvc.dll, which is present on every Windows host. + // LoadLibraryEx does not expand env vars; provider must expand %SystemRoot% before loading. var systemDirectory = Environment.SystemDirectory; var muiBinary = Path.Combine(systemDirectory, "wevtsvc.dll"); @@ -129,9 +120,7 @@ public void LoadProviderDetails_WhenCalledMultipleTimes_ShouldReturnConsistentRe [Fact] public void LoadProviderDetails_WhenChannelOwningPublisherUnknown_ShouldReturnEmptyDetailsWithoutFallback() { - // A made-up provider name that is neither a registered publisher nor a registered channel. - // The owning-publisher probe must return false and the resolver must end up with an empty - // ProviderDetails instead of recursing or throwing. + // Arrange — name is neither a registered publisher nor a channel. const string MadeUpName = "NonExistent-Publisher-And-Channel/Bogus"; EventMessageProvider provider = new(MadeUpName); @@ -162,9 +151,7 @@ public void LoadProviderDetails_WhenProviderHasNoData_ShouldReturnEmptyCollectio [Fact] public void LoadProviderDetails_WhenProviderNameIsActuallyAChannelPath_ShouldFallBackToOwningPublisher() { - // Some events arrive with a channel path in the ProviderName slot rather than the real - // publisher name (the AppXDeploymentServer/Operational case). We expect the fallback to - // resolve via EvtChannelConfigOwningPublisher and load the real publisher's metadata. + // Channel paths in the ProviderName slot must resolve via EvtChannelConfigOwningPublisher. Assert.SkipUnless( TryFindChannelWithDistinctOwningPublisher(out var channelName, out var owningPublisher), "Test requires a registered channel whose owning publisher differs from the channel path itself."); @@ -239,11 +226,7 @@ private static bool TryFindMuiSatellite(string systemDirectory, string binaryNam { var muiFileName = binaryName + ".mui"; - // Only probe locales the Win32 MUI loader will actually consult: the current UI culture, - // its parents, and "en-US" as the well-known system fallback that ships with every Windows - // SKU. A satellite present in some other locale subfolder would pass a broad existence - // check but the loader would never select it, so the test could still fail after skipping. - // The loop terminates naturally when culture.Name becomes empty (the invariant culture). + // Probe only locales the Win32 MUI loader actually consults: current UI culture chain + en-US fallback. var probeOrder = new List(); for (var culture = CultureInfo.CurrentUICulture; !string.IsNullOrEmpty(culture.Name); culture = culture.Parent) diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs index 2730493c..565a7e05 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs @@ -49,11 +49,7 @@ public void GetMessageFilesForLegacyProvider_WhenCalled_ShouldNotIncludeSysFiles // Arrange var provider = new RegistryProvider(); - // Act — drive the SUT through whichever well-known legacy provider is - // present on this host. The original test probed Application+System log - // names (which the SUT treats as provider names that produce no EMF on - // a standard Windows install); switch to the helper so the .sys filter - // is actually exercised against real provider data. + // Act var result = FindAnyLegacyProviderFiles(provider); // Assert @@ -119,9 +115,7 @@ public void GetMessageFilesForLegacyProvider_WhenCommonWindowsProvider_ShouldRet // Arrange var provider = new RegistryProvider(); - // Act — probe well-known legacy provider names (the original test passed - // a log name where the SUT expects a provider name, which produces no - // results on a standard Windows install). + // Act var result = FindAnyLegacyProviderFiles(provider); // Assert @@ -229,13 +223,10 @@ public void GetMessageFilesForLegacyProvider_WhenMultipleSemicolonSeparatedPaths // Arrange var provider = new RegistryProvider(); - // Act - Scan the registry for a legacy provider whose EventMessageFile - // value literally contains semicolon-separated paths and yields multiple - // .dll/.exe entries through the SUT's split + extension filter. + // Act var (providerName, files) = FindLegacyProviderWithSemicolonSplitFiles(provider); - // Assert - genuinely environment-dependent; not every host has a legacy - // provider registered with semicolon-separated EventMessageFile entries. + // Assert — environment-dependent; not every host registers such a provider. Assert.SkipUnless(providerName is not null, "Test requires a legacy provider with semicolon-separated EventMessageFile entries on this host."); @@ -293,10 +284,7 @@ public void GetMessageFilesForLegacyProvider_WhenProviderExists_ShouldReturnEnum [Fact] public void GetMessageFilesForLegacyProvider_WhenProviderFound_ShouldLogFoundMessage() { - // Arrange — discover a known-good provider name on this host first so - // the mock-logger assertion can be bound to that exact name. Without - // this binding, a stray "Found" debug emitted while probing an - // unrelated provider could vacuously satisfy the contract. + // Arrange — discover a known-good provider name first so the mock-logger assertion binds to it. var probe = new RegistryProvider(); var providerName = FindAnyLegacyProviderName(probe); Assert.NotNull(providerName); @@ -304,8 +292,7 @@ public void GetMessageFilesForLegacyProvider_WhenProviderFound_ShouldLogFoundMes var mockLogger = Substitute.For(); var provider = new RegistryProvider(mockLogger); - // Act — drive the SUT through the provider name we just confirmed - // yields a non-empty result on this host. + // Act var foundFiles = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); // Assert — bind the "Found message file" debug emission to the specific @@ -486,9 +473,7 @@ private static (string? Name, List Files) FindLegacyProviderWithSemicolo return (null, []); } - // Track provider names whose first non-empty EMF occurrence has already - // been evaluated. This mirrors the SUT, which returns from the first - // log subtree where a given provider name has a non-empty EMF. + // Mirrors SUT: returns from the first log subtree where a provider has a non-empty EMF. var seenWithEmf = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var logName in eventLogKey.GetSubKeyNames()) @@ -514,9 +499,7 @@ private static (string? Name, List Files) FindLegacyProviderWithSemicolo continue; } - // First non-empty EMF wins. If this occurrence lacks ';', the - // SUT will not return semicolon-split content for this name on - // this host regardless of any later log subtree. + // First non-empty EMF wins per provider name across log subtrees. if (!seenWithEmf.Add(providerName)) { continue; diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs index 2e9e4678..280927d8 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs @@ -113,9 +113,6 @@ public void Constructor_WhenInvalidLogName_ShouldThrowFileNotFoundException() var invalidLogName = "NonExistentLog_" + Guid.NewGuid(); // Act & Assert - // Surfaces the real EvtOpenLog error (ERROR_EVT_CHANNEL_NOT_FOUND) instead - // of the previous masked UnauthorizedAccessException from a follow-up - // GetLogInfo on an invalid handle. Assert.Throws(() => new EventLogInformation(session, invalidLogName, PathType.LogName)); } @@ -156,11 +153,7 @@ public void Constructor_WhenSpecialCharactersInLogName_ShouldNotMaskAsUnauthoriz var ex = Record.Exception(() => new EventLogInformation(session, invalidLogName, PathType.LogName)); - // Assert - // Avoid overfitting the specific Win32 mapping (e.g. - // ERROR_EVT_CHANNEL_PATH_INVALID) since it goes through the default switch - // case and may shift across Windows versions. Capture the smell-fix - // invariant: malformed paths must not be masked as UAE. + // Assert — malformed paths must not be masked as UAE. Assert.NotNull(ex); Assert.IsNotType(ex); } diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs index bd14ad08..4f00b1d6 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs @@ -165,12 +165,7 @@ public void Dispose_WhenCalledMultipleTimes_ShouldNotThrow() [Fact] public void EndOfResults_AfterExhaustion_ShouldKeepBookmarkAndNotSetLastErrorCode() { - // Use a small bounded .evtx fixture (max 5 events exported via wevtutil) so the - // exhaustion loop completes quickly regardless of the host machine's Application log - // size. Reading the live Application log here previously made the test runtime - // unbounded and CI-flaky. - - // Arrange + // Arrange — bounded fixture keeps the exhaustion loop fast on any host. using var fixture = new SmallEvtxFixture(); using var reader = new EventLogReader(fixture.FilePath, PathType.FilePath); @@ -179,11 +174,10 @@ public void EndOfResults_AfterExhaustion_ShouldKeepBookmarkAndNotSetLastErrorCod string? bookmarkAfterExhaustion = reader.LastBookmark; - // One additional read past end-of-results reader.TryGetEvents(out _); string? bookmarkAfterExtraRead = reader.LastBookmark; - // Assert — bookmark is stable past EOF, and normal end-of-results does NOT set LastErrorCode + // Assert — bookmark stable past EOF, normal end-of-results does not set LastErrorCode. Assert.Equal(bookmarkAfterExhaustion, bookmarkAfterExtraRead); Assert.Null(reader.LastErrorCode); } @@ -224,9 +218,7 @@ public void IsValid_WhenInvalidLogName_ShouldBeFalse() [Fact] public void LastBookmark_AfterTryGetEvents_ShouldBeSet() { - // Use SmallEvtxFixture so the read is deterministic — the live Application - // log may be empty on minimal hosts, which used to silently skip the - // assertions behind an `if (success && events.Length > 0)` guard. + // Arrange — fixture keeps the read deterministic on minimal hosts. using var fixture = new SmallEvtxFixture(); using var reader = new EventLogReader(fixture.FilePath, PathType.FilePath); diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs index c051f8fa..bc9a73d3 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs @@ -108,7 +108,6 @@ public void Constructor_WithNullLogName_ShouldThrowArgumentException() public void Constructor_WithValidBookmark_ShouldCreateWatcher() { // Arrange - // Get a valid bookmark from EventLogReader using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); reader.TryGetEvents(out _, 1); @@ -145,12 +144,7 @@ public void Dispose_AfterDispose_ShouldNotReceiveEvents() watcher.Enabled = true; - // Act — dispose, then write a stimulus event to prove the handler is - // no longer wired up. EventLogWatcher.Dispose drains in-flight - // callbacks before returning. Snapshot the count and reset the signal - // *after* the drain so any ambient callbacks delivered during the - // live subscription window (between Enabled=true and Dispose) cannot - // bleed into the post-stimulus assertion. + // Act watcher.Dispose(); int countBefore = Volatile.Read(ref eventCount); eventReceived.Reset(); @@ -185,16 +179,10 @@ public void Dispose_ShouldReleaseUnderlyingWaitHandle() var watcher = new EventLogWatcher(Constants.ApplicationLogName); watcher.Enabled = true; - // Reach into the private AutoResetEvent the watcher owns so we can - // assert the kernel handle is released — the leak this test guards - // against (per Copilot review on PR #516) was discovered precisely - // because there was no externally-observable signal of disposal. var newEventsField = typeof(EventLogWatcher).GetField( "_newEvents", BindingFlags.Instance | BindingFlags.NonPublic); - Assert.True( - newEventsField is not null, - "Private field _newEvents not found on EventLogWatcher — was it renamed? Update Dispose_ShouldReleaseUnderlyingWaitHandle."); + Assert.True(newEventsField is not null, "_newEvents field missing — was it renamed?"); var newEvents = (AutoResetEvent?)newEventsField.GetValue(watcher); Assert.NotNull(newEvents); @@ -203,13 +191,8 @@ public void Dispose_ShouldReleaseUnderlyingWaitHandle() watcher.Dispose(); // Assert - Assert.True( - newEvents.SafeWaitHandle.IsClosed, - "AutoResetEvent.SafeWaitHandle.IsClosed should be true after Dispose; otherwise the OS event handle leaks across watcher lifetimes."); + Assert.True(newEvents.SafeWaitHandle.IsClosed, "AutoResetEvent kernel handle leaked after Dispose."); - // Idempotency: the field-level Dispose unconditionally calls - // _newEvents.Dispose(), so a second top-level Dispose() must be - // gated by the _disposed fast-path to avoid a second call. watcher.Dispose(); Assert.True(newEvents.SafeWaitHandle.IsClosed); } @@ -238,9 +221,6 @@ public void Dispose_WhenCalledFromHandler_ShouldThrowInvalidOperationException() watcher.EventRecordWritten += (sender, record) => { - // Capture the exception inside the handler — the SUT isolates - // subscriber exceptions per its public contract, so an uncaught - // throw here would be silently swallowed and the test would hang. try { ((EventLogWatcher)sender!).Dispose(); @@ -270,10 +250,6 @@ public void Dispose_WhenCalledFromHandler_ShouldThrowInvalidOperationException() Assert.NotNull(captured); var ex = Assert.IsType(captured); Assert.Contains("cannot be stopped from within", ex.Message); - - // Cleanup happens via `using var watcher` from the test thread, which - // is not the ThreadPool callback thread, so the reentrancy guard does - // not trigger and Dispose completes normally. } [Fact] @@ -347,16 +323,7 @@ public async Task Enabled_WhenRacingFromMultipleThreads_ShouldNotHangOrCorruptSt // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - // Act: race many Subscribe/Unsubscribe pairs to maximize the window - // for Subscribe to mutate _subscriptionHandle/_waitHandle while - // another thread is in Unsubscribe. Without the lifecycle lock, two - // Enabled=false callers can both pass the _waitHandle null check and - // both call RegisteredWaitHandle.Unregister; the second call returns - // false (already unregistered) and does NOT signal its wait object, - // so its WaitOne blocks indefinitely. Separately, an Enabled=true - // racing with Enabled=false can leave _subscriptionHandle disposed - // mid-Subscribe, making the next ProcessNewEvents invocation crash - // on a closed native handle. + // Act var tasks = new List(capacity: 100); var ct = TestContext.Current.CancellationToken; @@ -365,18 +332,14 @@ public async Task Enabled_WhenRacingFromMultipleThreads_ShouldNotHangOrCorruptSt tasks.Add(Task.Run(() => { try { watcher.Enabled = true; } - catch (InvalidOperationException) { /* loser of a Subscribe race — expected */ } + catch (InvalidOperationException) { } }, ct)); tasks.Add(Task.Run(() => watcher.Enabled = false, ct)); } var allDone = Task.WhenAll(tasks); - // Assert: 30s budget far exceeds any legitimate Subscribe/Unsubscribe - // drain across 100 toggles. A TimeoutException here means the - // RegisteredWaitHandle race regression has returned. Any other - // exception (e.g., ObjectDisposedException from a torn-down handle) - // surfaces via the await. + // Assert: timeout regression-tests the RegisteredWaitHandle race. await allDone.WaitAsync(TimeSpan.FromSeconds(30), ct); } @@ -404,9 +367,6 @@ public void Enabled_WhenSetToFalseFromHandler_ShouldThrowInvalidOperationExcepti watcher.EventRecordWritten += (sender, record) => { - // Same reentrancy contract as Dispose: Enabled = false also calls - // Unsubscribe, which blocks waiting for in-flight callbacks. Calling - // it from a callback would self-deadlock, so the SUT throws. try { ((EventLogWatcher)sender!).Enabled = false; @@ -483,11 +443,7 @@ public void Enabled_WhenSetToTrueAfterDispose_ShouldThrowObjectDisposedException var watcher = new EventLogWatcher(Constants.ApplicationLogName); watcher.Dispose(); - // Act & Assert: post-dispose Subscribe is gated by an explicit - // _disposed check inside the lifecycle lock so callers see a clean - // ObjectDisposedException instead of a confusing native-handle - // failure (e.g., from _newEvents.SafeWaitHandle access after - // _newEvents has itself been disposed). + // Act & Assert Assert.Throws(() => watcher.Enabled = true); } @@ -552,10 +508,7 @@ public void EventRecordWritten_AfterResubscribe_ShouldReceiveEvents() eventReceived.Set(); }; - // Act — toggle the watcher off then on. Safe to reset the counter - // and signal between Disable and the second Enable: Unsubscribe - // blocks until in-flight callbacks complete, so no handler can fire - // between the count clear and the Reset below. + // Act watcher.Enabled = true; watcher.Enabled = false; Interlocked.Exchange(ref eventCount, 0); @@ -591,19 +544,12 @@ public void EventRecordWritten_AfterUnsubscribe_ShouldStopReceivingEvents() watcher.Enabled = true; - // Act — disable the watcher, then snapshot the post-action count and - // clear the signal accumulated during the populate phase. Safe: - // EventLogWatcher.Unsubscribe blocks until in-flight callbacks - // complete, so no handler can fire between the count capture and the - // Reset below. + // Act watcher.Enabled = false; int countBefore = Volatile.Read(ref eventCount); eventReceived.Reset(); - // The stimulus that proves Disable worked: write an event AFTER - // Disable and verify the handler does not fire. Without the stimulus - // the test would vacuously pass because no event would arrive in the - // wait window regardless of whether Disable did anything. + // Stimulus event written AFTER Disable so the test isn't vacuous. using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event after unsubscribe", EventLogEntryType.Information); @@ -636,12 +582,7 @@ public void EventRecordWritten_ForNormalEvent_ShouldHaveNullError() watcher.Enabled = true; - // Act — Write a normal event. EventRecord.Error is populated by - // ProcessNewEvents only when EvtRender throws, which is not reachable - // through the public API for a well-formed log entry. This test pins - // the happy-path invariant (Error is null on a normal event); see - // WithInvalidBookmark_ShouldThrowAndNotMaskAsUnauthorizedAccessException - // for an exercise of the failure path. + // Act using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event for normal-path null Error invariant", EventLogEntryType.Information); @@ -652,9 +593,6 @@ public void EventRecordWritten_ForNormalEvent_ShouldHaveNullError() var snapshot = receivedEvents.ToArray(); Assert.True(received, "Did not receive event within timeout period"); Assert.NotEmpty(snapshot); - // Assert only the first signaled record (the stimulus). Late ambient - // callbacks queued before ToArray could otherwise broaden the check - // beyond what the test stimulus controls. Assert.Null(snapshot[0].Error); } @@ -668,12 +606,6 @@ public void EventRecordWritten_ShouldHaveRecordId() watcher.EventRecordWritten += (sender, record) => { - // Volatile.Write provides cross-thread visibility for the - // reference assignment; the test thread reads via Volatile.Read - // after the signal Wait. Last-event-wins semantics: a later - // ambient callback may overwrite this value before the test - // reads it, which is acceptable here because the assertion is - // an "any watcher event has this invariant" check. Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -834,9 +766,7 @@ public async Task EventRecordWritten_ShouldReceiveEventsInOrder() { receivedEvents.Enqueue(record); - // Guard CountdownEvent.Signal: ambient callbacks past the - // expected count would throw InvalidOperationException on the - // callback thread. + // Bound Signal — ambient callbacks past expectedCount would throw. int count = Interlocked.Increment(ref signalCount); if (count <= expectedCount) @@ -860,14 +790,10 @@ public async Task EventRecordWritten_ShouldReceiveEventsInOrder() bool received = countdown.Wait(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); // Assert - // Snapshot the queue: ConcurrentQueue.ToArray returns a moment-in- - // time copy in enqueue order. Late ambient callbacks may continue - // to enqueue after this point, but they will not affect the snapshot. var snapshot = receivedEvents.ToArray(); Assert.True(received, "Did not receive all events within timeout period"); Assert.True(snapshot.Length >= expectedCount, $"Expected at least {expectedCount} events in snapshot, but got {snapshot.Length}."); - // Verify events are in chronological order (by TimeCreated) for (int i = 1; i < Math.Min(expectedCount, snapshot.Length); i++) { Assert.True(snapshot[i].TimeCreated >= snapshot[i - 1].TimeCreated, @@ -886,12 +812,7 @@ public void EventRecordWritten_WhenHandlerThrows_ShouldStillDeliverFutureEvents( watcher.EventRecordWritten += (sender, record) => { - // Signal "first invocation observed" BEFORE throwing so the - // stimulus thread can release the second write only after the - // throw has happened. Per-subscriber isolation in the SUT - // (introduced alongside this test) is what lets later invocations - // fire at all — without it the throw would short-circuit the - // multicast invoke and bubble into the ThreadPool worker. + // Signal "first observed" before the throw so the stimulus thread can release the second write. int n = Interlocked.Increment(ref attempts); if (n == 1) @@ -908,8 +829,7 @@ public void EventRecordWritten_WhenHandlerThrows_ShouldStillDeliverFutureEvents( using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; - // Act — write the stimulus, wait for the first (throwing) invocation, - // then write a second event to prove the watcher still delivers. + // Act eventLog.WriteEntry("Test event 1 (handler throws on first invocation)", EventLogEntryType.Information); bool firstReceived = firstObserved.Wait(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); Assert.True(firstReceived, "First handler invocation was not observed"); @@ -936,9 +856,7 @@ public void EventRecordWritten_WhenMultipleEventsWritten_ShouldReceiveAll() watcher.EventRecordWritten += (sender, record) => { - // Guard CountdownEvent.Signal: the shared Application log can - // produce ambient callbacks that, if signaled past zero, throw - // InvalidOperationException on the callback thread. + // Bound Signal — ambient callbacks past expectedCount would throw. int count = Interlocked.Increment(ref eventCount); if (count <= expectedCount) @@ -968,8 +886,7 @@ public void EventRecordWritten_WhenMultipleEventsWritten_ShouldReceiveAll() [Fact] public void EventRecordWritten_WhenNotSubscribed_ShouldNotReceiveEvents() { - // Arrange — handler is attached via += but watcher.Enabled is never - // set to true, so no native subscription should be active. + // Arrange — handler attached but Enabled is never set to true. using var watcher = new EventLogWatcher(Constants.ApplicationLogName); int eventCount = 0; var eventReceived = new ManualResetEventSlim(false); @@ -980,10 +897,7 @@ public void EventRecordWritten_WhenNotSubscribed_ShouldNotReceiveEvents() eventReceived.Set(); }; - // Act — write an event to prove the negative case. Without this - // stimulus the test would vacuously pass because no event would - // arrive in the wait window regardless of whether the watcher was - // actually subscribed. + // Act — stimulus event so the negative case isn't vacuous. using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event with watcher not enabled", EventLogEntryType.Information); @@ -1005,15 +919,14 @@ public void EventRecordWritten_WhenOneOfMultipleHandlersThrows_ShouldStillNotify int succeedingCount = 0; var succeedingObserved = new ManualResetEventSlim(false); - // First subscriber always throws — simulates a poorly written consumer. - // Per-subscriber isolation must prevent it from blocking the second. + // First subscriber always throws. watcher.EventRecordWritten += (sender, record) => { Interlocked.Increment(ref failingCount); throw new InvalidOperationException("subscriber A always fails"); }; - // Second subscriber must still be invoked despite the first throwing. + // Second subscriber must still fire despite the first throwing. watcher.EventRecordWritten += (sender, record) => { Interlocked.Increment(ref succeedingCount); @@ -1049,9 +962,6 @@ public void EventRecordWritten_WhenSubscribed_ShouldReceiveEvents() watcher.EventRecordWritten += (sender, record) => { - // Capture the latest record for the per-record non-null - // invariant. Volatile.Write pairs with Volatile.Read in the - // assertion to guarantee cross-thread visibility. Volatile.Write(ref capturedEvent, record); Interlocked.Increment(ref eventCount); eventReceived.Set(); @@ -1060,7 +970,6 @@ public void EventRecordWritten_WhenSubscribed_ShouldReceiveEvents() // Act watcher.Enabled = true; - // Write a test event to Application log using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event for EventLogWatcher", EventLogEntryType.Information); @@ -1079,7 +988,6 @@ public void EventRecordWritten_WhenSubscribed_ShouldReceiveEvents() public void EventRecordWritten_WithBookmark_ShouldReceiveNewEvents() { // Arrange - // Get a bookmark from current position using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); reader.TryGetEvents(out _, 1); var bookmark = reader.LastBookmark; @@ -1097,7 +1005,6 @@ public void EventRecordWritten_WithBookmark_ShouldReceiveNewEvents() // Act watcher.Enabled = true; - // Write a new event using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; eventLog.WriteEntry("Test event after bookmark", EventLogEntryType.Information); @@ -1121,9 +1028,7 @@ public async Task EventRecordWritten_WithConcurrentEventWrites_ShouldHandleAllEv watcher.EventRecordWritten += (sender, record) => { - // Guard CountdownEvent.Signal: ambient callbacks past the - // expected count would throw InvalidOperationException on the - // callback thread. + // Bound Signal — ambient callbacks past expectedCount would throw. int count = Interlocked.Increment(ref eventCount); if (count <= expectedCount) @@ -1188,11 +1093,7 @@ public void EventRecordWritten_WithInvalidBookmark_ShouldThrowAndNotMaskAsUnauth // Act var ex = Record.Exception(() => watcher.Enabled = true); - // Assert - // The bookmark XML failure flows through ThrowEventLogException and the - // exact Win32 mapping may shift across Windows versions. Capture the - // stable invariants instead of pinning the type: bad bookmarks must - // surface an exception and must not be masked as UAE. + // Assert — bad bookmark must surface an exception that is not masked as UAE. Assert.NotNull(ex); Assert.IsNotType(ex); } diff --git a/src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs index b830a47f..340ad394 100644 --- a/src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs @@ -23,13 +23,10 @@ public SmallEvtxFixture() { FilePath = Path.Combine(Path.GetTempPath(), $"elx-tests-{Guid.NewGuid():N}.evtx"); - // Use the absolute path to wevtutil.exe rather than relying on PATH so the fixture is - // robust on machines where %PATH% has been customized. + // Use the absolute path to wevtutil.exe so customized %PATH% does not break the fixture. var wevtutilPath = Path.Combine(Environment.SystemDirectory, "wevtutil.exe"); - // Probe the live Application log for its most recent EventRecordID. Hard-coding - // the legacy [1, 5] window only works on hosts where the log has not rolled past - // record 5 -- not the case on long-running dev boxes or persistent CI agents. + // Probe the live Application log — hard-coding [1, 5] fails once the log rolls past 5. long latestId = ProbeLatestRecordId(wevtutilPath); long oldestId = Math.Max(1, latestId - (RequestedEvents - 1)); @@ -53,9 +50,7 @@ public void Dispose() private static long ProbeLatestRecordId(string wevtutilPath) { - // qe = query events; /c:1 limits to one event; /rd:true reads in reverse-chronological - // order (newest first); /f:RenderedXml ensures EventRecordID is rendered as an XML - // element we can parse without depending on locale-formatted text output. + // qe = query events, /c:1 limits to one, /rd:true reads newest-first, /f:RenderedXml gives parseable output. var (stdout, _) = RunWevtutil( wevtutilPath, ["qe", "Application", "/c:1", "/rd:true", "/f:RenderedXml"], @@ -133,13 +128,7 @@ private void ExportRecordWindow(string wevtutilPath, long oldestId, long latestI private void VerifyMinimumEvents() { - // Open the freshly-exported file and count records so a host where the Application - // log was just cleared (or where the probed window happened to span gaps) produces - // a clear fixture-level error instead of cryptic failures (e.g. Assert.True(success) - // on ERROR_NO_MORE_ITEMS) inside test bodies. We use the project's own - // EventLogReader rather than the BCL event-log reader because the entire - // EventLogExpert.Eventing project owns the EVT P/Invoke layer and bypassing it - // (even in a fixture) would defeat the point. + // Count records so an empty/gappy export produces a clear fixture-level error instead of cryptic test failures. using var reader = new EventLogReader(FilePath, PathType.FilePath); int total = 0; diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs index 4a8e52ec..0703180c 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs @@ -2616,21 +2616,8 @@ public void ResolveEvent_WithPopulatedProviderButUnknownEventAndMultipleProperti [Fact] public void ResolveEvent_WithPrimaryAndSupplementalDetailsButUnmappedEventId_ShouldReturnNoMatchingMessage() { - // Arrange — both primary and supplemental have populated metadata with mapped - // legacy messages, but neither has a message matching the requested EventId. - // This exercises the fallback path where supplemental IS loaded (because - // primary's GetMessagesByShortId count == 0 for this Id) and supplemental - // also fails to match — descriptionDetails stays on primary, supplemental - // is forwarded into CreateEventModel as a present-but-unhelpful argument. - // Distinct from WithPopulatedProviderButUnknownEvent... which uses - // TestEventResolver (supplemental is always null). - // - // To prove supplemental was actually loaded AND forwarded into CreateEventModel - // (rather than the test passing trivially via the primary-only no-match path), - // we seed a task ONLY in supplemental and set eventRecord.Task to match. The - // ResolveTaskName fallback only consults supplemental when primary has no entry; - // observing the supplemental task name in displayEvent.TaskCategory therefore - // requires both supplemental lookup and supplemental forwarding into the model. + // Arrange — supplemental loads (primary has no match for Id) and is forwarded into + // CreateEventModel; a task seeded only in supplemental proves the forwarding. const string SupplementalOnlyTaskName = "SupplementalOnlyTask"; const int SharedTaskId = 42; @@ -2680,12 +2667,8 @@ public void ResolveEvent_WithPrimaryAndSupplementalDetailsButUnmappedEventId_Sho Assert.Contains("No matching message", displayEvent.Description); Assert.DoesNotContain("No matching provider", displayEvent.Description); Assert.DoesNotContain("Failed to resolve", displayEvent.Description); - // Neither primary nor supplemental message text should leak into the description. Assert.DoesNotContain("Primary event 100", displayEvent.Description); Assert.DoesNotContain("Supplemental event 200", displayEvent.Description); - // Proves supplemental was loaded AND forwarded into CreateEventModel: the - // supplemental-only task name only appears via the supplemental fallback in - // ResolveTaskName. Assert.Equal(SupplementalOnlyTaskName, displayEvent.TaskCategory); } diff --git a/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs index 67e9b920..78062f4e 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/VersatileEventResolverTests.cs @@ -347,10 +347,6 @@ public void ResolveEvent_WithLocalResolver_ShouldResolveEvent() [Fact] public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseTextToDbProvider() { - // Documents that a single resolver instance cleanly handles both DB-resolved - // and locally-resolved providers. The DB-loaded provider's seeded message text - // must not leak into the description for an unrelated unknown provider — - // that would indicate a per-provider cache-isolation bug in EventResolver. const string DBProviderName = "MixedScenarioDbProvider"; const string DBMessageText = "Resolved from test database for mixed scenario"; const ushort EventId = 1000; @@ -403,9 +399,7 @@ public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseT LogName = Constants.ApplicationLogName }; - // Act — same resolver instance resolves both. The DB-known provider - // hits TryResolveFromDatabase; the unknown provider falls through to - // ResolveFromLocalProvider. + // Act dbDisplayEvent = resolver.ResolveEvent(dbEventRecord); unknownDisplayEvent = resolver.ResolveEvent(unknownEventRecord); } @@ -418,9 +412,6 @@ public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseT Assert.NotNull(unknownDisplayEvent); Assert.Equal(unknownProviderName, unknownDisplayEvent.Source); Assert.DoesNotContain(DBMessageText, unknownDisplayEvent.Description); - // Unknown provider must hit a fallback "No matching ..." path (provider-level - // or message-level) rather than the "Failed to resolve" exception path or - // accidentally inheriting the DB-known provider's description. Assert.Contains("No matching", unknownDisplayEvent.Description); Assert.DoesNotContain("Failed to resolve", unknownDisplayEvent.Description); } @@ -464,13 +455,7 @@ public void ResolveEvent_WithNonExistentProvider_ShouldReturnDefaultDescription( [InlineData(Constants.TestProviderName)] public void ResolveEvent_WithProvider_ShouldReturnDisplayEventWithMatchingFields(string providerName) { - // Verifies model-construction round-trip: per-record fields (Id, ComputerName) - // and the requested ProviderName flow through CreateEventModel into the - // DisplayEventModel for representative provider names. This is intentionally - // a model-propagation test, not a provider-resolution-behavior test — none of - // the provider names are seeded with metadata, so each row exercises the same - // local-fallback path. The Theory shape over a foreach loop gives per-case - // diagnostics if one provider name regresses while others continue to pass. + // Theory verifies CreateEventModel propagates per-record fields and ProviderName. // Arrange using var resolver = new EventResolver(); diff --git a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs index aa102c19..07cd7d07 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs @@ -16,10 +16,7 @@ internal EventLogInformation(EventLogSession session, string logName, PathType p int error = Marshal.GetLastWin32Error(); - // Surface the real EvtOpenLog failure (e.g., FileNotFoundException for a - // missing channel). Without this check, the constructor would call - // GetLogInfo on a NULL handle and the secondary ERROR_INVALID_HANDLE would - // mask the original error as UnauthorizedAccessException. + // Surface the real EvtOpenLog failure — without this, GetLogInfo on a NULL handle masks it as UAE. if (handle.IsInvalid) { NativeMethods.ThrowEventLogException(error); diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index c539296a..6acc8ee5 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -11,30 +11,12 @@ namespace EventLogExpert.Eventing.Readers; public sealed class EventLogWatcher : IDisposable { private readonly string? _bookmark; - // Tracks ManagedThreadIds currently inside ProcessNewEvents. Used by - // Dispose() and the Enabled=false path to detect a handler trying to stop - // the watcher from inside its own callback, which would self-deadlock on - // Unsubscribe's drain wait. Non-disposable so it stays safe across - // idempotent Dispose calls and finalizer paths. private readonly ConcurrentDictionary _callbackThreadIds = new(); + private readonly Lock _lifecycleLock = new(); private readonly AutoResetEvent _newEvents = new(false); private readonly string _path; private readonly EvtHandle _queryHandle; private readonly bool _renderXml; - // Serializes lifecycle transitions: Subscribe (via Enabled=true), Unsubscribe - // (via Enabled=false or Dispose), and the disposed-state check that gates - // Subscribe. Without this, two concurrent threads can both pass the - // _waitHandle null check in Unsubscribe and both call - // RegisteredWaitHandle.Unregister; the second call returns false (already - // unregistered) and does NOT signal its wait object, so WaitOne blocks - // indefinitely. Beyond that race, Subscribe and Unsubscribe both mutate - // _subscriptionHandle/_waitHandle/_isSubscribed; without the lock, - // Enabled=true racing with Enabled=false (or Dispose) can leave - // _subscriptionHandle disposed but ProcessNewEvents already running over - // it. The lock also makes the loser's Unsubscribe wait for the winner's - // drain to complete — required so both callers honor the "no callback - // fires after Unsubscribe returns" contract. - private readonly Lock _lifecycleLock = new(); private int _disposed; private bool _isSubscribed; @@ -49,7 +31,6 @@ public EventLogWatcher(string path, string? bookmark, bool renderXml = false) _bookmark = bookmark; _renderXml = renderXml; - // Validate the log exists by attempting to open it _queryHandle = NativeMethods.EvtQuery(EventLogSession.GlobalSession.Handle, path, null, PathType.LogName); int error = Marshal.GetLastWin32Error(); @@ -66,27 +47,16 @@ public EventLogWatcher(string path, bool renderXml = false) : this(path, null, r Dispose(false); } - /// - /// Raised when one or more new event records arrive from the underlying Windows EventLog subscription; subscriber - /// exceptions are caught and isolated, and synchronously calling or setting - /// to false from inside a handler throws - /// unless another thread has already begun disposing or unsubscribing the watcher (in which case the call is a - /// silent no-op for IDisposable idempotency); schedule the stop on a different thread to avoid the throw entirely. - /// + /// Raised when new event records arrive; subscriber exceptions are isolated. public event EventHandler? EventRecordWritten; - /// - /// Gets or sets whether the watcher is actively subscribed to the log; setting false tears down the native - /// subscription and blocks until any in-flight callbacks complete. - /// + /// Gets or sets whether the watcher is actively subscribed. /// - /// Thrown when the setter is invoked with a value of false from inside an - /// handler while the watcher is still subscribed; the unsubscribe path waits - /// for in-flight callbacks to complete and would self-deadlock the calling thread. If another thread has already - /// unsubscribed or disposed the watcher, the call is a silent no-op. + /// Thrown when set to false from inside an handler + /// while the watcher is still subscribed. /// /// - /// Thrown when the setter is invoked with a value of true after the watcher has been disposed. + /// Thrown when set to true after the watcher has been disposed. /// public bool Enabled { @@ -109,15 +79,10 @@ public bool Enabled } } - /// - /// Releases the native subscription and query handles; idempotent and safe to call multiple times from any - /// non-callback thread. - /// + /// Releases the native subscription and query handles; idempotent. /// - /// Thrown when invoked from inside an handler while the watcher is still live. - /// The unsubscribe path waits for in-flight callbacks to complete and would self-deadlock the calling thread. - /// If another thread has already begun disposing the watcher, the call is a silent no-op for IDisposable - /// idempotency. + /// Thrown when invoked from inside an handler unless + /// the watcher has already been disposed. /// public void Dispose() { @@ -127,41 +92,27 @@ public void Dispose() private void Dispose(bool disposing) { - // Idempotent fast path: if another thread already disposed (or is - // disposing), reentrant calls — including ones that originate from - // inside a callback after the disposing thread has already won the - // CAS — must be a no-op. This preserves the IDisposable.Dispose - // idempotency contract that callers historically relied on. if (Volatile.Read(ref _disposed) != 0) { return; } - // Detect reentrancy BEFORE marking _disposed. A handler invoking - // Dispose() on its own watcher would otherwise wedge inside Unsubscribe - // (which blocks on the in-flight callback drain) and leave the watcher - // unrecoverable. Throwing here keeps _disposed at 0 so a subsequent - // Dispose call from another thread (e.g. the test cleanup path) can - // still complete normally. + // Throw before the CAS so a subsequent Dispose from another thread can still complete. if (disposing) { ThrowIfCurrentCallbackWouldDeadlock(); } - // Use Interlocked.CompareExchange for atomic check-and-set. - // Only one thread will successfully change _disposed from 0 to 1. if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) { - return; // Already disposed by another thread + return; } if (disposing) { Unsubscribe(); - // Release the AutoResetEvent's kernel handle. Must happen AFTER - // Unsubscribe completes its drain — in-flight callbacks reference - // _newEvents.SafeWaitHandle via the ThreadPool wait, so disposing - // earlier could crash a callback mid-flight. + + // After Unsubscribe — in-flight callbacks hold _newEvents.SafeWaitHandle. _newEvents.Dispose(); } @@ -214,12 +165,6 @@ private void ProcessNewEvents(object? state, bool timedOut) @event.PathName = _path; - // Subscriber failures are isolated by contract: one handler - // must not prevent subsequent handlers in the multicast - // chain or future events from being delivered. Without this - // per-subscriber try/catch, a single throwing subscriber - // would short-circuit the multicast invoke and let the - // exception bubble into the ThreadPool worker. var subscribers = EventRecordWritten; if (subscribers is null) { continue; } @@ -232,9 +177,7 @@ private void ProcessNewEvents(object? state, bool timedOut) } catch (Exception) { - // Intentional: per-subscriber isolation. No logger - // is wired into this layer; if one is added later, - // log the exception here. + // Intentional: per-subscriber isolation. } } } @@ -258,13 +201,6 @@ private void ThrowIfCurrentCallbackWouldDeadlock() private void Subscribe() { - // Serialized with Unsubscribe so Enabled=true racing with Enabled=false - // (or Dispose) cannot leave _subscriptionHandle disposed while this - // method is still mutating it. Reentry from inside a callback is - // impossible because the Enabled setter calls - // ThrowIfCurrentCallbackWouldDeadlock before invoking Subscribe; the - // synchronous ProcessNewEvents call below cannot self-recurse because - // it runs before _waitHandle is registered. lock (_lifecycleLock) { ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); @@ -318,27 +254,13 @@ private void Subscribe() private void Unsubscribe() { - // Serialized with Subscribe (see _lifecycleLock comment) so concurrent - // callers (Dispose racing with Enabled=false, or two Enabled=false calls) - // cannot both attempt Unregister on the same _waitHandle, and so a - // racing Subscribe cannot interleave with the teardown. The lock also - // guarantees the losing caller observes the winner's drain — required - // for the "no callback fires after Unsubscribe returns" contract that - // callers (e.g. DatabaseService delete) rely on. Reentry from inside a - // callback is impossible because both call sites first invoke - // ThrowIfCurrentCallbackWouldDeadlock. lock (_lifecycleLock) { _isSubscribed = false; if (_waitHandle is not null) { - // Pass a wait object (not null) so Unregister blocks until all in-flight - // ProcessNewEvents callbacks have completed. Per the .NET docs on - // RegisteredWaitHandle.Unregister, passing null does NOT wait — and our - // callback creates per-event resolver scopes that hold pooled SQLite - // connections, so callers (e.g., DatabaseService delete) need a hard - // guarantee that no callback can fire after Unregister returns. + // Signal (not null) — Unregister(null) does NOT wait for in-flight callbacks. using var unregisterSignal = new ManualResetEvent(false); _waitHandle.Unregister(unregisterSignal); @@ -346,12 +268,7 @@ private void Unsubscribe() _waitHandle = null; } - // Use IsClosed (not IsInvalid) — SafeHandle.IsInvalid is based on - // the underlying handle value and does NOT flip after Dispose, so - // checking IsInvalid would let a serialized loser call Dispose a - // second time. SafeHandle.Dispose is idempotent today, but the - // explicit IsClosed check makes the intent clear and removes the - // dependency on that idempotency. + // IsClosed flips on Dispose; IsInvalid is value-based and never flips. if (!_subscriptionHandle.IsClosed) { _subscriptionHandle.Dispose(); From 64dea41c8e50a4e28655a31efa47300c7cd02361 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Fri, 8 May 2026 09:19:19 -0500 Subject: [PATCH 21/24] Honor alignment in span AppendFormatted and drain initial backlog outside lifecycle lock --- .../Logging/LogHandlerCore.cs | 14 ++++++++++++-- .../Readers/EventLogWatcher.cs | 5 +++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs b/src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs index b31b0c14..805fc69f 100644 --- a/src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs +++ b/src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs @@ -60,8 +60,18 @@ internal void AppendFormatted(T value, string? format) internal void AppendFormatted(ReadOnlySpan value) => _builder!.Append(value); - internal void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) => - _builder!.Append(value); + internal void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) + { + if (alignment == 0) { _builder!.Append(value); return; } + + int width = alignment == int.MinValue ? int.MaxValue : Math.Abs(alignment); + int padding = width - value.Length; + + if (padding <= 0) { _builder!.Append(value); return; } + + if (alignment >= 0) { _builder!.Append(' ', padding).Append(value); } + else { _builder!.Append(value).Append(' ', padding); } + } public override readonly string ToString() => _builder?.ToString() ?? string.Empty; diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index 6acc8ee5..48f47bd7 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -246,10 +246,11 @@ private void Subscribe() _isSubscribed = true; - ProcessNewEvents(null, false); - _waitHandle = ThreadPool.RegisterWaitForSingleObject(_newEvents, ProcessNewEvents, null, -1, false); } + + // Drain initial backlog outside the lock so handlers never run under _lifecycleLock. + ProcessNewEvents(null, false); } private void Unsubscribe() From 7573962c6fe52e754b0ec57611592a6d76ef0d09 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Fri, 8 May 2026 11:20:43 -0500 Subject: [PATCH 22/24] Use Volatile reads/writes for _isSubscribed and drain backlog before TP wait registration --- .../Readers/EventLogWatcher.cs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index 48f47bd7..b8ca74f3 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -62,16 +62,16 @@ public bool Enabled { get { - return _isSubscribed; + return Volatile.Read(ref _isSubscribed); } set { switch (value) { - case true when !_isSubscribed: + case true when !Volatile.Read(ref _isSubscribed): Subscribe(); break; - case false when _isSubscribed: + case false when Volatile.Read(ref _isSubscribed): ThrowIfCurrentCallbackWouldDeadlock(); Unsubscribe(); break; @@ -135,7 +135,7 @@ private void ProcessNewEvents(object? state, bool timedOut) do { - if (_isSubscribed is false) { break; } + if (Volatile.Read(ref _isSubscribed) is false) { break; } int count = 0; @@ -205,7 +205,7 @@ private void Subscribe() { ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); - if (_isSubscribed) { throw new InvalidOperationException("Already subscribed."); } + if (Volatile.Read(ref _isSubscribed)) { throw new InvalidOperationException("Already subscribed."); } EvtSubscribeFlags flag = string.IsNullOrEmpty(_bookmark) ? EvtSubscribeFlags.ToFutureEvents : @@ -244,20 +244,28 @@ private void Subscribe() NativeMethods.ThrowEventLogException(subscriptionError); } - _isSubscribed = true; - - _waitHandle = ThreadPool.RegisterWaitForSingleObject(_newEvents, ProcessNewEvents, null, -1, false); + Volatile.Write(ref _isSubscribed, true); } - // Drain initial backlog outside the lock so handlers never run under _lifecycleLock. + // Drain backlog before TP wait registration to prevent concurrent EvtNext. ProcessNewEvents(null, false); + + lock (_lifecycleLock) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + + // Concurrent Unsubscribe may have torn down the subscription during the drain. + if (!Volatile.Read(ref _isSubscribed)) { return; } + + _waitHandle = ThreadPool.RegisterWaitForSingleObject(_newEvents, ProcessNewEvents, null, -1, false); + } } private void Unsubscribe() { lock (_lifecycleLock) { - _isSubscribed = false; + Volatile.Write(ref _isSubscribed, false); if (_waitHandle is not null) { From e9f240c12dbde6828b0ab9080d3c5c8cb36ddee6 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Fri, 8 May 2026 11:39:18 -0500 Subject: [PATCH 23/24] Pass resolved Win32 message to UnauthorizedAccessException --- src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs index ba2dd505..e27b49a2 100644 --- a/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs @@ -679,7 +679,7 @@ internal static void ThrowEventLogException(int error) throw new OperationCanceledException(message); case Win32ErrorCodes.ERROR_ACCESS_DENIED: case Win32ErrorCodes.ERROR_INVALID_HANDLE: - throw new UnauthorizedAccessException(); + throw new UnauthorizedAccessException(message); default: throw new Exception(message); } From 81c9daa20c445b3d63ed03c7aae6a131b282b83d Mon Sep 17 00:00:00 2001 From: jschick04 Date: Fri, 8 May 2026 11:53:14 -0500 Subject: [PATCH 24/24] Release native subscription handle and ThreadPool wait on finalizer path --- src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index b8ca74f3..9f2fada1 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -115,6 +115,16 @@ private void Dispose(bool disposing) // After Unsubscribe — in-flight callbacks hold _newEvents.SafeWaitHandle. _newEvents.Dispose(); } + else + { + // Finalizer path: release native resources without locks or blocking signals. + _waitHandle?.Unregister(null); + + if (!_subscriptionHandle.IsClosed) + { + _subscriptionHandle.Dispose(); + } + } if (_queryHandle is { IsInvalid: false }) {