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.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 9a10cdfe..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; @@ -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/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/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.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.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..5ec53796 --- /dev/null +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/EventMessageProviderIntegrationTests.cs @@ -0,0 +1,307 @@ +// // 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 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"); + + 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 env vars; provider must expand %SystemRoot% before loading. + 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() + { + // Arrange — name is neither a registered publisher nor a channel. + 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() + { + // 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."); + + 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"; + + // 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) + { + 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 88% rename from src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs index 2dcc370b..c6e799ef 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/ProviderMetadataTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/ProviderMetadataTests.cs @@ -1,14 +1,13 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; -using EventLogExpert.Eventing.Models; +using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Providers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; using NSubstitute; using System.Collections.ObjectModel; -namespace EventLogExpert.Eventing.Tests.Providers; +namespace EventLogExpert.Eventing.IntegrationTests.Providers; public sealed class ProviderMetadataTests { @@ -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/Providers/RegistryProviderTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs similarity index 62% rename from src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs index 1c165d46..565a7e05 100644 --- a/src/EventLogExpert.Eventing.Tests/Providers/RegistryProviderTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Providers/RegistryProviderTests.cs @@ -1,15 +1,27 @@ // // 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 EventLogExpert.Eventing.Tests.TestUtils.Constants; +using Microsoft.Win32; using NSubstitute; -namespace EventLogExpert.Eventing.Tests.Providers; +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() { @@ -38,23 +50,17 @@ public void GetMessageFilesForLegacyProvider_WhenCalled_ShouldNotIncludeSysFiles 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)); - } + 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 +76,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] @@ -108,10 +116,10 @@ public void GetMessageFilesForLegacyProvider_WhenCommonWindowsProvider_ShouldRet var provider = new RegistryProvider(); // Act - var result = provider.GetMessageFilesForLegacyProvider(Constants.ApplicationLogName).ToList(); + var result = FindAnyLegacyProviderFiles(provider); // Assert - Assert.NotNull(result); + Assert.NotEmpty(result); } [Fact] @@ -149,50 +157,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 +209,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 +223,17 @@ public void GetMessageFilesForLegacyProvider_WhenMultipleSemicolonSeparatedPaths // Arrange var provider = new RegistryProvider(); - // Act - Try to find any provider with multiple files - var result = FindAnyLegacyProviderFiles(provider); + // Act + 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 — 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."); + + 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 +249,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,33 +284,25 @@ public void GetMessageFilesForLegacyProvider_WhenProviderExists_ShouldReturnEnum [Fact] public void GetMessageFilesForLegacyProvider_WhenProviderFound_ShouldLogFoundMessage() { - // Arrange + // 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); + 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 - }; + // Act + var foundFiles = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); - 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; - } + // 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); - // If no providers found, skip assertion + mockLogger.Received() + .Debug(Arg.Is(h => + h.ToString().Contains("Found message file for legacy provider") && + h.ToString().Contains(providerName))); } [Fact] @@ -319,15 +315,16 @@ public void GetMessageFilesForLegacyProvider_WhenReturnedPaths_ShouldBeDllOrExe( var result = FindAnyLegacyProviderFiles(provider); // Assert - if (result.Count != 0) - { - Assert.All(result, 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] @@ -340,16 +337,15 @@ public void GetMessageFilesForLegacyProvider_WhenReturnedPaths_ShouldBeFullPaths var result = FindAnyLegacyProviderFiles(provider); // Assert - if (result.Count != 0) - { - Assert.All(result, path => + Assert.NotEmpty(result); + + 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}"); }); - } } [Fact] @@ -362,13 +358,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] @@ -381,31 +377,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]); } } @@ -438,23 +434,9 @@ 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[] - { - 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(); @@ -466,4 +448,77 @@ 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, []); + } + + // 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()) + { + 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 per provider name across log subtrees. + if (!seenWithEmf.Add(providerName)) + { + continue; + } + + if (!emf.Contains(';')) + { + continue; + } + + var files = provider.GetMessageFilesForLegacyProvider(providerName).ToList(); + + if (files.Count > 1) + { + return (providerName, files); + } + } + } + + return (null, []); + } } diff --git a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs similarity index 79% rename from src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs index 6b7e7e80..280927d8 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogInformationTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogInformationTests.cs @@ -1,28 +1,13 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.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() { @@ -40,25 +25,18 @@ public void Constructor_WhenApplicationLog_ShouldPopulateAllProperties() Assert.NotNull(logInfo.RecordCount); } - [Fact] - public void Constructor_WhenCalledMultipleTimes_ShouldCreateIndependentInstances() + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t")] + public void Constructor_WhenBlankLogName_ShouldThrowArgumentException(string logName) { // 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); + // Act & Assert + Assert.Throws(() => + new EventLogInformation(session, logName, PathType.LogName)); } [Theory] @@ -116,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() { @@ -139,25 +106,25 @@ 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(() => + 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)); } @@ -176,37 +143,19 @@ public void Constructor_WhenPathTypeLogName_ShouldSucceed() } [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_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 — malformed paths must not be masked as UAE. + Assert.NotNull(ex); + Assert.IsNotType(ex); } [Fact] @@ -315,22 +264,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() { @@ -345,7 +278,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); } @@ -365,20 +298,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() { @@ -450,10 +369,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 +388,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 +423,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.IntegrationTests/Readers/EventLogReaderTests.cs similarity index 90% rename from src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs index 48fa8b18..4f00b1d6 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogReaderTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogReaderTests.cs @@ -1,11 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.Readers; public sealed class EventLogReaderTests { @@ -166,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); @@ -180,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); } @@ -225,18 +218,18 @@ public void IsValid_WhenInvalidLogName_ShouldBeFalse() [Fact] public void LastBookmark_AfterTryGetEvents_ShouldBeSet() { - // Arrange - using var reader = new EventLogReader(Constants.ApplicationLogName, PathType.LogName); + // Arrange — fixture keeps the read deterministic on minimal hosts. + 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] @@ -252,8 +245,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); @@ -263,20 +258,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] @@ -471,29 +461,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() { @@ -594,7 +561,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.IntegrationTests/Readers/EventLogSessionTests.cs similarity index 88% rename from src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs index 7b2204fb..bff735db 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogSessionTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogSessionTests.cs @@ -1,11 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Tests.TestUtils.Constants; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.Readers; public sealed class EventLogSessionTests { @@ -23,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)] @@ -80,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)); } @@ -178,20 +163,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() { @@ -217,7 +188,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 +206,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) { @@ -329,20 +300,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() { @@ -383,7 +340,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.IntegrationTests/Readers/EventLogWatcherTests.cs similarity index 59% rename from src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs rename to src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs index de0bc0cc..bc9a73d3 100644 --- a/src/EventLogExpert.Eventing.Tests/Readers/EventLogWatcherTests.cs +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/EventLogWatcherTests.cs @@ -1,13 +1,14 @@ // // 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; +using System.Collections.Concurrent; using System.Diagnostics; +using System.Reflection; -namespace EventLogExpert.Eventing.Tests.Readers; +namespace EventLogExpert.Eventing.IntegrationTests.Readers; public sealed class EventLogWatcherTests { @@ -44,13 +45,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] @@ -107,11 +108,10 @@ 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); - + var bookmark = reader.LastBookmark; // Act @@ -133,22 +133,32 @@ 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) => + { + Interlocked.Increment(ref eventCount); + eventReceived.Set(); + }; - watcher.EventRecordWritten += (sender, record) => receivedEvents.Add(record); watcher.Enabled = true; // Act 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); - Thread.Sleep(200); // Wait to ensure event would have been received if subscription was active + bool received = eventReceived.Wait(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // Assert - Assert.Empty(receivedEvents); + int actual = Volatile.Read(ref eventCount); + Assert.False(received, "Should not have received any event after dispose"); + Assert.Equal(countBefore, actual); } [Fact] @@ -162,6 +172,31 @@ public void Dispose_BeforeSubscribe_ShouldNotThrow() watcher.Dispose(); } + [Fact] + public void Dispose_ShouldReleaseUnderlyingWaitHandle() + { + // Arrange + var watcher = new EventLogWatcher(Constants.ApplicationLogName); + watcher.Enabled = true; + + var newEventsField = typeof(EventLogWatcher).GetField( + "_newEvents", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.True(newEventsField is not null, "_newEvents field missing — was it renamed?"); + + var newEvents = (AutoResetEvent?)newEventsField.GetValue(watcher); + Assert.NotNull(newEvents); + + // Act + watcher.Dispose(); + + // Assert + Assert.True(newEvents.SafeWaitHandle.IsClosed, "AutoResetEvent kernel handle leaked after Dispose."); + + watcher.Dispose(); + Assert.True(newEvents.SafeWaitHandle.IsClosed); + } + [Fact] public void Dispose_WhenCalled_ShouldUnsubscribe() { @@ -176,6 +211,47 @@ 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) => + { + 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); + } + [Fact] public void Dispose_WhenCalledMultipleTimes_ShouldNotThrow() { @@ -241,6 +317,32 @@ 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 + 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) { } + }, ct)); + tasks.Add(Task.Run(() => watcher.Enabled = false, ct)); + } + + var allDone = Task.WhenAll(tasks); + + // Assert: timeout regression-tests the RegisteredWaitHandle race. + await allDone.WaitAsync(TimeSpan.FromSeconds(30), ct); + } + [Fact] public void Enabled_WhenSetToFalse_ShouldUnsubscribe() { @@ -255,6 +357,47 @@ 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) => + { + 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() { @@ -293,6 +436,17 @@ 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 + Assert.Throws(() => watcher.Enabled = true); + } + [Fact] public void Enabled_WhenSetToTrueTwice_ShouldNotThrow() { @@ -345,19 +499,19 @@ 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 watcher.Enabled = true; watcher.Enabled = false; - receivedEvents.Clear(); + Interlocked.Exchange(ref eventCount, 0); eventReceived.Reset(); watcher.Enabled = true; @@ -369,8 +523,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] @@ -378,18 +533,67 @@ 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) => + { + Interlocked.Increment(ref eventCount); + 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 + int countBefore = Volatile.Read(ref eventCount); + eventReceived.Reset(); + + // 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); + + bool fired = eventReceived.Wait(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // Assert - 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] + 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 + 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.Null(snapshot[0].Error); } [Fact] @@ -402,7 +606,7 @@ public void EventRecordWritten_ShouldHaveRecordId() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -416,10 +620,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] @@ -432,7 +637,7 @@ public void EventRecordWritten_ShouldHaveValidTimeCreated() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -440,7 +645,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); @@ -449,11 +654,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] @@ -466,7 +672,7 @@ public void EventRecordWritten_ShouldIncludePathName() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -480,9 +686,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] @@ -495,7 +702,7 @@ public void EventRecordWritten_ShouldIncludeProperties() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -509,9 +716,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] @@ -524,7 +732,7 @@ public void EventRecordWritten_ShouldProvideCorrectSender() watcher.EventRecordWritten += (sender, record) => { - capturedSender = sender; + Volatile.Write(ref capturedSender, sender); eventReceived.Set(); }; @@ -538,27 +746,33 @@ 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] - public void EventRecordWritten_ShouldReceiveEventsInOrder() + 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); + + // Bound Signal — ambient callbacks past expectedCount would throw. + int count = Interlocked.Increment(ref signalCount); + + if (count <= expectedCount) { - receivedEvents.Add(record); + countdown.Signal(); } - - countdown.Signal(); }; watcher.Enabled = true; @@ -567,59 +781,68 @@ public void EventRecordWritten_ShouldReceiveEventsInOrder() using var eventLog = new EventLog(Constants.ApplicationLogName); eventLog.Source = Constants.ApplicationLogName; - 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); // Assert + var snapshot = receivedEvents.ToArray(); 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++) + Assert.True(snapshot.Length >= expectedCount, $"Expected at least {expectedCount} events in snapshot, but got {snapshot.Length}."); + + 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}"); } } [Fact] - public void EventRecordWritten_WhenErrorOccurs_ShouldReceiveEventRecordWithError() + public void EventRecordWritten_WhenHandlerThrows_ShouldStillDeliverFutureEvents() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); - var eventReceived = new ManualResetEventSlim(false); + int attempts = 0; + var firstObserved = new ManualResetEventSlim(false); + var laterObserved = new ManualResetEventSlim(false); watcher.EventRecordWritten += (sender, record) => { - receivedEvents.Add(record); + // Signal "first observed" before the throw so the stimulus thread can release the second write. + int n = Interlocked.Increment(ref attempts); - if (!eventReceived.IsSet) + 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 + 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 - 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)); + 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] @@ -627,13 +850,19 @@ 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(); + // Bound Signal — ambient callbacks past expectedCount would throw. + int count = Interlocked.Increment(ref eventCount); + + if (count <= expectedCount) + { + countdown.Signal(); + } }; watcher.Enabled = true; @@ -649,24 +878,77 @@ 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 — handler attached but Enabled is never set to true. + using var watcher = new EventLogWatcher(Constants.ApplicationLogName); + int eventCount = 0; + var eventReceived = new ManualResetEventSlim(false); + + watcher.EventRecordWritten += (sender, record) => + { + Interlocked.Increment(ref eventCount); + eventReceived.Set(); + }; + + // 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); + + 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 when watcher is not Enabled"); + Assert.Equal(0, actual); + } + + [Fact] + public void EventRecordWritten_WhenOneOfMultipleHandlersThrows_ShouldStillNotifyOthers() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); - var receivedEvents = new List(); + int failingCount = 0; + int succeedingCount = 0; + var succeedingObserved = new ManualResetEventSlim(false); + + // First subscriber always throws. + watcher.EventRecordWritten += (sender, record) => + { + Interlocked.Increment(ref failingCount); + throw new InvalidOperationException("subscriber A always fails"); + }; + + // Second subscriber must still fire despite the first throwing. + watcher.EventRecordWritten += (sender, record) => + { + Interlocked.Increment(ref succeedingCount); + succeedingObserved.Set(); + }; - watcher.EventRecordWritten += (sender, record) => receivedEvents.Add(record); + watcher.Enabled = true; // Act - Thread.Sleep(100); // Brief wait to ensure no events arrive + 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 - Assert.Empty(receivedEvents); + 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] @@ -674,19 +956,20 @@ 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); + Volatile.Write(ref capturedEvent, record); + Interlocked.Increment(ref eventCount); eventReceived.Set(); }; // 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); @@ -694,34 +977,34 @@ 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] 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; 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(); }; // 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); @@ -729,8 +1012,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] @@ -738,17 +1022,19 @@ 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) + // Bound Signal — ambient callbacks past expectedCount would throw. + int count = Interlocked.Increment(ref eventCount); + + if (count <= expectedCount) { - receivedEvents.Add(record); + countdown.Signal(); } - - countdown.Signal(); }; watcher.Enabled = true; @@ -792,19 +1078,24 @@ 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] - 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 — bad bookmark must surface an exception that is not masked as UAE. + Assert.NotNull(ex); + Assert.IsNotType(ex); } [Fact] @@ -812,20 +1103,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; @@ -838,13 +1139,15 @@ 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] - public void EventRecordWritten_WithNoSubscribers_ShouldNotThrow() + public async Task EventRecordWritten_WithNoSubscribers_ShouldNotThrow() { // Arrange using var watcher = new EventLogWatcher(Constants.ApplicationLogName); @@ -856,7 +1159,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); @@ -872,7 +1175,7 @@ public void EventRecordWritten_WithRenderXmlFalse_ShouldNotIncludeXml() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -886,9 +1189,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] @@ -901,7 +1205,7 @@ public void EventRecordWritten_WithRenderXmlTrue_ShouldIncludeXml() watcher.EventRecordWritten += (sender, record) => { - capturedEvent = record; + Volatile.Write(ref capturedEvent, record); eventReceived.Set(); }; @@ -915,9 +1219,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] @@ -942,21 +1247,31 @@ 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(); + + 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; @@ -970,9 +1285,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] @@ -1006,7 +1323,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.IntegrationTests/Readers/SmallEvtxFixture.cs b/src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs new file mode 100644 index 00000000..340ad394 --- /dev/null +++ b/src/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs @@ -0,0 +1,149 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Readers; +using System.Diagnostics; +using System.Text.RegularExpressions; + +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 +{ + 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"); + + // 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 — hard-coding [1, 5] fails once the log rolls past 5. + 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, /rd:true reads newest-first, /f:RenderedXml gives parseable 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, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + 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(s_wevtutilTimeout)) + { + try { proc.Kill(true); } + catch + { /* best effort */ + } + + 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) + { + throw new InvalidOperationException( + $"wevtutil.exe ({commandLabel}) exited with code {proc.ExitCode}. Stderr: {stderr}"); + } + + return (stdout, stderr); + } + + 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"); + } + + private void VerifyMinimumEvents() + { + // 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; + + while (reader.TryGetEvents(out var batch) && batch.Length > 0) + { + total += batch.Length; + } + + if (total < MinimumExpectedEvents) + { + 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."); + } + } +} 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/EventProviderDatabase/EventProviderDbContextTests.cs b/src/EventLogExpert.Eventing.Tests/EventProviderDatabase/EventProviderDbContextTests.cs index a1847f5d..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; @@ -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/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 3d707165..0703180c 100644 --- a/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs +++ b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventResolverBaseTests.cs @@ -2,7 +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; @@ -495,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); @@ -2614,6 +2613,65 @@ public void ResolveEvent_WithPopulatedProviderButUnknownEventAndMultipleProperti Assert.DoesNotContain("Failed to resolve", displayEvent.Description); } + [Fact] + public void ResolveEvent_WithPrimaryAndSupplementalDetailsButUnmappedEventId_ShouldReturnNoMatchingMessage() + { + // 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; + + 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); + Assert.DoesNotContain("Primary event 100", displayEvent.Description); + Assert.DoesNotContain("Supplemental event 200", displayEvent.Description); + Assert.Equal(SupplementalOnlyTaskName, displayEvent.TaskCategory); + } + [Fact] public void ResolveEvent_WithPrimaryAndSupplementalTask_ShouldPreferPrimaryTaskName() { 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/EventResolvers/EventXmlResolverTests.cs b/src/EventLogExpert.Eventing.Tests/EventResolvers/EventXmlResolverTests.cs index b85bb9a6..616d9b04 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; @@ -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.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..78062f4e 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; @@ -345,32 +345,85 @@ public void ResolveEvent_WithLocalResolver_ShouldResolveEvent() } [Fact] - public void ResolveEvent_WithMultipleProviders_ShouldResolveAll() + public void ResolveEvent_WithMixedDbAndUnknownProviders_ShouldOnlyApplyDatabaseTextToDbProvider() { - // Arrange - using var resolver = new EventResolver(); + const string DBProviderName = "MixedScenarioDbProvider"; + const string DBMessageText = "Resolved from test database for mixed scenario"; + const ushort EventId = 1000; - var providers = new[] - { - Constants.ApplicationLogName, - Constants.SystemLogName, - Constants.TestProviderName - }; + string unknownProviderName = $"NotInDb-{Guid.NewGuid()}"; + string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); - // Act & Assert - foreach (var provider in providers) + try { - var eventRecord = new EventRecord + using (var context = new EventProviderDbContext(dbPath, false)) { - ProviderName = provider, - Id = 1000, - ComputerName = "TestComputer", - LogName = Constants.ApplicationLogName - }; + context.ProviderDetails.Add(new ProviderDetails + { + ProviderName = DBProviderName, + Messages = + [ + new MessageModel + { + ProviderName = DBProviderName, + ShortId = (short)EventId, + Text = DBMessageText + } + ] + }); - var displayEvent = resolver.ResolveEvent(eventRecord); - Assert.NotNull(displayEvent); - Assert.Equal(provider, displayEvent.Source); + context.SaveChanges(); + } + + var dbCollection = Substitute.For(); + dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); + + DisplayEventModel dbDisplayEvent; + DisplayEventModel unknownDisplayEvent; + + using (var resolver = new EventResolver(dbCollection)) + { + var dbEventRecord = new EventRecord + { + ProviderName = DBProviderName, + Id = EventId, + ComputerName = "TestComputer", + LogName = Constants.ApplicationLogName + }; + + var unknownEventRecord = new EventRecord + { + ProviderName = unknownProviderName, + Id = EventId, + ComputerName = "TestComputer", + LogName = Constants.ApplicationLogName + }; + + // Act + 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); + 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 +449,34 @@ 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) + { + // Theory verifies CreateEventModel propagates per-record fields and ProviderName. + // 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() { diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/ResolverMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Interop/NativeErrorResolverTests.cs similarity index 67% rename from src/EventLogExpert.Eventing.Tests/Helpers/ResolverMethodsTests.cs rename to src/EventLogExpert.Eventing.Tests/Interop/NativeErrorResolverTests.cs index daa65988..c50f9fcf 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/ResolverMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Interop/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; +namespace EventLogExpert.Eventing.Tests.Interop; -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/EventMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs similarity index 85% rename from src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs rename to src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs index e4037139..062ebd08 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/EventMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs @@ -1,14 +1,13 @@ // // Copyright (c) Microsoft Corporation. // // 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; -namespace EventLogExpert.Eventing.Tests.Helpers; +namespace EventLogExpert.Eventing.Tests.Interop; -public sealed class EventMethodsTests +public sealed class NativeMethodsEvtTests { [Fact] public void ConvertVariant_WhenAnsiString_ShouldReturnString() @@ -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); @@ -750,120 +749,120 @@ 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] public void ThrowEventLogException_WhenCancelled_ShouldThrowOperationCanceledException() { // Arrange - int error = Interop.ERROR_CANCELLED; + int error = Win32ErrorCodes.ERROR_CANCELLED; // Act & Assert - Assert.Throws(() => EventMethods.ThrowEventLogException(error)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + Assert.Throws(() => NativeMethods.ThrowEventLogException(error)); } [Fact] 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)); + 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/Helpers/NativeMethodsTests.cs b/src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsTests.cs similarity index 94% rename from src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs rename to src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsTests.cs index 36fd68a2..8c83a31c 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/NativeMethodsTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Interop/NativeMethodsTests.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +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/Providers/EventMessageProviderTests.cs b/src/EventLogExpert.Eventing.Tests/Providers/EventMessageProviderTests.cs index a3055d71..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.Helpers; -using EventLogExpert.Eventing.Models; +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 = EventMethods.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 = EventMethods.EvtGetChannelConfigProperty( - channelConfig, - EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, - 0, - 0, - IntPtr.Zero, - out int bufferSize); - - int error = Marshal.GetLastWin32Error(); - - if (!success && error != Interop.ERROR_INSUFFICIENT_BUFFER) - { - return false; - } - - IntPtr buffer = Marshal.AllocHGlobal(bufferSize); - - try - { - success = EventMethods.EvtGetChannelConfigProperty( - channelConfig, - EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, - 0, - bufferSize, - buffer, - out _); - - if (!success) - { - return false; - } - - var variant = Marshal.PtrToStructure(buffer); - publisher = EventMethods.ConvertVariant(variant) as string; - - return !string.IsNullOrEmpty(publisher); - } - finally - { - Marshal.FreeHGlobal(buffer); - } - } } diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs b/src/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs similarity index 91% rename from src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs rename to src/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs index ff058a3e..47d0aa7a 100644 --- a/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs +++ b/src/EventLogExpert.Eventing.Tests/Readers/LogNamesTests.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; +using EventLogExpert.Eventing.Readers; -namespace EventLogExpert.Eventing.Tests.Helpers; +namespace EventLogExpert.Eventing.Tests.Readers; public sealed class LogNamesTests { diff --git a/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs b/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs deleted file mode 100644 index 90d0e496..00000000 --- a/src/EventLogExpert.Eventing.Tests/Readers/SmallEvtxFixture.cs +++ /dev/null @@ -1,73 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using System.Diagnostics; - -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. -/// -internal sealed class SmallEvtxFixture : IDisposable -{ - 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. - 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. - 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."); - - if (!proc.WaitForExit(TimeSpan.FromSeconds(30))) - { - try { proc.Kill(entireProcessTree: true); } catch { /* best effort */ } - - throw new InvalidOperationException("wevtutil.exe did not complete within 30 seconds."); - } - - if (proc.ExitCode != 0) - { - var err = proc.StandardError.ReadToEnd(); - - throw new InvalidOperationException( - $"wevtutil.exe exited with code {proc.ExitCode}. Stderr: {err}"); - } - } - - 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. - } - } -} diff --git a/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj b/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj index a7a8991e..089c31d8 100644 --- a/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj +++ b/src/EventLogExpert.Eventing/EventLogExpert.Eventing.csproj @@ -10,4 +10,9 @@ + + + + + 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/EventProviderDbContext.cs b/src/EventLogExpert.Eventing/EventProviderDatabase/EventProviderDbContext.cs index e8962d35..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; @@ -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/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/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 4e13abdf..b0412767 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventResolverBase.cs @@ -1,9 +1,11 @@ // // 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; using System.Buffers; using System.Collections.Concurrent; using System.Security.Principal; @@ -172,18 +174,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 +786,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 +803,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/EventResolvers/EventXmlResolver.cs b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs index 6b63d847..264ba890 100644 --- a/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/EventResolvers/EventXmlResolver.cs @@ -1,7 +1,7 @@ // // 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; @@ -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,10 +102,22 @@ 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 = EventMethods.EvtQuery( + using EvtHandle handle = NativeMethods.EvtQuery( EventLogSession.GlobalSession.Handle, owningLog, $"*[System[EventRecordID='{recordId}']]", @@ -105,7 +128,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; } @@ -113,20 +136,7 @@ protected virtual string ResolveXml(string owningLog, long recordId, PathType pa if (eventHandle.IsInvalid) { return string.Empty; } - return EventMethods.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; - } + return NativeMethods.RenderEventXml(eventHandle) ?? string.Empty; } private void EnsureCapacity() @@ -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/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); - } -} 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/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/Models/EvtHandle.cs b/src/EventLogExpert.Eventing/Interop/EvtHandle.cs similarity index 78% rename from src/EventLogExpert.Eventing/Models/EvtHandle.cs rename to src/EventLogExpert.Eventing/Interop/EvtHandle.cs index 22742723..d7bb49de 100644 --- a/src/EventLogExpert.Eventing/Models/EvtHandle.cs +++ b/src/EventLogExpert.Eventing/Interop/EvtHandle.cs @@ -1,10 +1,9 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Helpers; using Microsoft.Win32.SafeHandles; -namespace EventLogExpert.Eventing.Models; +namespace EventLogExpert.Eventing.Interop; internal sealed partial class EvtHandle : SafeHandleZeroOrMinusOneIsInvalid { @@ -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/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/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/Models/LibraryHandle.cs b/src/EventLogExpert.Eventing/Interop/LibraryHandle.cs similarity index 89% rename from src/EventLogExpert.Eventing/Models/LibraryHandle.cs rename to src/EventLogExpert.Eventing/Interop/LibraryHandle.cs index a49d733f..1295d37e 100644 --- a/src/EventLogExpert.Eventing/Models/LibraryHandle.cs +++ b/src/EventLogExpert.Eventing/Interop/LibraryHandle.cs @@ -1,10 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -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/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/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/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/EventMethods.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs similarity index 85% rename from src/EventLogExpert.Eventing/Helpers/EventMethods.cs rename to src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs index 2f3f8b72..e27b49a2 100644 --- a/src/EventLogExpert.Eventing/Helpers/EventMethods.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.Evt.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Models; @@ -8,193 +8,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; @@ -532,11 +348,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 +380,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 +403,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 +480,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 +542,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 +615,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); } @@ -844,26 +660,26 @@ 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) { - 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: - throw new UnauthorizedAccessException(); + case Win32ErrorCodes.ERROR_ACCESS_DENIED: + case Win32ErrorCodes.ERROR_INVALID_HANDLE: + throw new UnauthorizedAccessException(message); default: throw new Exception(message); } diff --git a/src/EventLogExpert.Eventing/Helpers/NativeMethods.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.cs similarity index 87% rename from src/EventLogExpert.Eventing/Helpers/NativeMethods.cs rename to src/EventLogExpert.Eventing/Interop/NativeMethods.cs index a1a2e55f..7fdb924a 100644 --- a/src/EventLogExpert.Eventing/Helpers/NativeMethods.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.cs @@ -1,31 +1,13 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Models; 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; - -[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 -} +namespace EventLogExpert.Eventing.Interop; internal static partial class NativeMethods { @@ -147,7 +129,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 +144,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/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/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..805fc69f --- /dev/null +++ b/src/EventLogExpert.Eventing/Logging/LogHandlerCore.cs @@ -0,0 +1,110 @@ +// // 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) + { + 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; + + 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 96% rename from src/EventLogExpert.Eventing/Helpers/TraceLogger.cs rename to src/EventLogExpert.Eventing/Logging/TraceLogger.cs index 13f0ed47..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 { @@ -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/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/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/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.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")] diff --git a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs index 72d36fc0..aa015f62 100644 --- a/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/EventMessageProvider.cs @@ -1,7 +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; @@ -25,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) @@ -444,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); @@ -463,7 +464,7 @@ private bool TryGetChannelOwningPublisher(string channelName, out string? publis try { - bool success = EventMethods.EvtGetChannelConfigProperty( + bool success = NativeMethods.EvtGetChannelConfigProperty( channelConfig, EvtChannelConfigPropertyId.EvtChannelConfigOwningPublisher, 0, @@ -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}"); @@ -483,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, @@ -502,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 58b5213b..84f66062 100644 --- a/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs +++ b/src/EventLogExpert.Eventing/Providers/ProviderMetadata.cs @@ -1,7 +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; @@ -27,12 +28,12 @@ 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) { - Error = ResolverMethods.GetErrorMessage((uint)Converter.HResultFromWin32(error)); + Error = NativeErrorResolver.GetErrorMessage((uint)HResultConverter.HResultFromWin32(error)); } } @@ -49,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); @@ -85,12 +86,12 @@ 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) { - Error = ResolverMethods.GetErrorMessage((uint)Converter.HResultFromWin32(error)); + Error = NativeErrorResolver.GetErrorMessage((uint)HResultConverter.HResultFromWin32(error)); return events.AsReadOnly(); } @@ -113,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)); } @@ -135,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); } @@ -189,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); } @@ -243,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); } @@ -333,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 != Interop.ERROR_INSUFFICIENT_BUFFER) + 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 @@ -364,16 +365,16 @@ 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 != Interop.ERROR_NO_MORE_ITEMS) + if (error != Win32ErrorCodes.ERROR_NO_MORE_ITEMS) { - EventMethods.ThrowEventLogException(error); + NativeMethods.ThrowEventLogException(error); } - + return null; } @@ -383,7 +384,7 @@ private string GetPublisherMetadataProperty(EvtPublisherMetadataPropertyId prope try { - bool success = EventMethods.EvtGetPublisherMetadataProperty( + bool success = NativeMethods.EvtGetPublisherMetadataProperty( _publisherMetadataHandle, propertyId, 0, @@ -393,14 +394,14 @@ 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); + NativeMethods.ThrowEventLogException(error); } buffer = Marshal.AllocHGlobal(bufferUsed); - success = EventMethods.EvtGetPublisherMetadataProperty( + success = NativeMethods.EvtGetPublisherMetadataProperty( _publisherMetadataHandle, propertyId, 0, @@ -412,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 { @@ -431,7 +432,7 @@ private EvtHandle GetPublisherMetadataPropertyHandle(EvtPublisherMetadataPropert try { - bool success = EventMethods.EvtGetPublisherMetadataProperty( + bool success = NativeMethods.EvtGetPublisherMetadataProperty( _publisherMetadataHandle, propertyId, 0, @@ -441,14 +442,14 @@ 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); + NativeMethods.ThrowEventLogException(error); } buffer = Marshal.AllocHGlobal(bufferUsed); - success = EventMethods.EvtGetPublisherMetadataProperty( + success = NativeMethods.EvtGetPublisherMetadataProperty( _publisherMetadataHandle, propertyId, 0, @@ -460,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..7011d558 100644 --- a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs @@ -1,12 +1,13 @@ // // 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; -public class RegistryProvider(ITraceLogger? logger = null) +internal sealed class RegistryProvider(ITraceLogger? logger = null) { private readonly ITraceLogger? _logger = logger; @@ -92,5 +93,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 ddaf43bf..07cd7d07 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogInformation.cs @@ -1,8 +1,7 @@ -// // 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; @@ -11,7 +10,17 @@ public sealed class EventLogInformation { internal EventLogInformation(EventLogSession session, string logName, PathType pathType) { - using EvtHandle handle = EventMethods.EvtOpenLog(session.Handle, logName, pathType); + ArgumentException.ThrowIfNullOrWhiteSpace(logName); + + using EvtHandle handle = NativeMethods.EvtOpenLog(session.Handle, logName, pathType); + + int error = Marshal.GetLastWin32Error(); + + // Surface the real EvtOpenLog failure — without this, GetLogInfo on a NULL handle masks it as UAE. + if (handle.IsInvalid) + { + NativeMethods.ThrowEventLogException(error); + } Attributes = (int?)(uint?)GetLogInfo(handle, EvtLogPropertyId.Attributes); CreationTime = (DateTime?)GetLogInfo(handle, EvtLogPropertyId.CreationTime); @@ -45,27 +54,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 != Interop.ERROR_INSUFFICIENT_BUFFER) + 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 c957ee9a..16e95443 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogReader.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.Interop; using EventLogExpert.Eventing.Models; using System.Buffers; using System.Runtime.InteropServices; @@ -12,7 +12,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; @@ -61,12 +61,12 @@ 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) { 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; } @@ -86,12 +86,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) @@ -115,23 +115,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, @@ -142,9 +142,9 @@ 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); + NativeMethods.ThrowEventLogException(error); } Span buffer = stackalloc char[bufferUsed / sizeof(char)]; @@ -153,7 +153,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, @@ -168,7 +168,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 b91fa4cb..645448af 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogSession.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogSession.cs @@ -1,8 +1,7 @@ -// // 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; @@ -35,12 +34,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 +62,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 +88,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,32 +103,32 @@ 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) { - 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); + 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,32 +136,32 @@ 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) { - 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); + 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 beb4ae03..9f2fada1 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -1,15 +1,18 @@ -// // 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.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; + private readonly ConcurrentDictionary _callbackThreadIds = new(); + private readonly Lock _lifecycleLock = new(); private readonly AutoResetEvent _newEvents = new(false); private readonly string _path; private readonly EvtHandle _queryHandle; @@ -28,63 +31,99 @@ public EventLogWatcher(string path, string? bookmark, bool renderXml = false) _bookmark = bookmark; _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) { } ~EventLogWatcher() { - Dispose(disposing: false); + Dispose(false); } + /// Raised when new event records arrive; subscriber exceptions are isolated. public event EventHandler? EventRecordWritten; + /// Gets or sets whether the watcher is actively subscribed. + /// + /// Thrown when set to false from inside an handler + /// while the watcher is still subscribed. + /// + /// + /// Thrown when set to true after the watcher has been disposed. + /// 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; } } } + /// Releases the native subscription and query handles; idempotent. + /// + /// Thrown when invoked from inside an handler unless + /// the watcher has already been disposed. + /// public void Dispose() { - Dispose(disposing: true); + Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { - // Use Interlocked.CompareExchange for atomic check-and-set. - // Only one thread will successfully change _disposed from 0 to 1. + if (Volatile.Read(ref _disposed) != 0) + { + return; + } + + // Throw before the CAS so a subsequent Dispose from another thread can still complete. + if (disposing) + { + ThrowIfCurrentCallbackWouldDeadlock(); + } + if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) { - return; // Already disposed by another thread + return; } if (disposing) { Unsubscribe(); + + // 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 }) @@ -95,116 +134,164 @@ private void Dispose(bool disposing) private void ProcessNewEvents(object? state, bool timedOut) { - bool success; - var buffer = new IntPtr[64]; + int threadId = Environment.CurrentManagedThreadId; + + _callbackThreadIds[threadId] = 0; - do + try { - if (_isSubscribed is false) { break; } + bool success; + var buffer = new IntPtr[64]; - int count = 0; + do + { + if (Volatile.Read(ref _isSubscribed) is false) { break; } - success = EventMethods.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 = EventMethods.RenderEvent(eventHandle); - @event.Properties = EventMethods.RenderEventProperties(eventHandle); + using var eventHandle = new EvtHandle(buffer[i]); + EventRecord @event; - if (_renderXml) + try { - @event.Xml = EventMethods.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; + + var subscribers = EventRecordWritten; - @event.PathName = _path; + if (subscribers is null) { continue; } - EventRecordWritten?.Invoke(this, @event); + foreach (Delegate handler in subscribers.GetInvocationList()) + { + try + { + ((EventHandler)handler)(this, @event); + } + catch (Exception) + { + // Intentional: per-subscriber isolation. + } + } + } } - } 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."); } + lock (_lifecycleLock) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); - EvtSubscribeFlags flag = string.IsNullOrEmpty(_bookmark) ? - EvtSubscribeFlags.ToFutureEvents : - EvtSubscribeFlags.StartAfterBookmark; + if (Volatile.Read(ref _isSubscribed)) { throw new InvalidOperationException("Already subscribed."); } - EvtHandle bookmarkHandle = string.IsNullOrEmpty(_bookmark) ? - EvtHandle.Zero : - EventMethods.EvtCreateBookmark(_bookmark); + EvtSubscribeFlags flag = string.IsNullOrEmpty(_bookmark) ? + EvtSubscribeFlags.ToFutureEvents : + EvtSubscribeFlags.StartAfterBookmark; - if (!string.IsNullOrEmpty(_bookmark) && bookmarkHandle.IsInvalid) - { - int error = Marshal.GetLastWin32Error(); - bookmarkHandle.Dispose(); - EventMethods.ThrowEventLogException(error); - } + EvtHandle bookmarkHandle = string.IsNullOrEmpty(_bookmark) ? + EvtHandle.Zero : + NativeMethods.EvtCreateBookmark(_bookmark); - _subscriptionHandle = EventMethods.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) - { - EventMethods.ThrowEventLogException(subscriptionError); - } + int subscriptionError = Marshal.GetLastWin32Error(); - _isSubscribed = true; + if (_subscriptionHandle.IsInvalid) + { + NativeMethods.ThrowEventLogException(subscriptionError); + } + Volatile.Write(ref _isSubscribed, true); + } + + // Drain backlog before TP wait registration to prevent concurrent EvtNext. ProcessNewEvents(null, false); - _waitHandle = ThreadPool.RegisterWaitForSingleObject(_newEvents, ProcessNewEvents, null, -1, 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() { - _isSubscribed = false; - - if (_waitHandle is not null) + 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; - } + Volatile.Write(ref _isSubscribed, false); - if (!_subscriptionHandle.IsInvalid) - { - _subscriptionHandle.Dispose(); + if (_waitHandle is not null) + { + // Signal (not null) — Unregister(null) does NOT wait for in-flight callbacks. + using var unregisterSignal = new ManualResetEvent(false); + + _waitHandle.Unregister(unregisterSignal); + unregisterSignal.WaitOne(); + _waitHandle = null; + } + + // IsClosed flips on Dispose; IsInvalid is value-based and never flips. + if (!_subscriptionHandle.IsClosed) + { + _subscriptionHandle.Dispose(); + } } } } 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.Eventing/Readers/PathType.cs b/src/EventLogExpert.Eventing/Readers/PathType.cs new file mode 100644 index 00000000..49621514 --- /dev/null +++ b/src/EventLogExpert.Eventing/Readers/PathType.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.Eventing.Readers; + +public enum PathType +{ + LogName = 1, + FilePath +} 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/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/FilterCategoryItemsCacheTests.cs b/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs index c8bd040f..92a18607 100644 --- a/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs +++ b/src/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.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/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/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 cee29888..00178deb 100644 --- a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs +++ b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs @@ -2,8 +2,9 @@ // // 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; 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..259e505c 100644 --- a/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs +++ b/src/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.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.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/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/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.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/Models/EventLogData.cs b/src/EventLogExpert.UI/Models/EventLogData.cs index fcaa2c12..f6f90ecf 100644 --- a/src/EventLogExpert.UI/Models/EventLogData.cs +++ b/src/EventLogExpert.UI/Models/EventLogData.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; 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/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 246dda2d..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; @@ -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/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 764f73dd..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; @@ -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..2b3bbaed 100644 --- a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs +++ b/src/EventLogExpert.UI/Store/LoggingMiddleware.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 EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.StatusBar; 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 @@ + 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/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..e24aaf58 100644 --- a/src/EventLogExpert/MainPage.xaml.cs +++ b/src/EventLogExpert/MainPage.xaml.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 EventLogExpert.Services; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 71c5ecf4..4f55690f 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.Eventing.Logging; 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/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 9fa32866..53052354 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -17,7 +17,7 @@ @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