|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
#if TARGET_WINDOWS
using Internal.Win32;
#endif
namespace System.Diagnostics.Tracing
{
internal enum EventProviderType
{
None = 0,
ETW,
EventPipe
}
internal enum ControllerCommand
{
// Strictly Positive numbers are for provider-specific commands, negative number are for 'shared' commands. 256
// The first 256 negative numbers are reserved for the framework.
Update = 0, // Not used by EventProviderBase.
SendManifest = -1,
Enable = -2,
Disable = -3,
}
/// <summary>
/// Only here because System.Diagnostics.EventProvider needs one more extensibility hook (when it gets a
/// controller callback)
///
/// As of Feb 2023 the current factoring of this type remains a work in progress. Ideally all the ETW specific functionality
/// would be moved to EtwEventProvider and all the common functionality would be moved to EventProviderImpl. At that point this
/// type would no longer need to exist, EventSource would have a direct reference to EventProviderImpl, and EventProviderImpl's
/// WeakReference would point back to EventSource. However for now we still have this intermediate layer:
/// EventSource -- EventProvider -- EventProviderImpl.
///
/// Be careful interpreting code that uses 'ETW' naming.Some of it really is only used for ETW scenarios whereas in other places
/// ETW behavior was adopted as the standard that EventPipe also implements. Ideally the former would be moved to EtwEventProvider
/// and the latter would remove ETW from the naming.
/// </summary>
internal class EventProvider : IDisposable
{
// This is the windows EVENT_DATA_DESCRIPTOR structure. We expose it because this is what
// subclasses of EventProvider use when creating efficient (but unsafe) version of
// EventWrite. We do make it a nested type because we really don't expect anyone to use
// it except subclasses (and then only rarely).
[StructLayout(LayoutKind.Sequential)]
public struct EventData
{
internal ulong Ptr;
internal uint Size;
internal uint Reserved;
}
internal EventProviderImpl _eventProvider; // The implementation of the specific logging mechanism functions.
private string? _providerName; // Control name
private Guid _providerId; // Control Guid
internal bool _disposed; // when true provider has unregistered
[ThreadStatic]
private static WriteEventErrorCode s_returnCode; // The last return code
private const int BasicTypeAllocationBufferSize = 16;
private const int EtwMaxNumberArguments = 128;
private const int EtwAPIMaxRefObjCount = 8;
private const int TraceEventMaximumSize = 65482;
public enum WriteEventErrorCode : int
{
// check mapping to runtime codes
NoError = 0,
NoFreeBuffers = 1,
EventTooBig = 2,
NullInput = 3,
TooManyArgs = 4,
Other = 5,
}
// Because callbacks happen on registration, and we need the callbacks for those setup
// we can't call Register in the constructor.
//
// Note that EventProvider should ONLY be used by EventSource.
internal EventProvider(EventProviderType providerType)
{
_eventProvider = providerType switch
{
#if TARGET_WINDOWS
EventProviderType.ETW => new EtwEventProvider(this),
#endif
#if FEATURE_PERFTRACING
EventProviderType.EventPipe => new EventPipeEventProvider(this),
#endif
_ => new EventProviderImpl(),
};
}
/// <summary>
/// This method registers the provider with the backing tracing mechanism, either ETW or EventPipe.
/// </summary>
internal void Register(Guid id, string name)
{
_providerName = name;
_providerId = id;
_eventProvider.Register(id, name);
}
//
// implement Dispose Pattern to early deregister from ETW insted of waiting for
// the finalizer to call deregistration.
// Once the user is done with the provider it needs to call Close() or Dispose()
// If neither are called the finalizer will unregister the provider anyway
//
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
//
// explicit cleanup is done by calling Dispose with true from
// Dispose() or Close(). The disposing arguement is ignored because there
// are no unmanaged resources.
// The finalizer calls Dispose with false.
//
//
// check if the object has been already disposed
//
if (_disposed)
return;
// Disable the provider.
_eventProvider.Disable();
// Do most of the work under a lock to avoid shutdown race.
lock (EventListener.EventListenersLock)
{
// Double check
if (_disposed)
return;
_disposed = true;
}
// We do the Unregistration outside the EventListenerLock because there is a lock
// inside the ETW routines. This lock is taken before ETW issues commands
// Thus the ETW lock gets taken first and then our EventListenersLock gets taken
// in SendCommand(), and also here. If we called EventUnregister after taking
// the EventListenersLock then the take-lock order is reversed and we can have
// deadlocks in race conditions (dispose racing with an ETW command).
//
// We solve by Unregistering after releasing the EventListenerLock.
Debug.Assert(!Monitor.IsEntered(EventListener.EventListenersLock));
_eventProvider.Unregister();
}
/// <summary>
/// This method deregisters the controlGuid of this class with ETW.
///
/// </summary>
public virtual void Close()
{
Dispose();
}
~EventProvider()
{
Dispose(false);
}
internal virtual void OnControllerCommand(ControllerCommand command, IDictionary<string, string?>? arguments, int sessionId) { }
protected EventLevel Level
{
get => _eventProvider.Level;
set => _eventProvider.Level = value;
}
protected EventKeywords MatchAnyKeyword
{
get => _eventProvider.MatchAnyKeyword;
set => _eventProvider.MatchAnyKeyword = value;
}
protected EventKeywords MatchAllKeyword
{
get => _eventProvider.MatchAllKeyword;
set => _eventProvider.MatchAllKeyword = value;
}
/// <summary>
/// IsEnabled, method used to test if provider is enabled
/// </summary>
public bool IsEnabled()
{
return _eventProvider.IsEnabled();
}
/// <summary>
/// IsEnabled, method used to test if event is enabled
/// </summary>
/// <param name="level">
/// Level to test
/// </param>
/// <param name="keywords">
/// Keyword to test
/// </param>
public bool IsEnabled(byte level, long keywords)
{
return _eventProvider.IsEnabled(level, keywords);
}
public static WriteEventErrorCode GetLastWriteEventError()
{
return s_returnCode;
}
//
// Helper function to set the last error on the thread
//
private static void SetLastError(WriteEventErrorCode error)
{
s_returnCode = error;
}
private static unsafe object? EncodeObject(ref object? data, ref EventData* dataDescriptor, ref byte* dataBuffer, ref uint totalEventSize)
/*++
Routine Description:
This routine is used by WriteEvent to unbox the object type and
to fill the passed in ETW data descriptor.
Arguments:
data - argument to be decoded
dataDescriptor - pointer to the descriptor to be filled (updated to point to the next empty entry)
dataBuffer - storage buffer for storing user data, needed because cant get the address of the object
(updated to point to the next empty entry)
Return Value:
null if the object is a basic type other than string or byte[]. String otherwise
--*/
{
Again:
dataDescriptor->Reserved = 0;
string? sRet = data as string;
byte[]? blobRet = null;
if (sRet != null)
{
dataDescriptor->Size = ((uint)sRet.Length + 1) * 2;
}
else if ((blobRet = data as byte[]) != null)
{
// first store array length
*(int*)dataBuffer = blobRet.Length;
dataDescriptor->Ptr = (ulong)dataBuffer;
dataDescriptor->Size = 4;
totalEventSize += dataDescriptor->Size;
// then the array parameters
dataDescriptor++;
dataBuffer += BasicTypeAllocationBufferSize;
dataDescriptor->Size = (uint)blobRet.Length;
}
else if (data is IntPtr)
{
dataDescriptor->Size = (uint)sizeof(IntPtr);
IntPtr* intptrPtr = (IntPtr*)dataBuffer;
*intptrPtr = (IntPtr)data;
dataDescriptor->Ptr = (ulong)intptrPtr;
}
else if (data is int)
{
dataDescriptor->Size = (uint)sizeof(int);
int* intptr = (int*)dataBuffer;
*intptr = (int)data;
dataDescriptor->Ptr = (ulong)intptr;
}
else if (data is long)
{
dataDescriptor->Size = (uint)sizeof(long);
long* longptr = (long*)dataBuffer;
*longptr = (long)data;
dataDescriptor->Ptr = (ulong)longptr;
}
else if (data is uint)
{
dataDescriptor->Size = (uint)sizeof(uint);
uint* uintptr = (uint*)dataBuffer;
*uintptr = (uint)data;
dataDescriptor->Ptr = (ulong)uintptr;
}
else if (data is ulong)
{
dataDescriptor->Size = (uint)sizeof(ulong);
ulong* ulongptr = (ulong*)dataBuffer;
*ulongptr = (ulong)data;
dataDescriptor->Ptr = (ulong)ulongptr;
}
else if (data is char)
{
dataDescriptor->Size = (uint)sizeof(char);
char* charptr = (char*)dataBuffer;
*charptr = (char)data;
dataDescriptor->Ptr = (ulong)charptr;
}
else if (data is byte)
{
dataDescriptor->Size = (uint)sizeof(byte);
byte* byteptr = (byte*)dataBuffer;
*byteptr = (byte)data;
dataDescriptor->Ptr = (ulong)byteptr;
}
else if (data is short)
{
dataDescriptor->Size = (uint)sizeof(short);
short* shortptr = (short*)dataBuffer;
*shortptr = (short)data;
dataDescriptor->Ptr = (ulong)shortptr;
}
else if (data is sbyte)
{
dataDescriptor->Size = (uint)sizeof(sbyte);
sbyte* sbyteptr = (sbyte*)dataBuffer;
*sbyteptr = (sbyte)data;
dataDescriptor->Ptr = (ulong)sbyteptr;
}
else if (data is ushort)
{
dataDescriptor->Size = (uint)sizeof(ushort);
ushort* ushortptr = (ushort*)dataBuffer;
*ushortptr = (ushort)data;
dataDescriptor->Ptr = (ulong)ushortptr;
}
else if (data is float)
{
dataDescriptor->Size = (uint)sizeof(float);
float* floatptr = (float*)dataBuffer;
*floatptr = (float)data;
dataDescriptor->Ptr = (ulong)floatptr;
}
else if (data is double)
{
dataDescriptor->Size = (uint)sizeof(double);
double* doubleptr = (double*)dataBuffer;
*doubleptr = (double)data;
dataDescriptor->Ptr = (ulong)doubleptr;
}
else if (data is bool)
{
// WIN32 Bool is 4 bytes
dataDescriptor->Size = 4;
int* intptr = (int*)dataBuffer;
if ((bool)data)
{
*intptr = 1;
}
else
{
*intptr = 0;
}
dataDescriptor->Ptr = (ulong)intptr;
}
else if (data is Guid)
{
dataDescriptor->Size = (uint)sizeof(Guid);
Guid* guidptr = (Guid*)dataBuffer;
*guidptr = (Guid)data;
dataDescriptor->Ptr = (ulong)guidptr;
}
else if (data is decimal)
{
dataDescriptor->Size = (uint)sizeof(decimal);
decimal* decimalptr = (decimal*)dataBuffer;
*decimalptr = (decimal)data;
dataDescriptor->Ptr = (ulong)decimalptr;
}
else if (data is DateTime)
{
const long UTCMinTicks = 504911232000000000;
long dateTimeTicks = 0;
// We cannot translate dates sooner than 1/1/1601 in UTC.
// To avoid getting an ArgumentOutOfRangeException we compare with 1/1/1601 DateTime ticks
if (((DateTime)data).Ticks > UTCMinTicks)
dateTimeTicks = ((DateTime)data).ToFileTimeUtc();
dataDescriptor->Size = (uint)sizeof(long);
long* longptr = (long*)dataBuffer;
*longptr = dateTimeTicks;
dataDescriptor->Ptr = (ulong)longptr;
}
else
{
if (data is Enum)
{
try
{
Type underlyingType = Enum.GetUnderlyingType(data.GetType());
if (underlyingType == typeof(ulong))
data = (ulong)data;
else if (underlyingType == typeof(long))
data = (long)data;
else
data = (int)Convert.ToInt64(data); // This handles all int/uint or below (we treat them like 32 bit ints)
goto Again;
}
catch { } // On weird cases (e.g. enums of type double), give up and for compat simply tostring.
}
// To our eyes, everything else is a just a string
if (data == null)
sRet = "";
else
sRet = data.ToString()!;
dataDescriptor->Size = ((uint)sRet.Length + 1) * 2;
}
totalEventSize += dataDescriptor->Size;
// advance buffers
dataDescriptor++;
dataBuffer += BasicTypeAllocationBufferSize;
return (object?)sRet ?? (object?)blobRet;
}
/// <summary>
/// WriteEvent, method to write a parameters with event schema properties
/// </summary>
/// <param name="eventDescriptor">
/// Event Descriptor for this event.
/// </param>
/// <param name="eventHandle">
/// Event handle for this event.
/// </param>
/// <param name="activityID">
/// A pointer to the activity ID GUID to log
/// </param>
/// <param name="childActivityID">
/// childActivityID is marked as 'related' to the current activity ID.
/// </param>
/// <param name="eventPayload">
/// Payload for the ETW event.
/// </param>
internal unsafe bool WriteEvent(ref EventDescriptor eventDescriptor, IntPtr eventHandle, Guid* activityID, Guid* childActivityID, object?[] eventPayload)
{
WriteEventErrorCode status = WriteEventErrorCode.NoError;
if (IsEnabled(eventDescriptor.Level, eventDescriptor.Keywords))
{
int argCount = eventPayload.Length;
if (argCount > EtwMaxNumberArguments)
{
s_returnCode = WriteEventErrorCode.TooManyArgs;
return false;
}
uint totalEventSize = 0;
int index;
int refObjIndex = 0;
Debug.Assert(EtwAPIMaxRefObjCount == 8, $"{nameof(EtwAPIMaxRefObjCount)} must equal the number of fields in {nameof(EightObjects)}");
EightObjects eightObjectStack = default;
Span<int> refObjPosition = stackalloc int[EtwAPIMaxRefObjCount];
Span<object?> dataRefObj = eightObjectStack;
EventData* userData = stackalloc EventData[2 * argCount];
for (int i = 0; i < 2 * argCount; i++)
userData[i] = default;
EventData* userDataPtr = userData;
byte* dataBuffer = stackalloc byte[BasicTypeAllocationBufferSize * 2 * argCount]; // Assume 16 chars for non-string argument
byte* currentBuffer = dataBuffer;
//
// The loop below goes through all the arguments and fills in the data
// descriptors. For strings save the location in the dataString array.
// Calculates the total size of the event by adding the data descriptor
// size value set in EncodeObject method.
//
bool hasNonStringRefArgs = false;
for (index = 0; index < eventPayload.Length; index++)
{
if (eventPayload[index] != null)
{
object? supportedRefObj = EncodeObject(ref eventPayload[index], ref userDataPtr, ref currentBuffer, ref totalEventSize);
if (supportedRefObj != null)
{
// EncodeObject advanced userDataPtr to the next empty slot
int idx = (int)(userDataPtr - userData - 1);
if (supportedRefObj is not string)
{
if (eventPayload.Length + idx + 1 - index > EtwMaxNumberArguments)
{
s_returnCode = WriteEventErrorCode.TooManyArgs;
return false;
}
hasNonStringRefArgs = true;
}
if (refObjIndex >= dataRefObj.Length)
{
Span<object?> newDataRefObj = new object?[dataRefObj.Length * 2];
dataRefObj.CopyTo(newDataRefObj);
dataRefObj = newDataRefObj;
Span<int> newRefObjPosition = new int[refObjPosition.Length * 2];
refObjPosition.CopyTo(newRefObjPosition);
refObjPosition = newRefObjPosition;
}
dataRefObj[refObjIndex] = supportedRefObj;
refObjPosition[refObjIndex] = idx;
refObjIndex++;
}
}
else
{
s_returnCode = WriteEventErrorCode.NullInput;
return false;
}
}
// update argCount based on actual number of arguments written to 'userData'
argCount = (int)(userDataPtr - userData);
if (totalEventSize > TraceEventMaximumSize)
{
s_returnCode = WriteEventErrorCode.EventTooBig;
return false;
}
// the optimized path (using "fixed" instead of allocating pinned GCHandles
if (!hasNonStringRefArgs && (refObjIndex <= EtwAPIMaxRefObjCount))
{
// Fast path: at most 8 string arguments
// ensure we have at least s_etwAPIMaxStringCount in dataString, so that
// the "fixed" statement below works
while (refObjIndex < EtwAPIMaxRefObjCount)
{
dataRefObj[refObjIndex] = null;
refObjPosition[refObjIndex] = -1;
++refObjIndex;
}
//
// now fix any string arguments and set the pointer on the data descriptor
//
fixed (char* v0 = (string?)dataRefObj[0], v1 = (string?)dataRefObj[1], v2 = (string?)dataRefObj[2], v3 = (string?)dataRefObj[3],
v4 = (string?)dataRefObj[4], v5 = (string?)dataRefObj[5], v6 = (string?)dataRefObj[6], v7 = (string?)dataRefObj[7])
{
userDataPtr = userData;
if (dataRefObj[0] != null)
{
userDataPtr[refObjPosition[0]].Ptr = (ulong)v0;
}
if (dataRefObj[1] != null)
{
userDataPtr[refObjPosition[1]].Ptr = (ulong)v1;
}
if (dataRefObj[2] != null)
{
userDataPtr[refObjPosition[2]].Ptr = (ulong)v2;
}
if (dataRefObj[3] != null)
{
userDataPtr[refObjPosition[3]].Ptr = (ulong)v3;
}
if (dataRefObj[4] != null)
{
userDataPtr[refObjPosition[4]].Ptr = (ulong)v4;
}
if (dataRefObj[5] != null)
{
userDataPtr[refObjPosition[5]].Ptr = (ulong)v5;
}
if (dataRefObj[6] != null)
{
userDataPtr[refObjPosition[6]].Ptr = (ulong)v6;
}
if (dataRefObj[7] != null)
{
userDataPtr[refObjPosition[7]].Ptr = (ulong)v7;
}
status = _eventProvider.EventWriteTransfer(in eventDescriptor, eventHandle, activityID, childActivityID, argCount, userData);
}
}
else
{
// Slow path: use pinned handles
userDataPtr = userData;
GCHandle[] rgGCHandle = new GCHandle[refObjIndex];
for (int i = 0; i < refObjIndex; ++i)
{
// below we still use "fixed" to avoid taking dependency on the offset of the first field
// in the object (the way we would need to if we used GCHandle.AddrOfPinnedObject)
rgGCHandle[i] = GCHandle.Alloc(dataRefObj[i], GCHandleType.Pinned);
if (dataRefObj[i] is string)
{
fixed (char* p = (string?)dataRefObj[i])
userDataPtr[refObjPosition[i]].Ptr = (ulong)p;
}
else
{
fixed (byte* p = (byte[]?)dataRefObj[i])
userDataPtr[refObjPosition[i]].Ptr = (ulong)p;
}
}
status = _eventProvider.EventWriteTransfer(in eventDescriptor, eventHandle, activityID, childActivityID, argCount, userData);
for (int i = 0; i < refObjIndex; ++i)
{
rgGCHandle[i].Free();
}
}
}
if (status != WriteEventErrorCode.NoError)
{
SetLastError(status);
return false;
}
return true;
}
/// <summary>
/// WriteEvent, method to be used by generated code on a derived class
/// </summary>
/// <param name="eventDescriptor">
/// Event Descriptor for this event.
/// </param>
/// <param name="eventHandle">
/// Event handle for this event.
/// </param>
/// <param name="activityID">
/// A pointer to the activity ID to log
/// </param>
/// <param name="childActivityID">
/// If this event is generating a child activity (WriteEventTransfer related activity) this is child activity
/// This can be null for events that do not generate a child activity.
/// </param>
/// <param name="dataCount">
/// number of event descriptors
/// </param>
/// <param name="data">
/// pointer do the event data
/// </param>
protected internal unsafe bool WriteEvent(ref EventDescriptor eventDescriptor, IntPtr eventHandle, Guid* activityID, Guid* childActivityID, int dataCount, IntPtr data)
{
if (childActivityID != null)
{
// activity transfers are supported only for events that specify the Send or Receive opcode
Debug.Assert((EventOpcode)eventDescriptor.Opcode == EventOpcode.Send ||
(EventOpcode)eventDescriptor.Opcode == EventOpcode.Receive ||
(EventOpcode)eventDescriptor.Opcode == EventOpcode.Start ||
(EventOpcode)eventDescriptor.Opcode == EventOpcode.Stop);
}
WriteEventErrorCode status = _eventProvider.EventWriteTransfer(in eventDescriptor, eventHandle, activityID, childActivityID, dataCount, (EventData*)data);
if (status != 0)
{
SetLastError(status);
return false;
}
return true;
}
internal unsafe bool WriteEventRaw(
ref EventDescriptor eventDescriptor,
IntPtr eventHandle,
Guid* activityID,
Guid* relatedActivityID,
int dataCount,
IntPtr data)
{
WriteEventErrorCode status = _eventProvider.EventWriteTransfer(
in eventDescriptor,
eventHandle,
activityID,
relatedActivityID,
dataCount,
(EventData*)data);
if (status != WriteEventErrorCode.NoError)
{
SetLastError(status);
return false;
}
return true;
}
#if TARGET_WINDOWS
internal unsafe int SetInformation(
Interop.Advapi32.EVENT_INFO_CLASS eventInfoClass,
void* data,
uint dataSize)
{
return ((EtwEventProvider)_eventProvider).SetInformation(eventInfoClass, data, dataSize);
}
#endif
}
#if TARGET_WINDOWS
// A wrapper around the ETW-specific API calls.
internal sealed class EtwEventProvider : EventProviderImpl
{
/// <summary>
/// A struct characterizing ETW sessions (identified by the etwSessionId) as
/// activity-tracing-aware or legacy. A session that's activity-tracing-aware
/// has specified one non-zero bit in the reserved range 44-47 in the
/// 'allKeywords' value it passed in for a specific EventProvider.
/// </summary>
public struct SessionInfo
{
internal int sessionIdBit; // the index of the bit used for tracing in the "reserved" field of AllKeywords
internal int etwSessionId; // the machine-wide ETW session ID
internal SessionInfo(int sessionIdBit_, int etwSessionId_)
{ sessionIdBit = sessionIdBit_; etwSessionId = etwSessionId_; }
}
private readonly WeakReference<EventProvider> _eventProvider;
private long _registrationHandle;
private GCHandle _gcHandle;
private List<SessionInfo>? _liveSessions; // current live sessions (KeyValuePair<sessionIdBit, etwSessionId>)
private Guid _providerId;
internal EtwEventProvider(EventProvider eventProvider)
{
_eventProvider = new WeakReference<EventProvider>(eventProvider);
}
internal override void Disable()
{
base.Disable();
_liveSessions = null;
}
protected override unsafe void HandleEnableNotification(
EventProvider target,
byte *additionalData,
byte level,
long matchAnyKeywords,
long matchAllKeywords,
Interop.Advapi32.EVENT_FILTER_DESCRIPTOR* filterData)
{
Debug.Assert(additionalData == null);
// The GetSessions() logic was here to support the idea that different ETW sessions
// could have different user-defined filters. (I believe it is currently broken.)
//
// All this session based logic should be reviewed and likely removed, but that is a larger
// change that needs more careful staging.
List<KeyValuePair<SessionInfo, bool>> sessionsChanged = GetChangedSessions();
foreach (KeyValuePair<SessionInfo, bool> session in sessionsChanged)
{
int sessionChanged = session.Key.sessionIdBit;
bool bEnabling = session.Value;
// reinitialize args for every session...
IDictionary<string, string?>? args = null;
ControllerCommand command = ControllerCommand.Update;
// read filter data only when a session is being *added*
if (bEnabling)
{
byte[]? filterDataBytes;
// if we get more than one session changed we have no way
// of knowing which one "filterData" belongs to
if (sessionsChanged.Count > 1 || filterData == null)
{
TryReadRegistryFilterData(session.Key.etwSessionId, out command, out filterDataBytes);
}
else
{
MarshalFilterData(filterData, out command, out filterDataBytes);
}
args = ParseFilterData(filterDataBytes);
}
// execute OnControllerCommand once for every session that has changed.
// If the sessionId argument is positive it will be sent to the EventSource as an Enable,
// and if it is negative it will be sent as a disable. See EventSource.DoCommand()
target.OnControllerCommand(command, args, bEnabling ? sessionChanged : -sessionChanged);
}
}
[UnmanagedCallersOnly]
private static unsafe void Callback(Guid* sourceId, int isEnabled, byte level,
long matchAnyKeywords, long matchAllKeywords, Interop.Advapi32.EVENT_FILTER_DESCRIPTOR* filterData, void* callbackContext)
{
EtwEventProvider _this = (EtwEventProvider)GCHandle.FromIntPtr((IntPtr)callbackContext).Target!;
if (_this._eventProvider.TryGetTarget(out EventProvider? target))
{
_this.ProviderCallback(target, null, isEnabled, level, matchAnyKeywords, matchAllKeywords, filterData);
}
}
// Register an event provider.
internal override unsafe void Register(Guid id, string name)
{
Debug.Assert(!_gcHandle.IsAllocated);
_gcHandle = GCHandle.Alloc(this);
long registrationHandle = 0;
_providerId = id;
Guid providerId = _providerId;
uint status = Interop.Advapi32.EventRegister(
&providerId,
&Callback,
(void*)GCHandle.ToIntPtr(_gcHandle),
®istrationHandle);
if (status != 0)
{
_gcHandle.Free();
throw new ArgumentException(Interop.Kernel32.GetMessage((int)status));
}
Debug.Assert(_registrationHandle == 0);
_registrationHandle = registrationHandle;
}
// Unregister an event provider.
internal override void Unregister()
{
if (_registrationHandle != 0)
{
Interop.Advapi32.EventUnregister(_registrationHandle);
_registrationHandle = 0;
}
if (_gcHandle.IsAllocated)
{
_gcHandle.Free();
}
}
// Write an event.
internal override unsafe EventProvider.WriteEventErrorCode EventWriteTransfer(
in EventDescriptor eventDescriptor,
IntPtr eventHandle,
Guid* activityId,
Guid* relatedActivityId,
int userDataCount,
EventProvider.EventData* userData)
{
int error = Interop.Advapi32.EventWriteTransfer(
_registrationHandle,
in eventDescriptor,
activityId,
relatedActivityId,
userDataCount,
userData);
return error switch
{
Interop.Errors.ERROR_ARITHMETIC_OVERFLOW or Interop.Errors.ERROR_MORE_DATA => EventProvider.WriteEventErrorCode.EventTooBig,
Interop.Errors.ERROR_NOT_ENOUGH_MEMORY => EventProvider.WriteEventErrorCode.NoFreeBuffers,
_ => EventProvider.WriteEventErrorCode.NoError,
};
}
// Get or set the per-thread activity ID.
internal override int ActivityIdControl(Interop.Advapi32.ActivityControl ControlCode, ref Guid ActivityId)
{
return Interop.Advapi32.EventActivityIdControl(
ControlCode,
ref ActivityId);
}
// Define an EventPipeEvent handle.
internal override unsafe IntPtr DefineEventHandle(uint eventID, string eventName, long keywords, uint eventVersion,
uint level, byte* pMetadata, uint metadataLength)
{
throw new NotSupportedException();
}
private static bool s_setInformationMissing;
internal unsafe int SetInformation(
Interop.Advapi32.EVENT_INFO_CLASS eventInfoClass,
void* data,
uint dataSize)
{
int status = Interop.Errors.ERROR_NOT_SUPPORTED;
if (!s_setInformationMissing)
{
try
{
status = Interop.Advapi32.EventSetInformation(
_registrationHandle,
eventInfoClass,
data,
dataSize);
}
catch (TypeLoadException)
{
s_setInformationMissing = true;
}
}
return status;
}
/// <summary>
/// Callback data for ETW is only present when the provider is active at the time the controller issues the command.
/// To allow for providers to activate after the controller issued a command, we also check the registry and use that
/// to get the data. The function returns an array of bytes representing the data, the index into that byte array
/// where the data starts, and the command being issued associated with that data.
/// </summary>
private bool TryReadRegistryFilterData(int etwSessionId, out ControllerCommand command, out byte[]? data)
{
command = ControllerCommand.Update;
data = null;
string regKey = @"\Microsoft\Windows\CurrentVersion\Winevt\Publishers\{" + _providerId + "}";
if (IntPtr.Size == 8)
{
regKey = @"Software\Wow6432Node" + regKey;
}
else
{
regKey = "Software" + regKey;
}
string valueName = "ControllerData_Session_" + etwSessionId.ToString(CultureInfo.InvariantCulture);
// we need to assert this permission for partial trust scenarios
using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(regKey))
{
data = key?.GetValue(valueName, null) as byte[];
if (data != null)
{
// We only used the persisted data from the registry for updates.
return true;
}
}
return false;
}
/// <summary>
/// Determines the ETW sessions that have been added and/or removed to the set of
/// sessions interested in the current provider. It does so by (1) enumerating over all
/// ETW sessions that enabled 'this.m_Guid' for the current process ID, and (2)
/// comparing the current list with a list it cached on the previous invocation.
///
/// The return value is a list of tuples, where the SessionInfo specifies the
/// ETW session that was added or remove, and the bool specifies whether the
/// session was added or whether it was removed from the set.
/// </summary>
private List<KeyValuePair<SessionInfo, bool>> GetChangedSessions()
{
List<SessionInfo>? liveSessionList = null;
GetSessionInfo(
GetSessionInfoCallback,
ref liveSessionList);
List<KeyValuePair<SessionInfo, bool>> changedSessionList = new List<KeyValuePair<SessionInfo, bool>>();
// first look for sessions that have gone away (or have changed)
// (present in the _liveSessions but not in the new liveSessionList)
if (_liveSessions != null)
{
foreach (SessionInfo s in _liveSessions)
{
int idx;
if ((idx = IndexOfSessionInList(liveSessionList, s.etwSessionId)) < 0 ||
(liveSessionList![idx].sessionIdBit != s.sessionIdBit))
changedSessionList.Add(new KeyValuePair<SessionInfo, bool>(s, false));
}
}
// next look for sessions that were created since the last callback (or have changed)
// (present in the new liveSessionList but not in _liveSessions)
if (liveSessionList != null)
{
foreach (SessionInfo s in liveSessionList)
{
int idx;
if ((idx = IndexOfSessionInList(_liveSessions, s.etwSessionId)) < 0 ||
(_liveSessions![idx].sessionIdBit != s.sessionIdBit))
changedSessionList.Add(new KeyValuePair<SessionInfo, bool>(s, true));
}
}
_liveSessions = liveSessionList;
return changedSessionList;
}
/// <summary>
/// This method is the callback used by GetSessions() when it calls into GetSessionInfo().
/// It updates a List{SessionInfo} based on the etwSessionId and matchAllKeywords that
/// GetSessionInfo() passes in.
/// </summary>
private static void GetSessionInfoCallback(int etwSessionId, long matchAllKeywords,
ref List<SessionInfo>? sessionList)
{
uint sessionIdBitMask = (uint)SessionMask.FromEventKeywords(unchecked((ulong)matchAllKeywords));
// an ETW controller that specifies more than the mandated bit for our EventSource
// will be ignored...
int val = BitOperations.PopCount(sessionIdBitMask);
if (val > 1)
return;
sessionList ??= new List<SessionInfo>(8);
if (val == 1)
{
// activity-tracing-aware etw session
val = BitOperations.TrailingZeroCount(sessionIdBitMask);
}
else
{
// legacy etw session
val = BitOperations.PopCount((uint)SessionMask.All);
}
sessionList.Add(new SessionInfo(val + 1, etwSessionId));
}
private delegate void SessionInfoCallback(int etwSessionId, long matchAllKeywords, ref List<SessionInfo>? sessionList);
/// <summary>
/// This method enumerates over all active ETW sessions that have enabled 'this.m_Guid'
/// for the current process ID, calling 'action' for each session, and passing it the
/// ETW session and the 'AllKeywords' the session enabled for the current provider.
/// </summary>
private unsafe void GetSessionInfo(SessionInfoCallback action, ref List<SessionInfo>? sessionList)
{
int buffSize = 256; // An initial guess that probably works most of the time.
byte* stackSpace = stackalloc byte[buffSize];
byte* buffer = stackSpace;
try
{
while (true)
{
int hr = 0;
fixed (Guid* provider = &_providerId)
{
hr = Interop.Advapi32.EnumerateTraceGuidsEx(Interop.Advapi32.TRACE_QUERY_INFO_CLASS.TraceGuidQueryInfo,
provider, sizeof(Guid), buffer, buffSize, out buffSize);
}
if (hr == 0)
break;
if (hr != Interop.Errors.ERROR_INSUFFICIENT_BUFFER)
return;
if (buffer != stackSpace)
{
byte* toFree = buffer;
buffer = null;
Marshal.FreeHGlobal((IntPtr)toFree);
}
buffer = (byte*)Marshal.AllocHGlobal(buffSize);
}
var providerInfos = (Interop.Advapi32.TRACE_GUID_INFO*)buffer;
var providerInstance = (Interop.Advapi32.TRACE_PROVIDER_INSTANCE_INFO*)&providerInfos[1];
int processId = unchecked((int)Interop.Kernel32.GetCurrentProcessId());
// iterate over the instances of the EventProvider in all processes
for (int i = 0; i < providerInfos->InstanceCount; i++)
{
if (providerInstance->Pid == processId)
{
var enabledInfos = (Interop.Advapi32.TRACE_ENABLE_INFO*)&providerInstance[1];
// iterate over the list of active ETW sessions "listening" to the current provider
for (int j = 0; j < providerInstance->EnableCount; j++)
action(enabledInfos[j].LoggerId, enabledInfos[j].MatchAllKeyword, ref sessionList);
}
if (providerInstance->NextOffset == 0)
break;
Debug.Assert(0 <= providerInstance->NextOffset && providerInstance->NextOffset < buffSize);
byte* structBase = (byte*)providerInstance;
providerInstance = (Interop.Advapi32.TRACE_PROVIDER_INSTANCE_INFO*)&structBase[providerInstance->NextOffset];
}
}
finally
{
if (buffer != null && buffer != stackSpace)
{
Marshal.FreeHGlobal((IntPtr)buffer);
}
}
}
/// <summary>
/// Returns the index of the SesisonInfo from 'sessions' that has the specified 'etwSessionId'
/// or -1 if the value is not present.
/// </summary>
private static int IndexOfSessionInList(List<SessionInfo>? sessions, int etwSessionId)
{
if (sessions == null)
return -1;
// for non-coreclr code we could use List<T>.FindIndex(Predicate<T>), but we need this to compile
// on coreclr as well
for (int i = 0; i < sessions.Count; ++i)
if (sessions[i].etwSessionId == etwSessionId)
return i;
return -1;
}
}
#endif
#pragma warning disable CA1852 // EventProviderImpl is not derived from in all targets
internal class EventProviderImpl
{
protected byte _level; // Tracing Level
protected long _anyKeywordMask; // Trace Enable Flags
protected long _allKeywordMask; // Match all keyword
protected bool _enabled; // Enabled flag from Trace callback
internal EventLevel Level
{
get => (EventLevel)_level;
set => _level = (byte)value;
}
internal EventKeywords MatchAnyKeyword
{
get => (EventKeywords)_anyKeywordMask;
set => _anyKeywordMask = unchecked((long)value);
}
internal EventKeywords MatchAllKeyword
{
get => (EventKeywords)_allKeywordMask;
set => _allKeywordMask = unchecked((long)value);
}
protected virtual unsafe void HandleEnableNotification(
EventProvider target,
byte *additionalData,
byte level,
long matchAnyKeywords,
long matchAllKeywords,
Interop.Advapi32.EVENT_FILTER_DESCRIPTOR* filterData)
{
}
/// <summary>
/// IsEnabled, method used to test if provider is enabled
/// </summary>
public bool IsEnabled()
{
return _enabled;
}
/// <summary>
/// IsEnabled, method used to test if event is enabled
/// </summary>
/// <param name="level">
/// Level to test
/// </param>
/// <param name="keywords">
/// Keyword to test
/// </param>
public bool IsEnabled(byte level, long keywords)
{
//
// If not enabled at all, return false.
//
if (!_enabled)
{
return false;
}
// This also covers the case of Level == 0.
if ((level <= _level) ||
(_level == 0))
{
//
// Check if Keyword is enabled
//
if ((keywords == 0) ||
(((keywords & _anyKeywordMask) != 0) &&
((keywords & _allKeywordMask) == _allKeywordMask)))
{
return true;
}
}
return false;
}
internal void Enable(byte level, long anyKeyword, long allKeyword)
{
_enabled = true;
_level = level;
_anyKeywordMask = anyKeyword;
_allKeywordMask = allKeyword;
}
internal virtual void Disable()
{
_enabled = false;
_level = 0;
_anyKeywordMask = 0;
_allKeywordMask = 0;
}
internal virtual void Register(Guid id, string name)
{
}
internal virtual void Unregister()
{
}
internal virtual unsafe EventProvider.WriteEventErrorCode EventWriteTransfer(
in EventDescriptor eventDescriptor,
IntPtr eventHandle,
Guid* activityId,
Guid* relatedActivityId,
int userDataCount,
EventProvider.EventData* userData)
{
return EventProvider.WriteEventErrorCode.NoError;
}
internal virtual int ActivityIdControl(Interop.Advapi32.ActivityControl ControlCode, ref Guid ActivityId)
{
return 0;
}
// Define an EventPipeEvent handle.
internal virtual unsafe IntPtr DefineEventHandle(uint eventID, string eventName, long keywords, uint eventVersion,
uint level, byte* pMetadata, uint metadataLength)
{
return IntPtr.Zero;
}
protected unsafe void ProviderCallback(
EventProvider target,
byte *additionalData,
int controlCode,
byte level,
long matchAnyKeywords,
long matchAllKeywords,
Interop.Advapi32.EVENT_FILTER_DESCRIPTOR* filterData)
{
// This is an optional callback API. We will therefore ignore any failures that happen as a
// result of turning on this provider as to not crash the app.
// EventSource has code to validate whether initialization it expected to occur actually occurred
try
{
if (controlCode == Interop.Advapi32.EVENT_CONTROL_CODE_ENABLE_PROVIDER)
{
Enable(level, matchAnyKeywords, matchAllKeywords);
HandleEnableNotification(target, additionalData, level, matchAnyKeywords, matchAllKeywords, filterData);
return;
}
ControllerCommand command = ControllerCommand.Update;
if (controlCode == Interop.Advapi32.EVENT_CONTROL_CODE_DISABLE_PROVIDER)
{
Disable();
}
else if (controlCode == Interop.Advapi32.EVENT_CONTROL_CODE_CAPTURE_STATE)
{
command = ControllerCommand.SendManifest;
}
else
{
return; // per spec you ignore commands you don't recognize.
}
target.OnControllerCommand(command, null, 0);
}
catch
{
// We want to ignore any failures that happen as a result of turning on this provider as to
// not crash the app.
}
}
private static int FindNull(byte[] buffer, int idx)
{
while (idx < buffer.Length && buffer[idx] != 0)
{
idx++;
}
return idx;
}
protected static IDictionary<string, string?>? ParseFilterData(byte[]? data)
{
Dictionary<string, string?>? args = null;
// data can be null if the filterArgs had a very large size which failed our sanity check
if (data != null)
{
args = new Dictionary<string, string?>(4);
int dataStart = 0;
while (dataStart < data.Length)
{
int keyEnd = FindNull(data, dataStart);
int valueIdx = keyEnd + 1;
int valueEnd = FindNull(data, valueIdx);
if (valueEnd < data.Length)
{
string key = Text.Encoding.UTF8.GetString(data, dataStart, keyEnd - dataStart);
string value = Text.Encoding.UTF8.GetString(data, valueIdx, valueEnd - valueIdx);
args[key] = value;
}
dataStart = valueEnd + 1;
}
}
return args;
}
protected unsafe bool MarshalFilterData(Interop.Advapi32.EVENT_FILTER_DESCRIPTOR* filterData, out ControllerCommand command, out byte[]? data)
{
Debug.Assert(filterData != null);
data = null;
// ETW limited filter data to 1024 bytes but EventPipe doesn't. DiagnosticSourceEventSource
// can legitimately use large filter data buffers to encode a large set of events and properties
// that should be gathered so I am bumping the limit from 1K -> 100K.
if (filterData->Ptr != 0 && 0 < filterData->Size && filterData->Size <= 100 * 1024)
{
data = new byte[filterData->Size];
Marshal.Copy((IntPtr)(void*)filterData->Ptr, data, 0, data.Length);
}
command = (ControllerCommand)filterData->Type;
return true;
}
}
#pragma warning restore CA1852
}
|