From a6b13e911aa71250c31b7dde51d56e9961ba73ad Mon Sep 17 00:00:00 2001 From: Aaron LeMasters Date: Sat, 21 Oct 2023 19:42:58 -0400 Subject: [PATCH] Add trace sessions and related unit tests --- EnabledProvider.cs | 526 ++++++++++++++++++++++++++ EventParser.cs | 14 +- FileTrace.cs | 111 +----- NativeDefinitions.cs | 84 +++++ ParsedEtwEvent.cs | 13 +- ParsedEtwManifestEvent.cs | 6 +- ParsedEtwManifestField.cs | 6 +- ParsedEtwProvider.cs | 6 +- ParsedEtwSession.cs | 135 +++++++ ParsedEtwString.cs | 8 +- ParsedEtwTemplateItem.cs | 6 +- PayloadFilter.cs | 40 +- ProviderParser.cs | 42 +++ README.md | 36 +- RealTimeTrace.cs | 566 ++-------------------------- SessionParser.cs | 253 +++++++++++++ TraceLogger.cs | 17 +- TraceSession.cs | 145 +++++++ UnitTests/FileTraceTests.cs | 2 + UnitTests/FilterByEventIdTests.cs | 14 +- UnitTests/FilterByExeNameTests.cs | 11 +- UnitTests/FilterByPackageTests.cs | 84 +---- UnitTests/FilterByPayloadTests.cs | 50 ++- UnitTests/FilterByProcessTests.cs | 11 +- UnitTests/FilterByStackwalkTests.cs | 16 +- UnitTests/RealTimeTraceTests.cs | 8 +- UnitTests/SessionTests.cs | 98 +++++ Utilities.cs | 93 +++++ 28 files changed, 1532 insertions(+), 869 deletions(-) create mode 100644 EnabledProvider.cs create mode 100644 ParsedEtwSession.cs create mode 100644 SessionParser.cs create mode 100644 TraceSession.cs create mode 100644 UnitTests/SessionTests.cs diff --git a/EnabledProvider.cs b/EnabledProvider.cs new file mode 100644 index 0000000..bdcf7a0 --- /dev/null +++ b/EnabledProvider.cs @@ -0,0 +1,526 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +using System.Diagnostics; +using System.Runtime.InteropServices; +using static etwlib.NativeTraceControl; +using static etwlib.NativeTraceConsumer; + +namespace etwlib +{ + public class EnabledProvider : IEquatable, IComparable, IDisposable + { + public Guid Id; + public EventTraceLevel Level; + public ulong AllKeywords; + public ulong AnyKeywords; + private List m_FilterDescriptors; + private nint m_AggregatedPayloadFilters; + private bool m_Disposed; + private nint m_ParametersBuffer; + private nint m_FiltersBuffer; + + public EnabledProvider(Guid Id, EventTraceLevel Level, ulong AllKeywords, ulong AnyKeywords) + { + this.Id = Id; + this.Level = Level; + this.AllKeywords = AllKeywords; + this.AnyKeywords = AnyKeywords; + m_FilterDescriptors = new List(); + } + + ~EnabledProvider() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + if (m_Disposed) + { + return; + } + + m_Disposed = true; + + // + // Release filter descriptor buffers + // + foreach (var filter in m_FilterDescriptors) + { + if (filter.Type == EventFilterTypePayload) + { + // + // This one is released further down by ETW. + // + continue; + } + Marshal.FreeHGlobal(filter.Ptr); + } + + // + // Release array wrapper for filter descriptors + // + if (m_FiltersBuffer != nint.Zero) + { + Marshal.FreeHGlobal(m_FiltersBuffer); + } + + // + // Release containing parameters buffer + // + if (m_ParametersBuffer != nint.Zero) + { + Marshal.FreeHGlobal(m_ParametersBuffer); + } + + // + // Release any aggregated TDH payload filters + // + if (m_AggregatedPayloadFilters != nint.Zero) + { + var result = TdhCleanupPayloadEventFilterDescriptor( + m_AggregatedPayloadFilters); + Debug.Assert(result == ERROR_SUCCESS); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public override bool Equals(object? Other) + { + if (Other == null) + { + return false; + } + var field = Other as EnabledProvider; + return Equals(field); + } + + public bool Equals(EnabledProvider? Other) + { + if (Other == null) + { + return false; + } + return Id == Other.Id; + } + + public static bool operator ==(EnabledProvider? Provider1, EnabledProvider? Provider2) + { + if ((object)Provider1 == null || (object)Provider2 == null) + return Equals(Provider1, Provider2); + return Provider1.Equals(Provider2); + } + + public static bool operator !=(EnabledProvider? Provider1, EnabledProvider? Provider2) + { + if ((object)Provider1 == null || (object)Provider2 == null) + return !Equals(Provider1, Provider2); + return !(Provider1.Equals(Provider2)); + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public int CompareTo(EnabledProvider? Other) + { + if (Other == null) + { + return 1; + } + return Id.CompareTo(Other.Id); + } + + public override string ToString() + { + return $"{Id} : Level={Level}, All={AllKeywords:X}, Any={AnyKeywords:X}"; + } + + public uint Disable(long SessionHandle) + { + return EnableTraceEx2(SessionHandle, + ref Id, + EventControlCode.DisableProvider, + Level, + 0, 0, 0, + nint.Zero); + } + + public uint Enable(long SessionHandle) + { + GenerateTraceParameters(); + return EnableTraceEx2( + SessionHandle, + ref Id, + EventControlCode.EnableProvider, + Level, + AnyKeywords, + AllKeywords, + 0xffffffff, + m_ParametersBuffer); + } + + public void SetProcessFilter(List ProcessIds) + { + if (ProcessIds.Count == 0) + { + throw new Exception("Event ID list required."); + } + + if (ProcessIds.Count > MaxEventFilterPidCount) + { + throw new Exception($"Maximum {MaxEventFilterPidCount} filtered processes"); + } + + foreach (var desc in m_FilterDescriptors) + { + if (desc.Type == EventFilterTypePid) + { + throw new Exception("PID filter can only be used once per session."); + } + } + + var descriptor = new EVENT_FILTER_DESCRIPTOR(); + var size = ProcessIds.Count * sizeof(int); + descriptor.Type = EventFilterTypePid; + descriptor.Size = (uint)size; + descriptor.Ptr = Marshal.AllocHGlobal(size); + if (descriptor.Ptr == nint.Zero) + { + throw new Exception("Out of memory"); + } + Marshal.Copy(ProcessIds.ToArray(), 0, descriptor.Ptr, ProcessIds.Count); + m_FilterDescriptors.Add(descriptor); + } + + public void SetEventIdsFilter(List EventIds, bool Enable) + { + if (EventIds.Count == 0) + { + throw new Exception("Event ID list required."); + } + + if (EventIds.Count > MaxEventFilterEventIdCount) + { + throw new Exception($"Maximum {MaxEventFilterEventIdCount} event IDs"); + } + + foreach (var desc in m_FilterDescriptors) + { + if (desc.Type == EventFilterTypeEventId) + { + throw new Exception("Event ID filter can only be used once per session."); + } + } + + AddEventIdFilter(EventFilterTypeEventId, EventIds, Enable); + } + + public void SetStackwalkEventIdsFilter(List EventIds, bool Enable) + { + if (EventIds.Count == 0) + { + throw new Exception("Event ID list required."); + } + + if (EventIds.Count > MaxEventFilterEventIdCount) + { + throw new Exception($"Maximum {MaxEventFilterEventIdCount} stackwalk event IDs"); + } + + foreach (var desc in m_FilterDescriptors) + { + if (desc.Type == EventFilterTypeStackwalk) + { + throw new Exception("Stackwalk filter can only be used once per session."); + } + } + + AddEventIdFilter(EventFilterTypeStackwalk, EventIds, Enable); + } + + public void AddPayloadFilters(List> Filters) + { + if (Filters.Count == 0) + { + throw new Exception("At least one payload filter is required."); + } + + foreach (var desc in m_FilterDescriptors) + { + if (desc.Type == EventFilterTypePayload) + { + throw new Exception("Payload filter can only be used once per session."); + } + } + + var filters = new nint[Filters.Count]; + var index = 0; + + foreach (var entry in Filters) + { + var filter = entry.Item1.Create(); + Debug.Assert(filter != nint.Zero); + filters[index++] = filter; + } + + var matchAllFlags = Filters.ConvertAll(f => Convert.ToByte(f.Item2)).ToArray(); + var eventFilterDescriptor = Marshal.AllocHGlobal( + Marshal.SizeOf(typeof(EVENT_FILTER_DESCRIPTOR))); + if (eventFilterDescriptor == nint.Zero) + { + throw new Exception("Out of memory"); + } + var result = TdhAggregatePayloadFilters( + (uint)Filters.Count, + filters, + matchAllFlags, + eventFilterDescriptor); + if (result != ERROR_SUCCESS) + { + throw new Exception($"TdhAggregatePayloadFilters failed: 0x{result:X}"); + } + m_AggregatedPayloadFilters = eventFilterDescriptor; + } + + public void SetFilteredExeName(string ExeName) + { + // + // Note: the ExeName string can contain multiple executable names separated + // by semi-colons. + // + var length = (ExeName.Length + 1) * 2; + if (length > MaxEventFilterDataSize) + { + throw new Exception("Exe name too long"); + } + + foreach (var desc in m_FilterDescriptors) + { + if (desc.Type == EventFilterTypeExecutableName) + { + throw new Exception("Exe filter can only be used once per session."); + } + } + + var descriptor = new EVENT_FILTER_DESCRIPTOR(); + descriptor.Type = EventFilterTypeExecutableName; + descriptor.Size = (uint)length; + descriptor.Ptr = Marshal.StringToHGlobalUni(ExeName); + if (descriptor.Ptr == nint.Zero) + { + throw new Exception("Out of memory"); + } + m_FilterDescriptors.Add(descriptor); + } + + public void SetFilteredPackageAppId(string AppId) + { + // + // Note: the AppId string can contain multiple MS Store App IDs separated + // by semi-colons. + // + if (AppId.Length * 2 > MaxEventFilterDataSize) + { + throw new Exception("AppId too long"); + } + + foreach (var desc in m_FilterDescriptors) + { + if (desc.Type == EventFilterTypePackageAppId) + { + throw new Exception("App ID filter can only be used once per session."); + } + } + + var descriptor = new EVENT_FILTER_DESCRIPTOR(); + descriptor.Type = EventFilterTypePackageAppId; + descriptor.Size = (uint)AppId.Length * 2; + descriptor.Ptr = Marshal.StringToHGlobalUni(AppId); + if (descriptor.Ptr == nint.Zero) + { + throw new Exception("Out of memory"); + } + m_FilterDescriptors.Add(descriptor); + } + + public void SetFilteredPackageId(string PackageId) + { + // + // Note: the PackageId string can contain multiple MS Store package IDs separated + // by semi-colons. + // + if (PackageId.Length * 2 > MaxEventFilterDataSize) + { + throw new Exception("PackageId too long"); + } + + foreach (var desc in m_FilterDescriptors) + { + if (desc.Type == EventFilterTypePackageId) + { + throw new Exception("Package ID filter can only be used once per session."); + } + } + + var descriptor = new EVENT_FILTER_DESCRIPTOR(); + descriptor.Type = EventFilterTypePackageId; + descriptor.Size = (uint)PackageId.Length * 2; + descriptor.Ptr = Marshal.StringToHGlobalUni(PackageId); + if (descriptor.Ptr == nint.Zero) + { + throw new Exception("Out of memory"); + } + m_FilterDescriptors.Add(descriptor); + } + + public void SetStackwalkLevelKw(EventTraceLevel Level, ulong MatchAnyKeyword, ulong MatchAllKeyword, bool FilterIn) + { + foreach (var desc in m_FilterDescriptors) + { + if (desc.Type == EventFilterTypeStackWalkLevelKw) + { + throw new Exception("Stackwalk Level/KW filter can only be used once per session."); + } + } + + var descriptor = new EVENT_FILTER_DESCRIPTOR(); + descriptor.Type = EventFilterTypeStackWalkLevelKw; + descriptor.Size = (uint)Marshal.SizeOf(typeof(EVENT_FILTER_LEVEL_KW)); + descriptor.Ptr = Marshal.AllocHGlobal((int)descriptor.Size); + if (descriptor.Ptr == nint.Zero) + { + throw new Exception("Out of memory"); + } + + var filter = new EVENT_FILTER_LEVEL_KW(); + filter.Level = Level; + filter.MatchAllKeyword = MatchAllKeyword; + filter.MatchAnyKeyword = MatchAnyKeyword; + filter.FilterIn = FilterIn; + Marshal.StructureToPtr(filter, descriptor.Ptr, false); + m_FilterDescriptors.Add(descriptor); + } + + private + nint + GenerateTraceParameters() + { + if (m_ParametersBuffer != nint.Zero) + { + Debug.Assert(false); + throw new Exception("Parameters have already been generated"); + } + + var parameters = new ENABLE_TRACE_PARAMETERS + { + Version = 2, + EnableProperty = EnableTraceProperties.ProcessStartKey | + EnableTraceProperties.Sid + }; + + var numFilters = m_FilterDescriptors.Count; + if (m_AggregatedPayloadFilters != nint.Zero) + { + numFilters++; + } + + if (numFilters > 0) + { + Debug.Assert(numFilters < MaxEventFiltersCount); + + // + // EnableTraceEx2 expects an array of EVENT_FILTER_DESCRIPTOR + // + var size = Marshal.SizeOf(typeof(EVENT_FILTER_DESCRIPTOR)); + m_FiltersBuffer = Marshal.AllocHGlobal(numFilters * size); + if (m_FiltersBuffer == nint.Zero) + { + throw new Exception("Out of memory"); + } + nint pointer = m_FiltersBuffer; + foreach (var desc in m_FilterDescriptors) + { + Marshal.StructureToPtr(desc, pointer, false); + pointer = nint.Add(pointer, size); + } + if (m_AggregatedPayloadFilters != nint.Zero) + { + var payloadFilter = (EVENT_FILTER_DESCRIPTOR)Marshal.PtrToStructure( + m_AggregatedPayloadFilters, typeof(EVENT_FILTER_DESCRIPTOR))!; + Debug.Assert(payloadFilter.Type == EventFilterTypePayload); + Debug.Assert(payloadFilter.Ptr != nint.Zero); + Marshal.StructureToPtr(payloadFilter, pointer, false); + pointer = nint.Add(pointer, size); + } + parameters.FilterDescCount = (uint)numFilters; + parameters.EnableFilterDesc = m_FiltersBuffer; + + if (m_FilterDescriptors.Any( + f => f.Type == EventFilterTypeStackwalk || + f.Type == EventFilterTypeStackWalkLevelKw)) + { + // + // Note: events over 64kb will be dropped by etw with this set. + // + parameters.EnableProperty |= EnableTraceProperties.StackTrace; + } + } + + m_ParametersBuffer = Marshal.AllocHGlobal(Marshal.SizeOf(parameters)); + Marshal.StructureToPtr(parameters, m_ParametersBuffer, false); + return m_ParametersBuffer; + } + + private + void + AddEventIdFilter(uint FilterType, List EventIds, bool Enable) + { + var descriptor = new EVENT_FILTER_DESCRIPTOR(); + descriptor.Type = FilterType; + var size = Marshal.SizeOf(typeof(EVENT_FILTER_EVENT_ID)) + + ((EventIds.Count - 1) * Marshal.SizeOf(typeof(ushort))); + descriptor.Size = (uint)size; + descriptor.Ptr = Marshal.AllocHGlobal(size); + if (descriptor.Ptr == nint.Zero) + { + throw new Exception("Out of memory"); + } + var filter = new EVENT_FILTER_EVENT_ID(); + filter.FilterIn = Enable; + filter.Reserved = 0; + filter.Count = (ushort)EventIds.Count; + Marshal.StructureToPtr(filter, descriptor.Ptr, false); + var dest = nint.Add(descriptor.Ptr, (int)Marshal.OffsetOf("Events")); + var ids = EventIds.ConvertAll(id => (ushort)id).ToArray(); + var byteSize = ids.Length * Marshal.SizeOf(typeof(ushort)); + var bytes = new byte[byteSize]; + Buffer.BlockCopy(ids, 0, bytes, 0, byteSize); + Marshal.Copy(bytes, 0, dest, byteSize); + m_FilterDescriptors.Add(descriptor); + } + } +} diff --git a/EventParser.cs b/EventParser.cs index 4694e8b..f8e8ef8 100644 --- a/EventParser.cs +++ b/EventParser.cs @@ -311,7 +311,7 @@ EventParserBuffers Buffers m_ParsedEvent.Provider.Source = m_Buffers.m_TraceEventInfo.Source.ToString(); - m_ParsedEvent.Keywords = new List(); + var keywords = new List(); if (m_Buffers.m_TraceEventInfo.KeywordsNameOffset > 0) { for (int offset = m_Buffers.m_TraceEventInfo.KeywordsNameOffset; ;) @@ -321,10 +321,14 @@ EventParserBuffers Buffers { break; } - m_ParsedEvent.Keywords.Add(str.Trim()); + keywords.Add(str.Trim()); offset += Encoding.Unicode.GetByteCount(str) + 2; } } + if (keywords.Count > 0) + { + m_ParsedEvent.Keywords = string.Join(",", keywords); + } if (m_Buffers.m_TraceEventInfo.TaskNameOffset > 0) { @@ -403,8 +407,10 @@ EventParserBuffers Buffers { case EventHeaderExtendedDataType.Sid: { - m_ParsedEvent!.UserSid = new byte[size]; - Marshal.Copy(data, m_ParsedEvent.UserSid, 0, size); + var binarySid = new byte[size]; + Marshal.Copy(data, binarySid, 0, size); + var sid = new System.Security.Principal.SecurityIdentifier(binarySid, 0); + m_ParsedEvent!.UserSid = sid.ToString(); break; } case EventHeaderExtendedDataType.RelatedActivityId: diff --git a/FileTrace.cs b/FileTrace.cs index a0d2b98..6fe2860 100644 --- a/FileTrace.cs +++ b/FileTrace.cs @@ -17,24 +17,18 @@ specific language governing permissions and limitations under the License. */ using System.Diagnostics; -using System.Runtime.InteropServices; -using static etwlib.NativeTraceConsumer; using static etwlib.NativeTraceControl; namespace etwlib { using static TraceLogger; - public class FileTrace : IDisposable + public class FileTrace : TraceSession { - private bool m_Disposed; - private readonly string m_EtlFileName; - private long m_PerfFreq; - public FileTrace(string EtlFileName) { - m_EtlFileName = EtlFileName; - m_Disposed = false; + m_LogFile.LogFileName = EtlFileName; + m_LogFile.ProcessTraceMode = ProcessTraceMode.EventRecord; } ~FileTrace() @@ -42,103 +36,12 @@ public FileTrace(string EtlFileName) Dispose(false); } - protected virtual void Dispose(bool disposing) - { - if (m_Disposed) - { - return; - } - - Trace(TraceLoggerType.FileTrace, - TraceEventType.Information, - "Disposing FileTrace"); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public void Consume( - EventRecordCallback EventCallback, - BufferCallback BufferCallback - ) + public override void Start() { - var logfile = new EVENT_TRACE_LOGFILE() - { - LogFileName = m_EtlFileName, - EventCallback = EventCallback, - BufferCallback = BufferCallback, - ProcessTraceMode = ProcessTraceMode.EventRecord - }; - var logFilePointer = Marshal.AllocHGlobal(Marshal.SizeOf(logfile)); - Trace(TraceLoggerType.FileTrace, - TraceEventType.Information, - "Consuming events from ETL file " + m_EtlFileName); - Marshal.StructureToPtr(logfile, logFilePointer, false); - var handle = OpenTrace(logFilePointer); - // - // Marshal the structure back so we can get the PerfFreq - // - logfile = (EVENT_TRACE_LOGFILE)Marshal.PtrToStructure( - logFilePointer, typeof(EVENT_TRACE_LOGFILE))!; - Marshal.FreeHGlobal(logFilePointer); - logFilePointer = nint.Zero; - if (handle == -1 || handle == 0) - { - var error = "OpenTrace() returned an invalid handle: 0x" + - Marshal.GetLastWin32Error().ToString("X"); - Trace(TraceLoggerType.FileTrace, - TraceEventType.Error, - error); - throw new Exception(error); - } - Trace(TraceLoggerType.FileTrace, - TraceEventType.Information, - "Trace session successfully opened, processing trace.."); - - try - { - // - // Update PerfFreq so event's timestamps can be parsed. - // - m_PerfFreq = logfile.LogfileHeader.PerfFreq.QuadPart; - - // - // Blocking call. The caller's BufferCallback must return false to - // unblock this routine. - // - var status = ProcessTrace( - new long[1] { handle }, - 1, - nint.Zero, - nint.Zero); - if (status != ERROR_SUCCESS) - { - var error = "ProcessTrace() failed: 0x" + status.ToString("X") + - ", GetLastError: " + Marshal.GetLastWin32Error().ToString("X"); - Trace(TraceLoggerType.FileTrace, - TraceEventType.Error, - error); - throw new Exception(error); - } - Trace(TraceLoggerType.FileTrace, - TraceEventType.Information, - "Trace processing successfully completed."); - } - finally - { - CloseTrace(handle); - } - } - - public - long - GetPerfFreq() - { - return m_PerfFreq; + TraceEventType.Information, + $"Starting FileTrace for log {m_LogFile.LogFileName}"); + return; } } } diff --git a/NativeDefinitions.cs b/NativeDefinitions.cs index d1c7695..ddf9619 100644 --- a/NativeDefinitions.cs +++ b/NativeDefinitions.cs @@ -61,6 +61,7 @@ public enum EventControlCode : uint CaptureState = 2, } + [Flags] public enum EnableTraceProperties : uint { Sid = 0x1, @@ -151,6 +152,46 @@ public enum WNodeClientContext : uint CpuCycleCounter = 3 } + public enum TRACE_QUERY_INFO_CLASS : uint + { + TraceGuidQueryList = 0, + TraceGuidQueryInfo = 1, + TraceGuidQueryProcess = 2, + TraceStackTracingInfo = 3, + TraceSystemTraceEnableFlagsInfo = 4, + TraceSampledProfileIntervalInfo = 5, + TraceProfileSourceConfigInfo = 6, + TraceProfileSourceListInfo = 7, + TracePmcEventListInfo = 8, + TracePmcCounterListInfo = 9, + TraceSetDisallowList = 10, + TraceVersionInfo = 11, + TraceGroupQueryList = 12, + TraceGroupQueryInfo = 13, + TraceDisallowListQuery = 14, + TraceInfoReserved15, + TracePeriodicCaptureStateListInfo = 16, + TracePeriodicCaptureStateInfo = 17, + TraceProviderBinaryTracking = 18, + TraceMaxLoggersQuery = 19, + TraceLbrConfigurationInfo = 20, + TraceLbrEventListInfo = 21, + TraceMaxPmcCounterQuery = 22, + TraceStreamCount = 23, + TraceStackCachingInfo = 24, + TracePmcCounterOwners = 25, + TraceUnifiedStackCachingInfo = 26, + TracePmcSessionInformation = 27, + MaxTraceSetInfoClass = 28 + } + + [Flags] + public enum TRACE_PROVIDER_INSTANCE_FLAGS : uint + { + TRACE_PROVIDER_FLAG_LEGACY = 1, + TRACE_PROVIDER_FLAG_PRE_ENABLE = 2 + } + // // ETW filtering // @@ -321,6 +362,38 @@ public struct EVENT_FILTER_LEVEL_KW public bool FilterIn; } + // + // advapi structs for enumerating trace sessions + // + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct TRACE_GUID_INFO + { + public uint InstanceCount; + public uint Reserved; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct TRACE_PROVIDER_INSTANCE_INFO + { + public uint NextOffset; + public uint EnableCount; + public uint Pid; + public TRACE_PROVIDER_INSTANCE_FLAGS Flags; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct TRACE_ENABLE_INFO + { + public uint IsEnabled; + public EventTraceLevel Level; + public byte Reserved1; + public ushort LoggerId; + public EnableTraceProperties EnableProperty; + public uint Reserved2; + public ulong MatchAnyKeyword; + public ulong MatchAllKeyword; + } + #endregion #region APIs @@ -904,6 +977,16 @@ internal static extern uint TdhQueryProviderFieldInformation( [In, Out] nint Buffer, // PPROVIDER_FIELD_INFOARRAY [In, Out] ref uint BufferSize ); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern uint EnumerateTraceGuidsEx( + [In] NativeTraceControl.TRACE_QUERY_INFO_CLASS InfoClass, + [In] nint InBuffer, + [In] uint InBufferSize, + [In, Out] nint OutBuffer, + [In] uint OutBufferSize, + [In, Out] ref uint ReturnLength + ); #endregion public const int ERROR_SUCCESS = 0; @@ -915,6 +998,7 @@ internal static extern uint TdhQueryProviderFieldInformation( public const int ERROR_NOT_FOUND = 1168; public const int ERROR_XML_PARSE_ERROR = 1465; public const int ERROR_RESOURCE_TYPE_NOT_FOUND = 1813; + public const int ERROR_WMI_GUID_NOT_FOUND = 4200; public const int ERROR_EMPTY = 4306; public const int ERROR_EVT_INVALID_EVENT_DATA = 15005; public const int ERROR_MUI_FILE_NOT_FOUND = 15100; diff --git a/ParsedEtwEvent.cs b/ParsedEtwEvent.cs index 90800f3..64392ff 100644 --- a/ParsedEtwEvent.cs +++ b/ParsedEtwEvent.cs @@ -28,12 +28,12 @@ public class ParsedEtwEvent public uint ProcessId { get; set; } public long ProcessStartKey { get; set; } public uint ThreadId { get; set; } - public byte[]? UserSid { get; set; } + public string? UserSid { get; set; } public Guid ActivityId { get; set; } public DateTime Timestamp { get; set; } public string Level { get; set; } public ParsedEtwString? Channel { get; set; } - public List? Keywords { get; set; } + public string? Keywords { get; set; } public ulong KeywordsUlong { get; set; } public ParsedEtwString? Task { get; set; } public ParsedEtwString? Opcode { get; set; } @@ -64,14 +64,7 @@ public override string ToString() } sb.AppendLine($"Level: {Level}"); sb.AppendLine($"Channel: {Channel}"); ; - if (Keywords != null) - { - sb.AppendLine("Keywords: "); - foreach (var kw in Keywords) - { - sb.AppendLine($" {kw}"); - } - } + sb.AppendLine($"Keywords: {Keywords}"); sb.AppendLine($"Task: {Task}"); sb.AppendLine($"Opcode: {Opcode}"); sb.AppendLine($"Template data:"); diff --git a/ParsedEtwManifestEvent.cs b/ParsedEtwManifestEvent.cs index e234eda..55ce569 100644 --- a/ParsedEtwManifestEvent.cs +++ b/ParsedEtwManifestEvent.cs @@ -57,11 +57,7 @@ public override bool Equals(object? Other) return false; } var field = Other as ParsedEtwManifestEvent; - if (field == null) - { - return false; - } - return Equals(Other); + return Equals(field); } public bool Equals(ParsedEtwManifestEvent? Other) diff --git a/ParsedEtwManifestField.cs b/ParsedEtwManifestField.cs index 6168d13..0eace55 100644 --- a/ParsedEtwManifestField.cs +++ b/ParsedEtwManifestField.cs @@ -39,11 +39,7 @@ public override bool Equals(object? Other) return false; } var field = Other as ParsedEtwManifestField; - if (field == null) - { - return false; - } - return Equals(Other); + return Equals(field); } public bool Equals(ParsedEtwManifestField? Other) diff --git a/ParsedEtwProvider.cs b/ParsedEtwProvider.cs index 5438024..12b0f94 100644 --- a/ParsedEtwProvider.cs +++ b/ParsedEtwProvider.cs @@ -32,11 +32,7 @@ public override bool Equals(object? Other) return false; } var field = Other as ParsedEtwProvider; - if (field == null) - { - return false; - } - return Equals(Other); + return Equals(field); } public bool Equals(ParsedEtwProvider? Other) diff --git a/ParsedEtwSession.cs b/ParsedEtwSession.cs new file mode 100644 index 0000000..5c9a108 --- /dev/null +++ b/ParsedEtwSession.cs @@ -0,0 +1,135 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +using System.Text; + +namespace etwlib +{ + using static NativeTraceControl; + + public class SessionEnabledProvider + { + public Guid ProviderId; + public uint ProcessId; + public TRACE_PROVIDER_INSTANCE_FLAGS InstanceFlags; + public EventTraceLevel Level; + public EnableTraceProperties EnableProperty; + public ulong MatchAnyKeyword; + public ulong MatchAllKeyword; + + public SessionEnabledProvider( + Guid providerId, + uint processId, + TRACE_PROVIDER_INSTANCE_FLAGS instanceFlags, + EventTraceLevel level, + EnableTraceProperties enableProperty, + ulong matchAnyKeyword, + ulong matchAllKeyword) + { + ProviderId = providerId; + ProcessId = processId; + InstanceFlags = instanceFlags; + Level = level; + EnableProperty = enableProperty; + MatchAnyKeyword = matchAnyKeyword; + MatchAllKeyword = matchAllKeyword; + } + + public override string ToString() + { + var enablePropertyStr = ""; + if (EnableProperty != 0) + { + enablePropertyStr = $", EnableProperty={EnableProperty}"; + } + return $"{ProviderId} registered by PID {ProcessId}, InstanceFlags={InstanceFlags}, "+ + $"Level={Level}{enablePropertyStr}, AnyKeyword={MatchAnyKeyword:X}, "+ + $"AllKeyword={MatchAllKeyword:X}"; + } + } + + public class ParsedEtwSession : IEquatable, IComparable + { + public ushort LoggerId; + public List EnabledProviders; + + public ParsedEtwSession(ushort Id) + { + LoggerId = Id; + EnabledProviders = new List(); + } + + public override bool Equals(object? Other) + { + if (Other == null) + { + return false; + } + var field = Other as ParsedEtwSession; + return Equals(field); + } + + public bool Equals(ParsedEtwSession? Other) + { + if (Other == null) + { + return false; + } + return LoggerId == Other.LoggerId; + } + + public static bool operator ==(ParsedEtwSession? Session1, ParsedEtwSession? Session2) + { + if ((object)Session1 == null || (object)Session2 == null) + return Equals(Session1, Session2); + return Session1.Equals(Session2); + } + + public static bool operator !=(ParsedEtwSession? Session1, ParsedEtwSession? Session2) + { + if ((object)Session1 == null || (object)Session2 == null) + return !Equals(Session1, Session2); + return !(Session1.Equals(Session2)); + } + + public override int GetHashCode() + { + return LoggerId.GetHashCode(); + } + + public int CompareTo(ParsedEtwSession? Other) + { + if (Other == null) + { + return 1; + } + return LoggerId.CompareTo(Other.LoggerId); + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine($"Logger ID {LoggerId}"); + foreach (var p in EnabledProviders) + { + sb.AppendLine($" {p}"); + } + return sb.ToString(); + } + } +} diff --git a/ParsedEtwString.cs b/ParsedEtwString.cs index 010255d..5eceef7 100644 --- a/ParsedEtwString.cs +++ b/ParsedEtwString.cs @@ -37,16 +37,12 @@ public override bool Equals(object? Other) return false; } var field = Other as ParsedEtwString; - if (field == null) - { - return false; - } - return Equals(Other); + return Equals(field); } public bool Equals(ParsedEtwString? Other) { - if (Other == null) + if ((object)Other == null) { return false; } diff --git a/ParsedEtwTemplateItem.cs b/ParsedEtwTemplateItem.cs index a9a0dbe..47ae406 100644 --- a/ParsedEtwTemplateItem.cs +++ b/ParsedEtwTemplateItem.cs @@ -67,11 +67,7 @@ public override bool Equals(object? Other) return false; } var field = Other as ParsedEtwTemplateItem; - if (field == null) - { - return false; - } - return Equals(Other); + return Equals(field); } public bool Equals(ParsedEtwTemplateItem? Other) diff --git a/PayloadFilter.cs b/PayloadFilter.cs index ebc5e0f..563a4cf 100644 --- a/PayloadFilter.cs +++ b/PayloadFilter.cs @@ -84,6 +84,17 @@ public void AddPredicate(PAYLOAD_FILTER_PREDICATE Predicate) } public void AddPredicate(string Field, PAYLOAD_OPERATOR Operator, string Value) + { + ValidatePredicate(Field, Operator, Value); + m_Predicates.Add(new PAYLOAD_FILTER_PREDICATE + { + PayloadFieldName = Field, + CompareOp = Operator, + Value = Value, + }); + } + + public static void ValidatePredicate(string Field, PAYLOAD_OPERATOR Operator, string Value) { if (string.IsNullOrEmpty(Field) || string.IsNullOrEmpty(Value)) { @@ -121,7 +132,7 @@ public void AddPredicate(string Field, PAYLOAD_OPERATOR Operator, string Value) case PAYLOAD_OPERATOR.Between: case PAYLOAD_OPERATOR.NotBetween: { - _ = GetBetweenArguments(Value); + _ = Utilities.GetBetweenArguments(Value); break; } // @@ -132,33 +143,6 @@ public void AddPredicate(string Field, PAYLOAD_OPERATOR Operator, string Value) break; } } - - m_Predicates.Add(new PAYLOAD_FILTER_PREDICATE - { - PayloadFieldName = Field, - CompareOp = Operator, - Value = Value, - }); - } - - public static (int,int) GetBetweenArguments(string Value) - { - var loc = Value.IndexOf(","); - if (loc < 0) - { - throw new Exception("Between operator requires two integers " + - "separated by a comma"); - } - var values = Value.Split(','); - if (values.Length != 2) - { - throw new Exception("Between operator requires two integers " + - "separated by a comma"); - } - - var first = Utilities.StringToInteger(values[0]); - var second = Utilities.StringToInteger(values[1]); - return (first, second); } public nint Create() diff --git a/ProviderParser.cs b/ProviderParser.cs index e709a56..0651fe6 100644 --- a/ProviderParser.cs +++ b/ProviderParser.cs @@ -534,6 +534,48 @@ public static class ProviderParser return results; } + public + static + bool + IsManifestKnown(Guid ProviderGuid) + { + var buffer = nint.Zero; + try + { + uint bufferSize = 0; + for (; ; ) + { + var status = TdhEnumerateManifestProviderEvents( + ref ProviderGuid, + buffer, + ref bufferSize); + switch (status) + { + case ERROR_SUCCESS: + case ERROR_INSUFFICIENT_BUFFER: + { + return true; + } + case ERROR_NOT_FOUND: + case ERROR_FILE_NOT_FOUND: + case ERROR_RESOURCE_TYPE_NOT_FOUND: + case ERROR_MUI_FILE_NOT_FOUND: + { + return false; + } + default: + { + return false; + } + } + } + } + catch (Exception) + { + return false; + } + } + private static List diff --git a/README.md b/README.md index 362bafb..f6642b7 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,16 @@ Before diving in, you should understand basic ETW terminology and the mechanics To start a real-time, information-level trace for the Microsoft-Windows-RPC provider, with no keywords: ``` -using (var trace = new RealTimeTrace( - "My ETW Trace", - new Guid("6ad52b32-d609-4be9-ae07-ce8dae937e39"), - EventTraceLevel.Information, - 0xFFFFFFFFFFFFFFFF, - 0)) +using (var trace = new RealTimeTrace("My ETW Trace")) using (var parserBuffers = new EventParserBuffers()) { try { + var provider = trace.AddProvider( + new Guid("6ad52b32-d609-4be9-ae07-ce8dae937e39"), + EventTraceLevel.Information, + 0xFFFFFFFFFFFFFFFF, + 0); trace.Start(); // @@ -199,15 +199,17 @@ See the unit tests defined in `FilterByPackageTests.cs` for details on determini ### Attribute filtering -The simplest form of attribute filtering is to specify a level and keyword when you start the trace session: +The simplest form of attribute filtering is to specify a level and keyword to any provider added to the trace session: ``` -using (var trace = new RealTimeTrace( - "My ETW Trace", - new Guid("70eb4f03-c1de-4f73-a051-33d13d5413bd"), - EventTraceLevel.Information, << LEVEL - 0xFFFFFFFFFFFFFFFF, << ANY KEYWORD - 0)) << ALL KEYWORD +using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) +using (var parserBuffers = new EventParserBuffers()) +{ + try + { + var provider = trace.AddProvider(s_RpcEtwGuid, Level, 0xFFFFFFFFFFFFFFFF, 0); + trace.Start(); +... ``` This is the most powerful form of filtering, as it is extremely efficient and entirely eliminates unnecessary event production. The worst thing you can do when using ETW filtering is to set the level to `Verbose` and either leverage payload filtering or perform your own filtering. You will surely strain the system and cause very noticable impact. By understanding the logging level and keywords of interest for the events you want to analyze, you can easily set this attribute filter and go unnoticed. Be sure to read the [documentation](https://learn.microsoft.com/en-us/windows/win32/api/evntrace/nf-evntrace-enabletraceex2) to properly use the `any`/`all` keyword bitmasks. @@ -216,7 +218,7 @@ The other form of attribute filtering, and also extremely lightweight and effici ``` var eventIds = new List { 5, 7 }; -trace.SetEventIdsFilter(eventIds, Enable); +provider.SetEventIdsFilter(eventIds, Enable); ``` ### Stackwalk filtering @@ -225,13 +227,13 @@ This type of filtering allows you to capture a stack trace of the thread that ca ``` var eventIds = new List { 5 }; -trace.SetStackwalkEventIdsFilter(eventIds, Enable); +provider.SetStackwalkEventIdsFilter(eventIds, Enable); ``` To produce a stackwalk trace for _all_ Microsoft-Windows-Kernel-Registry events by keyword and level (note: you must also include the same keyword/level in the trace session constructor `RealTimeTrace`): ``` -trace.SetStackwalkLevelKw( +provider.SetStackwalkLevelKw( EventTraceLevel.Information, RegistryProviderKeywords.CreateKey | RegistryProviderKeywords.QueryKey, 0, @@ -260,7 +262,7 @@ var filters = new List> { new Tuple(payloadFilter, false) }; -trace.AddPayloadFilters(filters); +provider.AddPayloadFilters(filters); ``` To specify that predicate conditions should be OR'd together, pass `true` to the `PayloadFilter` constructor. To require that all predicates match (AND'd together), pass `false`. ETW does not support any more complicated predicate grouping. diff --git a/RealTimeTrace.cs b/RealTimeTrace.cs index 1155753..1b023d4 100644 --- a/RealTimeTrace.cs +++ b/RealTimeTrace.cs @@ -27,40 +27,21 @@ namespace etwlib { using static TraceLogger; - public class RealTimeTrace : IDisposable + public class RealTimeTrace : TraceSession { private readonly string m_SessionName; private readonly Guid m_SessionGuid; - private bool m_Disposed; private nint m_PropertiesBuffer; - private nint m_ParametersBuffer; - private nint m_FiltersBuffer; - private Guid m_ProviderGuid; private long m_SessionHandle; - private EventTraceLevel m_TraceLevel; - private ulong m_MatchAnyKeyword; - private ulong m_MatchAllKeyword; - private long m_PerfFreq; - private List m_FilterDescriptors; - private nint m_AggregatedPayloadFilters; - public RealTimeTrace( - string SessionName, - Guid Provider, - EventTraceLevel Level, - ulong MatchAnyKeyword, - ulong MatchAllKeyword) + public RealTimeTrace(string SessionName) { m_SessionName = SessionName; m_SessionGuid = Guid.NewGuid(); - m_Disposed = false; m_PropertiesBuffer = nint.Zero; - m_ProviderGuid = Provider; m_SessionHandle = 0; - m_TraceLevel = Level; - m_MatchAnyKeyword = MatchAnyKeyword; - m_MatchAllKeyword = MatchAllKeyword; - m_FilterDescriptors = new List(); + m_LogFile.LoggerName = m_SessionName; + m_LogFile.ProcessTraceMode = ProcessTraceMode.EventRecord | ProcessTraceMode.RealTime; } ~RealTimeTrace() @@ -68,7 +49,7 @@ public RealTimeTrace( Dispose(false); } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { if (m_Disposed) { @@ -79,22 +60,19 @@ protected virtual void Dispose(bool disposing) TraceEventType.Information, "Disposing RealTimeTrace"); - m_Disposed = true; - if (m_SessionHandle != 0 && m_SessionHandle != -1) { - var result = EnableTraceEx2(m_SessionHandle, - ref m_ProviderGuid, - EventControlCode.DisableProvider, - m_TraceLevel, - 0, 0, 0, - nint.Zero); - if (result != ERROR_SUCCESS) + uint result; + foreach (var provider in m_EnabledProviders) { - Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Error, - $"RealTimeTrace dispose could not disable provider: " + - $"{result:X}"); + result = provider.Disable(m_SessionHandle); + if (result != ERROR_SUCCESS) + { + Trace(TraceLoggerType.RealTimeTrace, + TraceEventType.Error, + $"RealTimeTrace dispose could not disable provider: " + + $"{result:X}"); + } } result = ControlTrace( m_SessionHandle, @@ -115,302 +93,14 @@ protected virtual void Dispose(bool disposing) Marshal.FreeHGlobal(m_PropertiesBuffer); } - // - // Release filter descriptor buffers - // - foreach (var filter in m_FilterDescriptors) - { - if (filter.Type == EventFilterTypePayload) - { - // - // This one is released further down by ETW. - // - continue; - } - Marshal.FreeHGlobal(filter.Ptr); - } - - // - // Release array wrapper for filter descriptors - // - if (m_FiltersBuffer != nint.Zero) - { - Marshal.FreeHGlobal(m_FiltersBuffer); - } - - // - // Release containing parameters buffer - // - if (m_ParametersBuffer != nint.Zero) - { - Marshal.FreeHGlobal(m_ParametersBuffer); - } - - // - // Release any aggregated TDH payload filters - // - if (m_AggregatedPayloadFilters != nint.Zero) - { - var result = TdhCleanupPayloadEventFilterDescriptor( - m_AggregatedPayloadFilters); - Debug.Assert(result == ERROR_SUCCESS); - } - - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public void SetProcessFilter(List ProcessIds) - { - if (ProcessIds.Count == 0) - { - throw new Exception("Event ID list required."); - } - - if (ProcessIds.Count > MaxEventFilterPidCount) - { - throw new Exception($"Maximum {MaxEventFilterPidCount} filtered processes"); - } - - foreach (var desc in m_FilterDescriptors) - { - if (desc.Type == EventFilterTypePid) - { - throw new Exception("PID filter can only be used once per session."); - } - } - - var descriptor = new EVENT_FILTER_DESCRIPTOR(); - var size = ProcessIds.Count * sizeof(int); - descriptor.Type = EventFilterTypePid; - descriptor.Size = (uint)size; - descriptor.Ptr = Marshal.AllocHGlobal(size); - if (descriptor.Ptr == nint.Zero) - { - throw new Exception("Out of memory"); - } - Marshal.Copy(ProcessIds.ToArray(), 0, descriptor.Ptr, ProcessIds.Count); - m_FilterDescriptors.Add(descriptor); - } - - public void SetEventIdsFilter(List EventIds, bool Enable) - { - if (EventIds.Count == 0) - { - throw new Exception("Event ID list required."); - } - - if (EventIds.Count > MaxEventFilterEventIdCount) - { - throw new Exception($"Maximum {MaxEventFilterEventIdCount} event IDs"); - } - - foreach (var desc in m_FilterDescriptors) - { - if (desc.Type == EventFilterTypeEventId) - { - throw new Exception("Event ID filter can only be used once per session."); - } - } - - AddEventIdFilter(EventFilterTypeEventId, EventIds, Enable); - } - - public void SetStackwalkEventIdsFilter(List EventIds, bool Enable) - { - if (EventIds.Count == 0) - { - throw new Exception("Event ID list required."); - } - - if (EventIds.Count > MaxEventFilterEventIdCount) - { - throw new Exception($"Maximum {MaxEventFilterEventIdCount} stackwalk event IDs"); - } - - foreach (var desc in m_FilterDescriptors) - { - if (desc.Type == EventFilterTypeStackwalk) - { - throw new Exception("Stackwalk filter can only be used once per session."); - } - } - - AddEventIdFilter(EventFilterTypeStackwalk, EventIds, Enable); - } - - public void AddPayloadFilters(List> Filters) - { - if (Filters.Count == 0) - { - throw new Exception("At least one payload filter is required."); - } - - foreach (var desc in m_FilterDescriptors) - { - if (desc.Type == EventFilterTypePayload) - { - throw new Exception("Payload filter can only be used once per session."); - } - } - - var filters = new nint[Filters.Count]; - var index = 0; - - foreach (var entry in Filters) - { - var filter = entry.Item1.Create(); - Debug.Assert(filter != nint.Zero); - filters[index++] = filter; - } - - var matchAllFlags = Filters.ConvertAll(f => Convert.ToByte(f.Item2)).ToArray(); - var eventFilterDescriptor = Marshal.AllocHGlobal( - Marshal.SizeOf(typeof(EVENT_FILTER_DESCRIPTOR))); - if (eventFilterDescriptor == nint.Zero) - { - throw new Exception("Out of memory"); - } - var result = TdhAggregatePayloadFilters( - (uint)Filters.Count, - filters, - matchAllFlags, - eventFilterDescriptor); - if (result != ERROR_SUCCESS) - { - throw new Exception($"TdhAggregatePayloadFilters failed: 0x{result:X}"); - } - m_AggregatedPayloadFilters = eventFilterDescriptor; - } - - public void SetFilteredExeName(string ExeName) - { - // - // Note: the ExeName string can contain multiple executable names separated - // by semi-colons. - // - var length = (ExeName.Length + 1) * 2; - if (length > MaxEventFilterDataSize) - { - throw new Exception("Exe name too long"); - } - - foreach (var desc in m_FilterDescriptors) - { - if (desc.Type == EventFilterTypeExecutableName) - { - throw new Exception("Exe filter can only be used once per session."); - } - } - - var descriptor = new EVENT_FILTER_DESCRIPTOR(); - descriptor.Type = EventFilterTypeExecutableName; - descriptor.Size = (uint)length; - descriptor.Ptr = Marshal.StringToHGlobalUni(ExeName); - if (descriptor.Ptr == nint.Zero) - { - throw new Exception("Out of memory"); - } - m_FilterDescriptors.Add(descriptor); - } - - public void SetFilteredPackageAppId(string AppId) - { - // - // Note: the AppId string can contain multiple MS Store App IDs separated - // by semi-colons. - // - if (AppId.Length * 2 > MaxEventFilterDataSize) - { - throw new Exception("AppId too long"); - } - - foreach (var desc in m_FilterDescriptors) - { - if (desc.Type == EventFilterTypePackageAppId) - { - throw new Exception("App ID filter can only be used once per session."); - } - } - - var descriptor = new EVENT_FILTER_DESCRIPTOR(); - descriptor.Type = EventFilterTypePackageAppId; - descriptor.Size = (uint)AppId.Length * 2; - descriptor.Ptr = Marshal.StringToHGlobalUni(AppId); - if (descriptor.Ptr == nint.Zero) - { - throw new Exception("Out of memory"); - } - m_FilterDescriptors.Add(descriptor); - } - - public void SetFilteredPackageId(string PackageId) - { - // - // Note: the PackageId string can contain multiple MS Store package IDs separated - // by semi-colons. - // - if (PackageId.Length * 2 > MaxEventFilterDataSize) - { - throw new Exception("PackageId too long"); - } - - foreach (var desc in m_FilterDescriptors) - { - if (desc.Type == EventFilterTypePackageId) - { - throw new Exception("Package ID filter can only be used once per session."); - } - } - - var descriptor = new EVENT_FILTER_DESCRIPTOR(); - descriptor.Type = EventFilterTypePackageId; - descriptor.Size = (uint)PackageId.Length * 2; - descriptor.Ptr = Marshal.StringToHGlobalUni(PackageId); - if (descriptor.Ptr == nint.Zero) - { - throw new Exception("Out of memory"); - } - m_FilterDescriptors.Add(descriptor); - } - - public void SetStackwalkLevelKw(EventTraceLevel Level, ulong MatchAnyKeyword, ulong MatchAllKeyword, bool FilterIn) - { - foreach (var desc in m_FilterDescriptors) - { - if (desc.Type == EventFilterTypeStackWalkLevelKw) - { - throw new Exception("Stackwalk Level/KW filter can only be used once per session."); - } - } - - var descriptor = new EVENT_FILTER_DESCRIPTOR(); - descriptor.Type = EventFilterTypeStackWalkLevelKw; - descriptor.Size = (uint)Marshal.SizeOf(typeof(EVENT_FILTER_LEVEL_KW)); - descriptor.Ptr = Marshal.AllocHGlobal((int)descriptor.Size); - if (descriptor.Ptr == nint.Zero) - { - throw new Exception("Out of memory"); - } - - var filter = new EVENT_FILTER_LEVEL_KW(); - filter.Level = Level; - filter.MatchAllKeyword = MatchAllKeyword; - filter.MatchAnyKeyword = MatchAnyKeyword; - filter.FilterIn = FilterIn; - Marshal.StructureToPtr(filter, descriptor.Ptr, false); - m_FilterDescriptors.Add(descriptor); + base.Dispose(disposing); } - public void Start() + public override void Start() { Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Information, - "Starting RealTimeTrace " + m_SessionName + "..."); + TraceEventType.Information, + $"Starting RealTimeTrace {m_SessionName}..."); // // Current user must be in "Performance Log Users" group to enable a provider @@ -436,9 +126,9 @@ public void Start() if (status == ERROR_ALREADY_EXISTS) { Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Warning, - $"A trace is already opened with instance " + - $"name {m_SessionName}, attempting to stop it."); + TraceEventType.Warning, + $"A trace is already opened with instance " + + $"name {m_SessionName}, attempting to stop it."); // // Orphaned session, possibly from a crash. Try to stop it. // @@ -451,13 +141,13 @@ public void Start() { var error = $"Unable to stop orphaned trace session: {status:X}"; Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Error, - error); + TraceEventType.Error, + error); throw new Exception(error); } Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Information, - "Prior trace session stopped."); + TraceEventType.Information, + "Prior trace session stopped."); continue; } else if (status != ERROR_SUCCESS || m_SessionHandle == 0 || m_SessionHandle == -1) @@ -465,188 +155,32 @@ public void Start() m_SessionHandle = 0; var error = $"StartTrace() failed: 0x{status:X}"; Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Error, - error); + TraceEventType.Error, + error); throw new Exception(error); } break; } Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Information, - "Trace started. Enabling provider..."); - GenerateTraceParameters(); - status = EnableTraceEx2( - m_SessionHandle, - ref m_ProviderGuid, - EventControlCode.EnableProvider, - m_TraceLevel, - m_MatchAnyKeyword, - m_MatchAllKeyword, - 0xffffffff, - m_ParametersBuffer); - if (status != ERROR_SUCCESS) + TraceEventType.Information, + "Trace started. Enabling provider..."); + foreach (var provider in m_EnabledProviders) { - var error = $"EnableTraceEx2() failed: 0x{status:X}"; - Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Error, - error); - throw new Exception(error); - } - - Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Information, - "Provider enabled successfully."); - } - - public void Consume( - EventRecordCallback EventCallback, - BufferCallback BufferCallback - ) - { - var logfile = new EVENT_TRACE_LOGFILE() - { - EventCallback = EventCallback, - BufferCallback = BufferCallback, - LoggerName = m_SessionName, - ProcessTraceMode = ProcessTraceMode.EventRecord | - ProcessTraceMode.RealTime - }; - - var logFilePointer = Marshal.AllocHGlobal(Marshal.SizeOf(logfile)); - if (logFilePointer == nint.Zero) - { - throw new Exception("Out of memory"); - } - Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Information, - "Consuming events..."); - Marshal.StructureToPtr(logfile, logFilePointer, false); - var handle = OpenTrace(logFilePointer); - // - // Marshal the structure back so we can get the PerfFreq - // - logfile = (EVENT_TRACE_LOGFILE)Marshal.PtrToStructure( - logFilePointer, typeof(EVENT_TRACE_LOGFILE))!; - Marshal.FreeHGlobal(logFilePointer); - logFilePointer = nint.Zero; - if (handle == -1 || handle == 0) - { - var error = $"OpenTrace() returned an invalid handle: 0x" + - $"{Marshal.GetLastWin32Error():X}"; - Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Error, - error); - throw new Exception(error); - } - - Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Information, - "Trace session successfully opened, processing trace.."); - - try - { - // - // Update PerfFreq so event's timestamps can be parsed. - // - m_PerfFreq = logfile.LogfileHeader.PerfFreq.QuadPart; - - // - // Blocking call. The caller's BufferCallback must return false to - // unblock this routine. - // - var status = ProcessTrace( - new long[1] { handle }, - 1, - nint.Zero, - nint.Zero); + status = provider.Enable(m_SessionHandle); if (status != ERROR_SUCCESS) { - var error = $"ProcessTrace() failed: 0x{status:X}" + - $", GetLastError: {Marshal.GetLastWin32Error():X}"; + var error = $"EnableTraceEx2() failed for {provider}: 0x{status:X}"; Trace(TraceLoggerType.RealTimeTrace, TraceEventType.Error, error); throw new Exception(error); } - Trace(TraceLoggerType.RealTimeTrace, - TraceEventType.Information, - "Trace processing successfully completed."); - } - finally - { - CloseTrace(handle); - } - } - - public - long - GetPerfFreq() - { - return m_PerfFreq; - } - private - void - GenerateTraceParameters() - { - var parameters = new ENABLE_TRACE_PARAMETERS - { - Version = 2, - EnableProperty = EnableTraceProperties.ProcessStartKey | - EnableTraceProperties.Sid - }; - - var numFilters = m_FilterDescriptors.Count; - if (m_AggregatedPayloadFilters != nint.Zero) - { - numFilters++; - } - - if (numFilters > 0) - { - Debug.Assert(numFilters < MaxEventFiltersCount); - - // - // EnableTraceEx2 expects an array of EVENT_FILTER_DESCRIPTOR - // - var size = Marshal.SizeOf(typeof(EVENT_FILTER_DESCRIPTOR)); - m_FiltersBuffer = Marshal.AllocHGlobal(numFilters * size); - if (m_FiltersBuffer == nint.Zero) - { - throw new Exception("Out of memory"); - } - nint pointer = m_FiltersBuffer; - foreach (var desc in m_FilterDescriptors) - { - Marshal.StructureToPtr(desc, pointer, false); - pointer = nint.Add(pointer, size); - } - if (m_AggregatedPayloadFilters != nint.Zero) - { - var payloadFilter = (EVENT_FILTER_DESCRIPTOR)Marshal.PtrToStructure( - m_AggregatedPayloadFilters, typeof(EVENT_FILTER_DESCRIPTOR))!; - Debug.Assert(payloadFilter.Type == EventFilterTypePayload); - Debug.Assert(payloadFilter.Ptr != nint.Zero); - Marshal.StructureToPtr(payloadFilter, pointer, false); - pointer = nint.Add(pointer, size); - } - parameters.FilterDescCount = (uint)numFilters; - parameters.EnableFilterDesc = m_FiltersBuffer; - - if (m_FilterDescriptors.Any( - f => f.Type == EventFilterTypeStackwalk || - f.Type == EventFilterTypeStackWalkLevelKw)) - { - // - // Note: events over 64kb will be dropped by etw with this set. - // - parameters.EnableProperty |= EnableTraceProperties.StackTrace; - } + Trace(TraceLoggerType.RealTimeTrace, + TraceEventType.Information, + $"Provider {provider} enabled successfully."); } - - m_ParametersBuffer = Marshal.AllocHGlobal(Marshal.SizeOf(parameters)); - Marshal.StructureToPtr(parameters, m_ParametersBuffer, false); } private @@ -678,33 +212,5 @@ BufferCallback BufferCallback Marshal.Copy(loggerName, 0, dest, loggerName.Length); m_PropertiesBuffer = buffer; } - - private - void - AddEventIdFilter(uint FilterType, List EventIds, bool Enable) - { - var descriptor = new EVENT_FILTER_DESCRIPTOR(); - descriptor.Type = FilterType; - var size = Marshal.SizeOf(typeof(EVENT_FILTER_EVENT_ID)) + - ((EventIds.Count - 1) * Marshal.SizeOf(typeof(ushort))); - descriptor.Size = (uint)size; - descriptor.Ptr = Marshal.AllocHGlobal(size); - if (descriptor.Ptr == nint.Zero) - { - throw new Exception("Out of memory"); - } - var filter = new EVENT_FILTER_EVENT_ID(); - filter.FilterIn = Enable; - filter.Reserved = 0; - filter.Count = (ushort)EventIds.Count; - Marshal.StructureToPtr(filter, descriptor.Ptr, false); - var dest = nint.Add(descriptor.Ptr,(int)Marshal.OffsetOf("Events")); - var ids = EventIds.ConvertAll(id => (ushort)id).ToArray(); - var byteSize = ids.Length * Marshal.SizeOf(typeof(ushort)); - var bytes = new byte[byteSize]; - Buffer.BlockCopy(ids, 0, bytes, 0, byteSize); - Marshal.Copy(bytes, 0, dest, byteSize); - m_FilterDescriptors.Add(descriptor); - } } } diff --git a/SessionParser.cs b/SessionParser.cs new file mode 100644 index 0000000..d4a7d59 --- /dev/null +++ b/SessionParser.cs @@ -0,0 +1,253 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Xml.Linq; + +namespace etwlib +{ + using static TraceLogger; + using static NativeTraceConsumer; + using static NativeTraceControl; + + public static class SessionParser + { + public + static + List + GetSessions() + { + var results = new List(); + nint buffer = nint.Zero; + + try + { + uint result = 0; + uint bufferSize = 0; + uint returnLength = 0; + + for (; ; ) + { + result = EnumerateTraceGuidsEx( + TRACE_QUERY_INFO_CLASS.TraceGuidQueryList, + nint.Zero, + 0, + buffer, + bufferSize, + ref returnLength); + if (result == ERROR_SUCCESS) + { + break; + } + else if (result != ERROR_INSUFFICIENT_BUFFER) + { + var error = $"EnumerateTraceGuidsEx failed: 0x{result:X}"; + Trace(TraceLoggerType.EtwSessionParser, + TraceEventType.Warning, + error); + throw new Exception(error); + } + + buffer = Marshal.AllocHGlobal((int)returnLength); + bufferSize = returnLength; + if (buffer == nint.Zero) + { + throw new Exception("Out of memory"); + } + } + + if (buffer == nint.Zero || bufferSize == 0) + { + throw new Exception("EnumerateTraceGuidsEx returned null " + + " or empty buffer."); + } + + int numProviders = (int)bufferSize / Marshal.SizeOf(typeof(Guid)); + var pointer = buffer; + + for (int i = 0; i < numProviders; i++) + { + var guid = (Guid)Marshal.PtrToStructure(pointer, typeof(Guid))!; + var session = GetSessions(guid); + if (session == null) + { + continue; + } + results.AddRange(session); + pointer = nint.Add(pointer, Marshal.SizeOf(typeof(Guid))); + } + + results.Sort(); + return results; + } + catch (Exception ex) + { + Trace(TraceLoggerType.EtwProviderParser, + TraceEventType.Error, + $"Exception in GetProviders(): {ex.Message}"); + throw; + } + finally + { + if (buffer != nint.Zero) + { + Marshal.FreeHGlobal(buffer); + } + } + } + + public static List? GetSessions(string ProviderName) + { + var provider = ProviderParser.GetProvider(ProviderName); + if (provider == null) + { + throw new Exception($"Provider {ProviderName} not found"); + } + return GetSessions(provider.Id); + } + + public static List? GetSessions(Guid ProviderId) + { + var inBufferSize = (uint)Marshal.SizeOf(typeof(Guid)); + var inBuffer = Marshal.AllocHGlobal((int)inBufferSize); + if (inBuffer == nint.Zero) + { + throw new Exception("Out of memory"); + } + + Marshal.StructureToPtr(ProviderId, inBuffer, false); + + var sessions = new List(); + var outBuffer = nint.Zero; + uint outBufferSize = 0; + uint returnLength = 0; + + try + { + for ( ; ; ) + { + var result = EnumerateTraceGuidsEx( + TRACE_QUERY_INFO_CLASS.TraceGuidQueryInfo, + inBuffer, + inBufferSize, + outBuffer, + outBufferSize, + ref returnLength); + if (result == ERROR_SUCCESS) + { + break; + } + else if (result == ERROR_WMI_GUID_NOT_FOUND) + { + // + // This can occur if the GUID is registered but not loaded + // + return null; + } + else if (result != ERROR_INSUFFICIENT_BUFFER) + { + var error = $"EnumerateTraceGuidsEx failed: 0x{result:X}"; + Trace(TraceLoggerType.EtwSessionParser, + TraceEventType.Error, + error); + throw new Exception(error); + } + + outBuffer = Marshal.AllocHGlobal((int)returnLength); + outBufferSize = returnLength; + if (outBuffer == nint.Zero) + { + throw new Exception("Out of memory"); + } + } + + if (outBuffer == nint.Zero || outBufferSize == 0) + { + throw new Exception("EnumerateTraceGuidsEx returned null " + + " or empty buffer."); + } + + var pointer = outBuffer; + var info = (TRACE_GUID_INFO)Marshal.PtrToStructure( + pointer, typeof(TRACE_GUID_INFO))!; + pointer = nint.Add(pointer, Marshal.SizeOf(typeof(TRACE_GUID_INFO))); + + // + // NB: there can be multiple instances of a provider with the same + // GUID if they're hosted in a DLL loaded in multiple processes. + // + for (int i = 0; i < info.InstanceCount; i++) + { + var instance = (TRACE_PROVIDER_INSTANCE_INFO)Marshal.PtrToStructure( + pointer, typeof(TRACE_PROVIDER_INSTANCE_INFO))!; + if (instance.EnableCount > 0) + { + var sessionPointer = pointer; + for (int j = 0; j < instance.EnableCount; j++) + { + var sessionInfo = (TRACE_ENABLE_INFO)Marshal.PtrToStructure( + sessionPointer, typeof(TRACE_ENABLE_INFO))!; + var enabledProvider = new SessionEnabledProvider( + ProviderId, + instance.Pid, + instance.Flags, + sessionInfo.Level, + sessionInfo.EnableProperty, + sessionInfo.MatchAnyKeyword, + sessionInfo.MatchAllKeyword); + var session = new ParsedEtwSession(sessionInfo.LoggerId); + if (!sessions.Contains(session)) + { + sessions.Add(session); + } + else + { + session = sessions.FirstOrDefault(s => s == session); + } + session!.EnabledProviders.Add(enabledProvider); + sessionPointer = nint.Add(sessionPointer, + Marshal.SizeOf(typeof(TRACE_ENABLE_INFO))); + } + } + pointer = nint.Add(pointer, (int)instance.NextOffset); + } + return sessions; + } + catch (Exception ex) + { + Trace(TraceLoggerType.EtwSessionParser, + TraceEventType.Error, + $"Exception in GetSessionsForProvider(): {ex.Message}"); + throw; + } + finally + { + if (outBuffer != nint.Zero) + { + Marshal.FreeHGlobal(outBuffer); + } + if (inBuffer != nint.Zero) + { + Marshal.FreeHGlobal(inBuffer); + } + } + } + } +} diff --git a/TraceLogger.cs b/TraceLogger.cs index 25da590..225c996 100644 --- a/TraceLogger.cs +++ b/TraceLogger.cs @@ -22,30 +22,30 @@ namespace etwlib { public static class TraceLogger { - private static readonly string m_TraceFileDir = Path.Combine( - new string[] { Path.GetTempPath(), "etwlib", "Logs" }); - private static string m_Location = Path.Combine(new string[] { m_TraceFileDir, - DateTime.Now.ToString("yyyy-MM-dd-HHmmss") + - ".txt" }); + private static MemoryStream m_BaseStream = new MemoryStream(); private static TextWriterTraceListener m_TraceListener = - new TextWriterTraceListener(m_Location, "etwlibListener"); + new TextWriterTraceListener(m_BaseStream, "etwlibListener"); private static SourceSwitch m_Switch = new SourceSwitch("etwlibListener", "Verbose"); private static TraceSource[] Sources = { + new TraceSource("TraceSession", SourceLevels.Verbose), new TraceSource("RealTimeTrace", SourceLevels.Verbose), new TraceSource("FileTrace", SourceLevels.Verbose), new TraceSource("EventParser", SourceLevels.Verbose), new TraceSource("ProviderParser", SourceLevels.Verbose), new TraceSource("ManifestParser", SourceLevels.Verbose), + new TraceSource("SessionParser", SourceLevels.Verbose), }; public enum TraceLoggerType { + TraceSession, RealTimeTrace, FileTrace, EtwEventParser, EtwProviderParser, EtwManifestParser, + EtwSessionParser, Max } @@ -72,5 +72,10 @@ public static void Trace(TraceLoggerType Type, TraceEventType EventType, string } Sources[(int)Type].TraceEvent(EventType, 1, Message); } + + public static MemoryStream GetStream() + { + return m_BaseStream; + } } } diff --git a/TraceSession.cs b/TraceSession.cs new file mode 100644 index 0000000..21dd6e9 --- /dev/null +++ b/TraceSession.cs @@ -0,0 +1,145 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +using static etwlib.NativeTraceConsumer; +using static etwlib.NativeTraceControl; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace etwlib +{ + using static TraceLogger; + + public abstract class TraceSession : IDisposable + { + protected EVENT_TRACE_LOGFILE m_LogFile; + protected bool m_Disposed; + protected long m_PerfFreq; + protected List m_EnabledProviders; + + public TraceSession() + { + m_LogFile = new EVENT_TRACE_LOGFILE(); + m_Disposed = false; + m_PerfFreq = 0; + m_EnabledProviders = new List(); + } + + protected virtual void Dispose(bool disposing) + { + Trace(TraceLoggerType.TraceSession, + TraceEventType.Information, + "Disposing FileTrace"); + if (m_Disposed) + { + return; + } + m_Disposed = true; + } + + public virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public EnabledProvider AddProvider(Guid Id, EventTraceLevel traceLevel, ulong matchAnyKeyword, ulong matchAllKeyword) + { + var provider = new EnabledProvider(Id, traceLevel, matchAllKeyword, matchAnyKeyword); + m_EnabledProviders.Add(provider); + return provider; + } + + public void AddProvider(EnabledProvider Provider) + { + m_EnabledProviders.Add(Provider); + } + + public abstract void Start(); + + public void Consume( + EventRecordCallback EventCallback, + BufferCallback BufferCallback + ) + { + m_LogFile.BufferCallback = BufferCallback; + m_LogFile.EventCallback = EventCallback; + var logFilePointer = Marshal.AllocHGlobal(Marshal.SizeOf(m_LogFile)); + Marshal.StructureToPtr(m_LogFile, logFilePointer, false); + var handle = OpenTrace(logFilePointer); + // + // Marshal the structure back so we can get the PerfFreq + // + var logfile = (EVENT_TRACE_LOGFILE)Marshal.PtrToStructure( + logFilePointer, typeof(EVENT_TRACE_LOGFILE))!; + Marshal.FreeHGlobal(logFilePointer); + if (handle == -1 || handle == 0) + { + var error = "OpenTrace() returned an invalid handle: 0x" + + Marshal.GetLastWin32Error().ToString("X"); + Trace(TraceLoggerType.TraceSession, + TraceEventType.Error, + error); + throw new Exception(error); + } + + Trace(TraceLoggerType.TraceSession, + TraceEventType.Information, + "Trace session successfully opened, processing trace.."); + + try + { + // + // Update PerfFreq so event's timestamps can be parsed. + // + m_PerfFreq = logfile.LogfileHeader.PerfFreq.QuadPart; + + // + // Blocking call. The caller's BufferCallback must return false to + // unblock this routine. + // + var status = ProcessTrace( + new long[1] { handle }, + 1, + nint.Zero, + nint.Zero); + if (status != ERROR_SUCCESS) + { + var error = "ProcessTrace() failed: 0x" + status.ToString("X") + + ", GetLastError: " + Marshal.GetLastWin32Error().ToString("X"); + Trace(TraceLoggerType.TraceSession, + TraceEventType.Error, + error); + throw new Exception(error); + } + Trace(TraceLoggerType.TraceSession, + TraceEventType.Information, + "Trace processing successfully completed."); + } + finally + { + CloseTrace(handle); + } + } + + public long GetPerfFreq() + { + return m_PerfFreq; + } + } +} diff --git a/UnitTests/FileTraceTests.cs b/UnitTests/FileTraceTests.cs index dfb1d51..ac179da 100644 --- a/UnitTests/FileTraceTests.cs +++ b/UnitTests/FileTraceTests.cs @@ -56,6 +56,8 @@ public void Basic() { try { + trace.Start(); + // // Begin consuming events. This is a blocking call. // diff --git a/UnitTests/FilterByEventIdTests.cs b/UnitTests/FilterByEventIdTests.cs index eaa6557..35890a5 100644 --- a/UnitTests/FilterByEventIdTests.cs +++ b/UnitTests/FilterByEventIdTests.cs @@ -47,25 +47,23 @@ public void Basic(bool Enable, bool Stackwalk) // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_RpcEtwGuid, - EventTraceLevel.Information, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try { + var provider = trace.AddProvider( + s_RpcEtwGuid, EventTraceLevel.Information, 0xFFFFFFFFFFFFFFFF, 0); + // // We'll use RpcClientCallStart_V1 and RpcClientCallStop7_V1 // var eventIds = new List { 5, 7 }; - trace.SetEventIdsFilter(eventIds, Enable); + provider.SetEventIdsFilter(eventIds, Enable); if (Stackwalk) { var eventIds2 = new List { 5 }; - trace.SetStackwalkEventIdsFilter(eventIds2, Enable); + provider.SetStackwalkEventIdsFilter(eventIds2, Enable); } trace.Start(); diff --git a/UnitTests/FilterByExeNameTests.cs b/UnitTests/FilterByExeNameTests.cs index cd9d9cd..6272630 100644 --- a/UnitTests/FilterByExeNameTests.cs +++ b/UnitTests/FilterByExeNameTests.cs @@ -46,17 +46,14 @@ public void Basic(string ExeName) // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_RpcEtwGuid, - EventTraceLevel.Information, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try { - trace.SetFilteredExeName(ExeName); + var provider = trace.AddProvider( + s_RpcEtwGuid, EventTraceLevel.Information, 0xFFFFFFFFFFFFFFFF, 0); + provider.SetFilteredExeName(ExeName); trace.Start(); // diff --git a/UnitTests/FilterByPackageTests.cs b/UnitTests/FilterByPackageTests.cs index f27b15a..166179b 100644 --- a/UnitTests/FilterByPackageTests.cs +++ b/UnitTests/FilterByPackageTests.cs @@ -24,7 +24,6 @@ under the License. using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using static etwlib.NativeTraceConsumer; using static etwlib.NativeTraceControl; using static UnitTests.Shared; @@ -34,26 +33,6 @@ namespace UnitTests [TestClass] public class FilterByPackageTests { - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - static extern int GetPackageFullName( - [In] nint hProcess, - [In, Out] ref uint packageFullNameLength, - [Out] StringBuilder fullName); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - static extern int GetApplicationUserModelId( - [In] nint hProcess, - [In, Out] ref uint applicationUserModelIdLength, - [Out] StringBuilder applicationUserModelId); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - static extern int ParseApplicationUserModelId( - [In] string applicationUserModelId, - [In, Out] ref uint packageFamilyNameLength, - [In] StringBuilder packageFamilyName, - [In, Out] ref uint packageRelativeApplicationIdLength, - [Out] StringBuilder packageRelativeApplicationId); - [DataTestMethod] [DataRow(true, 5)] [DataRow(false, 5)] @@ -87,7 +66,7 @@ public void Basic(bool TestPackageId, int Attempts) { continue; // probably the process died } - var package = GetPackage(process.Handle); + var package = MSStoreAppPackageHelper.GetPackage(process.Handle); if (package == null) { continue; @@ -114,52 +93,6 @@ public void Basic(bool TestPackageId, int Attempts) Assert.Fail(); } - private Tuple? GetPackage(nint ProcessHandle) - { - uint bufferLength = 1024; - - // - // The package full name is a serialized form of the Package ID, which - // consists of identifying info like: name, publisher, architecture, etc. - // This is what the ETW filter type EVENT_FILTER_TYPE_PACKAGE_ID wants. - // - var packageId = new StringBuilder((int)bufferLength); - var result = GetPackageFullName(ProcessHandle, ref bufferLength, packageId); - if (result != ERROR_SUCCESS) - { - return null; - } - - var userModelId = new StringBuilder((int)bufferLength); - result = GetApplicationUserModelId(ProcessHandle, ref bufferLength, userModelId); - if (result != ERROR_SUCCESS) - { - return null; - } - - // - // The ETW filter type EVENT_FILTER_TYPE_PACKAGE_APP_ID wants the - // "package-relative APP ID (PRAID)" which must be parsed from - // the "application user model ID". - // - var packageFamilyLength = bufferLength; - var packageFamily = new StringBuilder((int)packageFamilyLength); - var relativeAppIdLength = bufferLength; - var relativeAppId = new StringBuilder((int)relativeAppIdLength); - result = ParseApplicationUserModelId(userModelId.ToString(), - ref packageFamilyLength, - packageFamily, - ref relativeAppIdLength, - relativeAppId); - if (result != ERROR_SUCCESS) - { - return null; - } - - return new Tuple( - packageId.ToString(), relativeAppId.ToString()); - } - private bool TryWindowsStoreApp(string PackageId, string AppId, bool TestPackageId) { int eventsConsumed = 0; @@ -171,25 +104,22 @@ private bool TryWindowsStoreApp(string PackageId, string AppId, bool TestPackage // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_LoggingChannel, - EventTraceLevel.Verbose, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try { + var provider = trace.AddProvider( + s_LoggingChannel, EventTraceLevel.Verbose, 0xFFFFFFFFFFFFFFFF, 0); if (TestPackageId) { Assert.IsNotNull(PackageId); - trace.SetFilteredPackageId(PackageId); + provider.SetFilteredPackageId(PackageId); } else { Assert.IsNotNull(AppId); - trace.SetFilteredPackageAppId(AppId); + provider.SetFilteredPackageAppId(AppId); } trace.Start(); stopwatch.Start(); @@ -242,7 +172,7 @@ private bool TryWindowsStoreApp(string PackageId, string AppId, bool TestPackage try { var process = Process.GetProcessById((int)parsedEvent.ProcessId); - var package = GetPackage(process.Handle); + var package = MSStoreAppPackageHelper.GetPackage(process.Handle); if (package == null) { return; diff --git a/UnitTests/FilterByPayloadTests.cs b/UnitTests/FilterByPayloadTests.cs index 1c22dde..7e2a3d0 100644 --- a/UnitTests/FilterByPayloadTests.cs +++ b/UnitTests/FilterByPayloadTests.cs @@ -54,12 +54,7 @@ public void IntegerEquality( // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_WinKernelRegistryGuid, - EventTraceLevel.Information, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try @@ -73,7 +68,10 @@ public void IntegerEquality( { new Tuple(payloadFilter, false) }; - trace.AddPayloadFilters(filters); + + var provider = trace.AddProvider( + s_WinKernelRegistryGuid, EventTraceLevel.Information, 0xFFFFFFFFFFFFFFFF, 0); + provider.AddPayloadFilters(filters); trace.Start(); // @@ -214,12 +212,7 @@ public void IntegerRange( // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_WinKernelRegistryGuid, - EventTraceLevel.Information, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try @@ -234,7 +227,10 @@ public void IntegerRange( payloadFilter.AddPredicate(FieldName, Operator, Value); filters.Add(new Tuple(payloadFilter, false)); } - trace.AddPayloadFilters(filters); + + var provider = trace.AddProvider( + s_WinKernelRegistryGuid, EventTraceLevel.Information, 0xFFFFFFFFFFFFFFFF, 0); + provider.AddPayloadFilters(filters); trace.Start(); // @@ -284,7 +280,7 @@ public void IntegerRange( Assert.IsNotNull(value); Assert.IsNotNull(value.Value); var fieldValue = Utilities.StringToInteger(value.Value); - var expectedRange = PayloadFilter.GetBetweenArguments(Value); + var expectedRange = Utilities.GetBetweenArguments(Value); var lower = expectedRange.Item1; var upper = expectedRange.Item2; @@ -359,12 +355,7 @@ public void StringEquality( // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_WinKernelRegistryGuid, - EventTraceLevel.Information, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try @@ -379,7 +370,10 @@ public void StringEquality( payloadFilter.AddPredicate(FieldName, Operator, Value); filters.Add(new Tuple(payloadFilter, false)); } - trace.AddPayloadFilters(filters); + + var provider = trace.AddProvider( + s_WinKernelRegistryGuid, EventTraceLevel.Information, 0xFFFFFFFFFFFFFFFF, 0); + provider.AddPayloadFilters(filters); trace.Start(); // @@ -558,12 +552,7 @@ public void MultiplePredicates( // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_WinKernelRegistryGuid, - EventTraceLevel.Information, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try @@ -581,7 +570,10 @@ public void MultiplePredicates( } filters.Add(new Tuple(payloadFilter, false)); } - trace.AddPayloadFilters(filters); + + var provider = trace.AddProvider( + s_WinKernelRegistryGuid, EventTraceLevel.Information, 0xFFFFFFFFFFFFFFFF, 0); + provider.AddPayloadFilters(filters); trace.Start(); // diff --git a/UnitTests/FilterByProcessTests.cs b/UnitTests/FilterByProcessTests.cs index cadf8a5..eb7f52d 100644 --- a/UnitTests/FilterByProcessTests.cs +++ b/UnitTests/FilterByProcessTests.cs @@ -44,12 +44,7 @@ public void Basic(int ProcessCount) // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_RpcEtwGuid, - EventTraceLevel.LogAlways, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try @@ -62,7 +57,9 @@ public void Basic(int ProcessCount) p => p.ProcessName != null && p.ProcessName.Contains("svchost")).Select( p => p.Id).Take(ProcessCount).ToList(); Debug.Assert(targets.Count > 0); - trace.SetProcessFilter(targets); + var provider = trace.AddProvider( + s_RpcEtwGuid, EventTraceLevel.LogAlways, 0xFFFFFFFFFFFFFFFF, 0); + provider.SetProcessFilter(targets); trace.Start(); // diff --git a/UnitTests/FilterByStackwalkTests.cs b/UnitTests/FilterByStackwalkTests.cs index 23f49ce..e0ebae6 100644 --- a/UnitTests/FilterByStackwalkTests.cs +++ b/UnitTests/FilterByStackwalkTests.cs @@ -49,17 +49,14 @@ public void LevelKw( // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_WinKernelRegistryGuid, - Level, - (ulong)MatchAnyKeyword, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try { - trace.SetStackwalkLevelKw(Level, (ulong)MatchAnyKeyword, 0, Enable); + var provider = trace.AddProvider( + s_WinKernelRegistryGuid, Level, (ulong)MatchAnyKeyword, 0); + provider.SetStackwalkLevelKw(Level, (ulong)MatchAnyKeyword, 0, Enable); trace.Start(); // @@ -107,10 +104,9 @@ public void LevelKw( // Keyword checks // Assert.IsNotNull(parsedEvent.Keywords); - Assert.IsTrue(parsedEvent.Keywords.Count > 0); - + var keywords = parsedEvent.Keywords.Split(','); int matchedKeywords = 0; - foreach (var kw in parsedEvent.Keywords) + foreach (var kw in keywords) { if (!Enum.TryParse(kw, true, out var parsed)) { diff --git a/UnitTests/RealTimeTraceTests.cs b/UnitTests/RealTimeTraceTests.cs index 021396a..8937d88 100644 --- a/UnitTests/RealTimeTraceTests.cs +++ b/UnitTests/RealTimeTraceTests.cs @@ -43,16 +43,12 @@ public void Basic(EventTraceLevel Level) // This trace will automatically terminate after a set number // of ETW events have been successfully consumed/parsed. // - using (var trace = new RealTimeTrace( - "Unit Test Real-Time Tracing", - s_RpcEtwGuid, - Level, - 0xFFFFFFFFFFFFFFFF, - 0)) + using (var trace = new RealTimeTrace("Unit Test Real-Time Tracing")) using (var parserBuffers = new EventParserBuffers()) { try { + var provider = trace.AddProvider(s_RpcEtwGuid, Level, 0xFFFFFFFFFFFFFFFF, 0); trace.Start(); // diff --git a/UnitTests/SessionTests.cs b/UnitTests/SessionTests.cs new file mode 100644 index 0000000..7820a13 --- /dev/null +++ b/UnitTests/SessionTests.cs @@ -0,0 +1,98 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using etwlib; +using System; + +namespace UnitTests +{ + using static Shared; + + [TestClass] + public class SessionTests + { + [TestMethod] + public void SessionsByProviderName() + { + ConfigureLoggers(); + + try + { + var sessions = SessionParser.GetSessions(); + Assert.IsNotNull(sessions); + Assert.IsTrue(sessions.Count > 0); + foreach (var session in sessions) + { + var provider = ProviderParser.GetProvider( + session.EnabledProviders[0].ProviderId); + if (provider != null && !string.IsNullOrEmpty(provider.Name)) + { + var results = SessionParser.GetSessions(provider.Name); + Assert.IsNotNull(results); + Assert.IsTrue(session == results[0]); + return; + } + } + Assert.Fail(); + } + catch (Exception ex) + { + Assert.Fail(ex.Message); + } + } + + [TestMethod] + public void SingleProviderById() + { + ConfigureLoggers(); + + try + { + var sessions = SessionParser.GetSessions(); + Assert.IsNotNull(sessions); + Assert.IsTrue(sessions.Count > 0); + var session = sessions[0]; + var results = SessionParser.GetSessions(session.EnabledProviders[0].ProviderId); + Assert.IsNotNull(results); + Assert.IsTrue(session == results[0]); + } + catch (Exception ex) + { + Assert.Fail(ex.Message); + } + } + + [TestMethod] + public void AllSessions() + { + ConfigureLoggers(); + + try + { + var sessions = SessionParser.GetSessions(); + Assert.IsNotNull(sessions); + Assert.IsTrue(sessions.Count > 0); + } + catch (Exception ex) + { + Assert.Fail(ex.Message); + } + } + } +} diff --git a/Utilities.cs b/Utilities.cs index 5b1e608..3455554 100644 --- a/Utilities.cs +++ b/Utilities.cs @@ -16,9 +16,81 @@ software distributed under the License is distributed on an specific language governing permissions and limitations under the License. */ +using System.Runtime.InteropServices; +using System.Text; +using static etwlib.NativeTraceConsumer; namespace etwlib { + public static class MSStoreAppPackageHelper + { + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static extern int GetPackageFullName( + [In] nint hProcess, + [In, Out] ref uint packageFullNameLength, + [Out] StringBuilder fullName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static extern int GetApplicationUserModelId( + [In] nint hProcess, + [In, Out] ref uint applicationUserModelIdLength, + [Out] StringBuilder applicationUserModelId); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static extern int ParseApplicationUserModelId( + [In] string applicationUserModelId, + [In, Out] ref uint packageFamilyNameLength, + [In] StringBuilder packageFamilyName, + [In, Out] ref uint packageRelativeApplicationIdLength, + [Out] StringBuilder packageRelativeApplicationId); + + public static Tuple? GetPackage(nint ProcessHandle) + { + uint bufferLength = 1024; + + // + // The package full name is a serialized form of the Package ID, which + // consists of identifying info like: name, publisher, architecture, etc. + // This is what the ETW filter type EVENT_FILTER_TYPE_PACKAGE_ID wants. + // + var packageId = new StringBuilder((int)bufferLength); + var result = GetPackageFullName(ProcessHandle, ref bufferLength, packageId); + if (result != ERROR_SUCCESS) + { + return null; + } + + var userModelId = new StringBuilder((int)bufferLength); + result = GetApplicationUserModelId(ProcessHandle, ref bufferLength, userModelId); + if (result != ERROR_SUCCESS) + { + return null; + } + + // + // The ETW filter type EVENT_FILTER_TYPE_PACKAGE_APP_ID wants the + // "package-relative APP ID (PRAID)" which must be parsed from + // the "application user model ID". + // + var packageFamilyLength = bufferLength; + var packageFamily = new StringBuilder((int)packageFamilyLength); + var relativeAppIdLength = bufferLength; + var relativeAppId = new StringBuilder((int)relativeAppIdLength); + result = ParseApplicationUserModelId(userModelId.ToString(), + ref packageFamilyLength, + packageFamily, + ref relativeAppIdLength, + relativeAppId); + if (result != ERROR_SUCCESS) + { + return null; + } + + return new Tuple( + packageId.ToString(), relativeAppId.ToString()); + } + } + public static class Utilities { public static int StringToInteger(string Value) @@ -36,5 +108,26 @@ public static int StringToInteger(string Value) return value; } } + + public static (int, int) GetBetweenArguments(string Value) + { + var loc = Value.IndexOf(","); + if (loc < 0) + { + throw new Exception("Between operator requires two integers " + + "separated by a comma"); + } + var values = Value.Split(','); + if (values.Length != 2) + { + throw new Exception("Between operator requires two integers " + + "separated by a comma"); + } + + var first = Utilities.StringToInteger(values[0]); + var second = Utilities.StringToInteger(values[1]); + return (first, second); + } } + }