diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 2c5f234cc2d163..4090ef0d1b6fcf 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1594,11 +1594,13 @@
+
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventListener.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventListener.cs
new file mode 100644
index 00000000000000..5d1ab4b30becf5
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventListener.cs
@@ -0,0 +1,638 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Threading;
+
+namespace System.Diagnostics.Tracing;
+
+///
+/// An EventListener represents a target for the events generated by EventSources (that is subclasses
+/// of ), in the current appdomain. When a new EventListener is created
+/// it is logically attached to all eventSources in that appdomain. When the EventListener is Disposed, then
+/// it is disconnected from the event eventSources. Note that there is a internal list of STRONG references
+/// to EventListeners, which means that relying on the lack of references to EventListeners to clean up
+/// EventListeners will NOT work. You must call EventListener.Dispose explicitly when a dispatcher is no
+/// longer needed.
+///
+/// Once created, EventListeners can enable or disable on a per-eventSource basis using verbosity levels
+/// ( ) and bitfields ( ) to further restrict the set of
+/// events to be sent to the dispatcher. The dispatcher can also send arbitrary commands to a particular
+/// eventSource using the 'SendCommand' method. The meaning of the commands are eventSource specific.
+///
+/// The Null Guid (that is (new Guid()) has special meaning as a wildcard for 'all current eventSources in
+/// the appdomain'. Thus it is relatively easy to turn on all events in the appdomain if desired.
+///
+/// It is possible for there to be many EventListener's defined in a single appdomain. Each dispatcher is
+/// logically independent of the other listeners. Thus when one dispatcher enables or disables events, it
+/// affects only that dispatcher (other listeners get the events they asked for). It is possible that
+/// commands sent with 'SendCommand' would do a semantic operation that would affect the other listeners
+/// (like doing a GC, or flushing data ...), but this is the exception rather than the rule.
+///
+/// Thus the model is that each EventSource keeps a list of EventListeners that it is sending events
+/// to. Associated with each EventSource-dispatcher pair is a set of filtering criteria that determine for
+/// that eventSource what events that dispatcher will receive.
+///
+/// Listeners receive the events on their 'OnEventWritten' method. Thus subclasses of EventListener must
+/// override this method to do something useful with the data.
+///
+/// In addition, when new eventSources are created, the 'OnEventSourceCreate' method is called. The
+/// invariant associated with this callback is that every eventSource gets exactly one
+/// 'OnEventSourceCreate' call for ever eventSource that can potentially send it log messages. In
+/// particular when a EventListener is created, typically a series of OnEventSourceCreate' calls are
+/// made to notify the new dispatcher of all the eventSources that existed before the EventListener was
+/// created.
+///
+///
+public abstract class EventListener : IDisposable
+{
+ private event EventHandler? _EventSourceCreated;
+
+ ///
+ /// This event is raised whenever a new eventSource is 'attached' to the dispatcher.
+ /// This can happen for all existing EventSources when the EventListener is created
+ /// as well as for any EventSources that come into existence after the EventListener
+ /// has been created.
+ ///
+ /// These 'catch up' events are called during the construction of the EventListener.
+ /// Subclasses need to be prepared for that.
+ ///
+ /// In a multi-threaded environment, it is possible that 'EventSourceEventWrittenCallback'
+ /// events for a particular eventSource to occur BEFORE the EventSourceCreatedCallback is issued.
+ ///
+ public event EventHandler? EventSourceCreated
+ {
+ add
+ {
+ CallBackForExistingEventSources(false, value);
+
+ this._EventSourceCreated = (EventHandler?)Delegate.Combine(_EventSourceCreated, value);
+ }
+ remove
+ {
+ this._EventSourceCreated = (EventHandler?)Delegate.Remove(_EventSourceCreated, value);
+ }
+ }
+
+ ///
+ /// This event is raised whenever an event has been written by a EventSource for which
+ /// the EventListener has enabled events.
+ ///
+ public event EventHandler? EventWritten;
+
+ ///
+ /// Create a new EventListener in which all events start off turned off (use EnableEvents to turn
+ /// them on).
+ ///
+ protected EventListener()
+ {
+ // This will cause the OnEventSourceCreated callback to fire.
+ CallBackForExistingEventSources(true, (obj, args) =>
+ args.EventSource!.AddListener((EventListener)obj!));
+ }
+
+ ///
+ /// Dispose should be called when the EventListener no longer desires 'OnEvent*' callbacks. Because
+ /// there is an internal list of strong references to all EventListeners, calling 'Dispose' directly
+ /// is the only way to actually make the listen die. Thus it is important that users of EventListener
+ /// call Dispose when they are done with their logging.
+ ///
+ public virtual void Dispose()
+ {
+ lock (EventListenersLock)
+ {
+ if (s_Listeners != null)
+ {
+ if (this == s_Listeners)
+ {
+ EventListener cur = s_Listeners;
+ s_Listeners = this.m_Next;
+ RemoveReferencesToListenerInEventSources(cur);
+ }
+ else
+ {
+ // Find 'this' from the s_Listeners linked list.
+ EventListener prev = s_Listeners;
+ while (true)
+ {
+ EventListener? cur = prev.m_Next;
+ if (cur == null)
+ break;
+ if (cur == this)
+ {
+ // Found our Listener, remove references to it in the eventSources
+ prev.m_Next = cur.m_Next; // Remove entry.
+ RemoveReferencesToListenerInEventSources(cur);
+ break;
+ }
+ prev = cur;
+ }
+ }
+ }
+ Validate();
+ }
+
+#if FEATURE_PERFTRACING
+ // Remove the listener from the EventPipe dispatcher. EventCommand.Update with enable==false removes it.
+ EventPipeEventDispatcher.Instance.SendCommand(this, EventCommand.Update, false, EventLevel.LogAlways, (EventKeywords)0);
+#endif // FEATURE_PERFTRACING
+ }
+ // We don't expose a Dispose(bool), because the contract is that you don't have any non-syncronous
+ // 'cleanup' associated with this object
+
+ ///
+ /// Enable all events from the eventSource identified by 'eventSource' to the current
+ /// dispatcher that have a verbosity level of 'level' or lower.
+ ///
+ /// This call can have the effect of REDUCING the number of events sent to the
+ /// dispatcher if 'level' indicates a less verbose level than was previously enabled.
+ ///
+ /// This call never has an effect on other EventListeners.
+ ///
+ ///
+ public void EnableEvents(EventSource eventSource, EventLevel level)
+ {
+ EnableEvents(eventSource, level, EventKeywords.None);
+ }
+ ///
+ /// Enable all events from the eventSource identified by 'eventSource' to the current
+ /// dispatcher that have a verbosity level of 'level' or lower and have a event keyword
+ /// matching any of the bits in 'matchAnyKeyword'.
+ ///
+ /// This call can have the effect of REDUCING the number of events sent to the
+ /// dispatcher if 'level' indicates a less verbose level than was previously enabled or
+ /// if 'matchAnyKeyword' has fewer keywords set than where previously set.
+ ///
+ /// This call never has an effect on other EventListeners.
+ ///
+ public void EnableEvents(EventSource eventSource, EventLevel level, EventKeywords matchAnyKeyword)
+ {
+ EnableEvents(eventSource, level, matchAnyKeyword, null);
+ }
+ ///
+ /// Enable all events from the eventSource identified by 'eventSource' to the current
+ /// dispatcher that have a verbosity level of 'level' or lower and have a event keyword
+ /// matching any of the bits in 'matchAnyKeyword' as well as any (eventSource specific)
+ /// effect passing additional 'key-value' arguments 'arguments' might have.
+ ///
+ /// This call can have the effect of REDUCING the number of events sent to the
+ /// dispatcher if 'level' indicates a less verbose level than was previously enabled or
+ /// if 'matchAnyKeyword' has fewer keywords set than where previously set.
+ ///
+ /// This call never has an effect on other EventListeners.
+ ///
+ public void EnableEvents(EventSource eventSource, EventLevel level, EventKeywords matchAnyKeyword, IDictionary? arguments)
+ {
+ ArgumentNullException.ThrowIfNull(eventSource);
+
+ eventSource.SendCommand(this, EventProviderType.None, 0, EventCommand.Update, true, level, matchAnyKeyword, arguments);
+
+#if FEATURE_PERFTRACING
+ if (eventSource.GetType() == typeof(NativeRuntimeEventSource))
+ {
+ EventPipeEventDispatcher.Instance.SendCommand(this, EventCommand.Update, true, level, matchAnyKeyword);
+ }
+#endif // FEATURE_PERFTRACING
+ }
+ ///
+ /// Disables all events coming from eventSource identified by 'eventSource'.
+ ///
+ /// This call never has an effect on other EventListeners.
+ ///
+ public void DisableEvents(EventSource eventSource)
+ {
+ ArgumentNullException.ThrowIfNull(eventSource);
+
+ eventSource.SendCommand(this, EventProviderType.None, 0, EventCommand.Update, false, EventLevel.LogAlways, EventKeywords.None, null);
+
+#if FEATURE_PERFTRACING
+ if (eventSource.GetType() == typeof(NativeRuntimeEventSource))
+ {
+ EventPipeEventDispatcher.Instance.SendCommand(this, EventCommand.Update, false, EventLevel.LogAlways, EventKeywords.None);
+ }
+#endif // FEATURE_PERFTRACING
+ }
+
+ ///
+ /// EventSourceIndex is small non-negative integer (suitable for indexing in an array)
+ /// identifying EventSource. It is unique per-appdomain. Some EventListeners might find
+ /// it useful to store additional information about each eventSource connected to it,
+ /// and EventSourceIndex allows this extra information to be efficiently stored in a
+ /// (growable) array (eg List(T)).
+ ///
+ protected internal static int EventSourceIndex(EventSource eventSource) { return eventSource.m_id; }
+
+ ///
+ /// This method is called whenever a new eventSource is 'attached' to the dispatcher.
+ /// This can happen for all existing EventSources when the EventListener is created
+ /// as well as for any EventSources that come into existence after the EventListener
+ /// has been created.
+ ///
+ /// These 'catch up' events are called during the construction of the EventListener.
+ /// Subclasses need to be prepared for that.
+ ///
+ /// In a multi-threaded environment, it is possible that 'OnEventWritten' callbacks
+ /// for a particular eventSource to occur BEFORE the OnEventSourceCreated is issued.
+ ///
+ ///
+ protected internal virtual void OnEventSourceCreated(EventSource eventSource)
+ {
+ EventHandler? callBack = this._EventSourceCreated;
+ if (callBack != null)
+ {
+ EventSourceCreatedEventArgs args = new EventSourceCreatedEventArgs();
+ args.EventSource = eventSource;
+ callBack(this, args);
+ }
+ }
+
+ ///
+ /// This method is called whenever an event has been written by a EventSource for which
+ /// the EventListener has enabled events.
+ ///
+ ///
+ protected internal virtual void OnEventWritten(EventWrittenEventArgs eventData)
+ {
+ this.EventWritten?.Invoke(this, eventData);
+ }
+
+#region private
+ ///
+ /// This routine adds newEventSource to the global list of eventSources, it also assigns the
+ /// ID to the eventSource (which is simply the ordinal in the global list).
+ ///
+ /// EventSources currently do not pro-actively remove themselves from this list. Instead
+ /// when eventSources's are GCed, the weak handle in this list naturally gets nulled, and
+ /// we will reuse the slot. Today this list never shrinks (but we do reuse entries
+ /// that are in the list). This seems OK since the expectation is that EventSources
+ /// tend to live for the lifetime of the appdomain anyway (they tend to be used in
+ /// global variables).
+ ///
+ ///
+ internal static void AddEventSource(EventSource newEventSource)
+ {
+ lock (EventListenersLock)
+ {
+ Debug.Assert(s_EventSources != null);
+
+ // Periodically search the list for existing entries to reuse, this avoids
+ // unbounded memory use if we keep recycling eventSources (an unlikely thing).
+ int newIndex = -1;
+ if (s_EventSources.Count % 64 == 63) // on every block of 64, fill up the block before continuing
+ {
+ int i = s_EventSources.Count; // Work from the top down.
+ while (0 < i)
+ {
+ --i;
+ WeakReference weakRef = s_EventSources[i];
+ if (!weakRef.TryGetTarget(out _))
+ {
+ newIndex = i;
+ weakRef.SetTarget(newEventSource);
+ break;
+ }
+ }
+ }
+ if (newIndex < 0)
+ {
+ newIndex = s_EventSources.Count;
+ s_EventSources.Add(new WeakReference(newEventSource));
+ }
+ newEventSource.m_id = newIndex;
+
+#if DEBUG
+ // Disable validation of EventSource/EventListener connections in case a call to EventSource.AddListener
+ // causes a recursive call into this method.
+ bool previousValue = s_ConnectingEventSourcesAndListener;
+ s_ConnectingEventSourcesAndListener = true;
+ try
+ {
+#endif
+ // Add every existing dispatcher to the new EventSource
+ for (EventListener? listener = s_Listeners; listener != null; listener = listener.m_Next)
+ newEventSource.AddListener(listener);
+#if DEBUG
+ }
+ finally
+ {
+ s_ConnectingEventSourcesAndListener = previousValue;
+ }
+#endif
+
+ Validate();
+ }
+ }
+
+ // Whenever we have async callbacks from native code, there is an ugly issue where
+ // during .NET shutdown native code could be calling the callback, but the CLR
+ // has already prohibited callbacks to managed code in the appdomain, causing the CLR
+ // to throw a COMPLUS_BOOT_EXCEPTION. The guideline we give is that you must unregister
+ // such callbacks on process shutdown or appdomain so that unmanaged code will never
+ // do this. This is what this callback is for.
+ // See bug 724140 for more
+ internal static void DisposeOnShutdown()
+ {
+ Debug.Assert(EventSource.IsSupported);
+ List sourcesToDispose = new List();
+ lock (EventListenersLock)
+ {
+ Debug.Assert(s_EventSources != null);
+ foreach (WeakReference esRef in s_EventSources)
+ {
+ if (esRef.TryGetTarget(out EventSource? es))
+ {
+ sourcesToDispose.Add(es);
+ }
+ }
+ }
+
+ // Do not invoke Dispose under the lock as this can lead to a deadlock.
+ // See https://github.com/dotnet/runtime/issues/48342 for details.
+ Debug.Assert(!Monitor.IsEntered(EventListenersLock));
+ foreach (EventSource es in sourcesToDispose)
+ {
+ es.Dispose();
+ }
+ }
+
+ // If an EventListener calls Dispose without calling DisableEvents first we want to issue the Disable command now
+ private static void CallDisableEventsIfNecessary(EventDispatcher eventDispatcher, EventSource eventSource)
+ {
+#if DEBUG
+ // Disable validation of EventSource/EventListener connections in case a call to EventSource.AddListener
+ // causes a recursive call into this method.
+ bool previousValue = s_ConnectingEventSourcesAndListener;
+ s_ConnectingEventSourcesAndListener = true;
+ try
+ {
+#endif
+ if (eventDispatcher.m_EventEnabled == null)
+ {
+ return;
+ }
+
+ foreach (bool value in eventDispatcher.m_EventEnabled.Values)
+ {
+ if (value)
+ {
+ eventDispatcher.m_Listener.DisableEvents(eventSource);
+ }
+ }
+#if DEBUG
+ }
+ finally
+ {
+ s_ConnectingEventSourcesAndListener = previousValue;
+ }
+#endif
+ }
+
+ ///
+ /// Helper used in code:Dispose that removes any references to 'listenerToRemove' in any of the
+ /// eventSources in the appdomain.
+ ///
+ /// The EventListenersLock must be held before calling this routine.
+ ///
+ private static void RemoveReferencesToListenerInEventSources(EventListener listenerToRemove)
+ {
+ Debug.Assert(Monitor.IsEntered(EventListenersLock));
+ // Foreach existing EventSource in the appdomain
+ Debug.Assert(s_EventSources != null);
+
+ // First pass to call DisableEvents
+ WeakReference[] eventSourcesSnapshot = s_EventSources.ToArray();
+ foreach (WeakReference eventSourceRef in eventSourcesSnapshot)
+ {
+ if (eventSourceRef.TryGetTarget(out EventSource? eventSource))
+ {
+ EventDispatcher? cur = eventSource.m_Dispatchers;
+ while (cur != null)
+ {
+ if (cur.m_Listener == listenerToRemove)
+ {
+ CallDisableEventsIfNecessary(cur, eventSource);
+ }
+
+ cur = cur.m_Next;
+ }
+ }
+ }
+
+ // DisableEvents can call back to user code and we have to start over since s_EventSources and
+ // eventSource.m_Dispatchers could have mutated
+ foreach (WeakReference eventSourceRef in s_EventSources)
+ {
+ if (eventSourceRef.TryGetTarget(out EventSource? eventSource)
+ && eventSource.m_Dispatchers != null)
+ {
+ // Is the first output dispatcher the dispatcher we are removing?
+ if (eventSource.m_Dispatchers.m_Listener == listenerToRemove)
+ {
+ eventSource.m_Dispatchers = eventSource.m_Dispatchers.m_Next;
+ }
+ else
+ {
+ // Remove 'listenerToRemove' from the eventSource.m_Dispatchers linked list.
+ EventDispatcher? prev = eventSource.m_Dispatchers;
+ while (true)
+ {
+ EventDispatcher? cur = prev.m_Next;
+ if (cur == null)
+ {
+ Debug.Fail("EventSource did not have a registered EventListener!");
+ break;
+ }
+ if (cur.m_Listener == listenerToRemove)
+ {
+ prev.m_Next = cur.m_Next; // Remove entry.
+ break;
+ }
+ prev = cur;
+ }
+ }
+ }
+ }
+ }
+
+
+ ///
+ /// Checks internal consistency of EventSources/Listeners.
+ ///
+ [Conditional("DEBUG")]
+ internal static void Validate()
+ {
+#if DEBUG
+ // Don't run validation code if we're in the middle of modifying the connections between EventSources and EventListeners.
+ if (s_ConnectingEventSourcesAndListener)
+ {
+ return;
+ }
+#endif
+
+ lock (EventListenersLock)
+ {
+ Debug.Assert(s_EventSources != null);
+ // Get all listeners
+ Dictionary allListeners = new Dictionary();
+ EventListener? cur = s_Listeners;
+ while (cur != null)
+ {
+ allListeners.Add(cur, true);
+ cur = cur.m_Next;
+ }
+
+ // For all eventSources
+ int id = -1;
+ foreach (WeakReference eventSourceRef in s_EventSources)
+ {
+ id++;
+ if (!eventSourceRef.TryGetTarget(out EventSource? eventSource))
+ continue;
+ Debug.Assert(eventSource.m_id == id, "Unexpected event source ID.");
+
+ // None listeners on eventSources exist in the dispatcher list.
+ EventDispatcher? dispatcher = eventSource.m_Dispatchers;
+ while (dispatcher != null)
+ {
+ Debug.Assert(allListeners.ContainsKey(dispatcher.m_Listener), "EventSource has a listener not on the global list.");
+ dispatcher = dispatcher.m_Next;
+ }
+
+ // Every dispatcher is on Dispatcher List of every eventSource.
+ foreach (EventListener listener in allListeners.Keys)
+ {
+ dispatcher = eventSource.m_Dispatchers;
+ while (true)
+ {
+ Debug.Assert(dispatcher != null, "Listener is not on all eventSources.");
+ if (dispatcher.m_Listener == listener)
+ break;
+ dispatcher = dispatcher.m_Next;
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets a global lock that is intended to protect the code:s_Listeners linked list and the
+ /// code:s_EventSources list. (We happen to use the s_EventSources list as the lock object)
+ ///
+ internal static object EventListenersLock
+ {
+ get
+ {
+ if (s_EventSources == null)
+ {
+ Interlocked.CompareExchange(ref s_EventSources, new List>(2), null);
+ }
+ return s_EventSources;
+ }
+ }
+
+ private void CallBackForExistingEventSources(bool addToListenersList, EventHandler? callback)
+ {
+ // Pre-registered EventSources may not have been constructed yet but we need to do so now to ensure they are
+ // reported to the EventListener.
+ EventSourceInitHelper.EnsurePreregisteredEventSourcesExist();
+
+ lock (EventListenersLock)
+ {
+ Debug.Assert(s_EventSources != null);
+
+ // Disallow creating EventListener reentrancy.
+ if (s_CreatingListener)
+ {
+ throw new InvalidOperationException(SR.EventSource_ListenerCreatedInsideCallback);
+ }
+
+ try
+ {
+ s_CreatingListener = true;
+
+ if (addToListenersList)
+ {
+ // Add to list of listeners in the system, do this BEFORE firing the 'OnEventSourceCreated' so that
+ // Those added sources see this listener.
+ this.m_Next = s_Listeners;
+ s_Listeners = this;
+ }
+
+ if (callback != null)
+ {
+ // Find all existing eventSources call OnEventSourceCreated to 'catchup'
+ // Note that we DO have reentrancy here because 'AddListener' calls out to user code (via OnEventSourceCreated callback)
+ // We tolerate this by iterating over a copy of the list here. New event sources will take care of adding listeners themselves
+ // EventSources are not guaranteed to be added at the end of the s_EventSource list -- We re-use slots when a new source
+ // is created.
+ WeakReference[] eventSourcesSnapshot = s_EventSources.ToArray();
+
+#if DEBUG
+ bool previousValue = s_ConnectingEventSourcesAndListener;
+ s_ConnectingEventSourcesAndListener = true;
+ try
+ {
+#endif
+ for (int i = 0; i < eventSourcesSnapshot.Length; i++)
+ {
+ WeakReference eventSourceRef = eventSourcesSnapshot[i];
+ if (eventSourceRef.TryGetTarget(out EventSource? eventSource))
+ {
+ EventSourceCreatedEventArgs args = new EventSourceCreatedEventArgs();
+ args.EventSource = eventSource;
+ callback(this, args);
+ }
+ }
+#if DEBUG
+ }
+ finally
+ {
+ s_ConnectingEventSourcesAndListener = previousValue;
+ }
+#endif
+ }
+
+ Validate();
+ }
+ finally
+ {
+ s_CreatingListener = false;
+ }
+ }
+ }
+
+ // Instance fields
+ internal volatile EventListener? m_Next; // These form a linked list in s_Listeners
+
+ // static fields
+
+ ///
+ /// The list of all listeners in the appdomain. Listeners must be explicitly disposed to remove themselves
+ /// from this list. Note that EventSources point to their listener but NOT the reverse.
+ ///
+ internal static EventListener? s_Listeners;
+ ///
+ /// The list of all active eventSources in the appdomain. Note that eventSources do NOT
+ /// remove themselves from this list this is a weak list and the GC that removes them may
+ /// not have happened yet. Thus it can contain event sources that are dead (thus you have
+ /// to filter those out.
+ ///
+ internal static List>? s_EventSources;
+
+ ///
+ /// Used to disallow reentrancy.
+ ///
+ private static bool s_CreatingListener;
+
+#if DEBUG
+ ///
+ /// Used to disable validation of EventSource and EventListener connectivity.
+ /// This is needed when an EventListener is in the middle of being published to all EventSources
+ /// and another EventSource is created as part of the process.
+ ///
+ [ThreadStatic]
+ private static bool s_ConnectingEventSourcesAndListener;
+#endif
+
+#endregion
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs
index 546bb5874b2343..964766b8ee30ba 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs
@@ -4067,2167 +4067,609 @@ public enum EventSourceSettings
}
///
- /// An EventListener represents a target for the events generated by EventSources (that is subclasses
- /// of ), in the current appdomain. When a new EventListener is created
- /// it is logically attached to all eventSources in that appdomain. When the EventListener is Disposed, then
- /// it is disconnected from the event eventSources. Note that there is a internal list of STRONG references
- /// to EventListeners, which means that relying on the lack of references to EventListeners to clean up
- /// EventListeners will NOT work. You must call EventListener.Dispose explicitly when a dispatcher is no
- /// longer needed.
- ///
- /// Once created, EventListeners can enable or disable on a per-eventSource basis using verbosity levels
- /// ( ) and bitfields ( ) to further restrict the set of
- /// events to be sent to the dispatcher. The dispatcher can also send arbitrary commands to a particular
- /// eventSource using the 'SendCommand' method. The meaning of the commands are eventSource specific.
- ///
- /// The Null Guid (that is (new Guid()) has special meaning as a wildcard for 'all current eventSources in
- /// the appdomain'. Thus it is relatively easy to turn on all events in the appdomain if desired.
- ///
- /// It is possible for there to be many EventListener's defined in a single appdomain. Each dispatcher is
- /// logically independent of the other listeners. Thus when one dispatcher enables or disables events, it
- /// affects only that dispatcher (other listeners get the events they asked for). It is possible that
- /// commands sent with 'SendCommand' would do a semantic operation that would affect the other listeners
- /// (like doing a GC, or flushing data ...), but this is the exception rather than the rule.
- ///
- /// Thus the model is that each EventSource keeps a list of EventListeners that it is sending events
- /// to. Associated with each EventSource-dispatcher pair is a set of filtering criteria that determine for
- /// that eventSource what events that dispatcher will receive.
- ///
- /// Listeners receive the events on their 'OnEventWritten' method. Thus subclasses of EventListener must
- /// override this method to do something useful with the data.
- ///
- /// In addition, when new eventSources are created, the 'OnEventSourceCreate' method is called. The
- /// invariant associated with this callback is that every eventSource gets exactly one
- /// 'OnEventSourceCreate' call for ever eventSource that can potentially send it log messages. In
- /// particular when a EventListener is created, typically a series of OnEventSourceCreate' calls are
- /// made to notify the new dispatcher of all the eventSources that existed before the EventListener was
- /// created.
- ///
+ /// Passed to the code:EventSource.OnEventCommand callback
///
- public abstract class EventListener : IDisposable
+ public class EventCommandEventArgs : EventArgs
{
- private event EventHandler? _EventSourceCreated;
+ ///
+ /// Gets the command for the callback.
+ ///
+ public EventCommand Command { get; internal set; }
///
- /// This event is raised whenever a new eventSource is 'attached' to the dispatcher.
- /// This can happen for all existing EventSources when the EventListener is created
- /// as well as for any EventSources that come into existence after the EventListener
- /// has been created.
- ///
- /// These 'catch up' events are called during the construction of the EventListener.
- /// Subclasses need to be prepared for that.
- ///
- /// In a multi-threaded environment, it is possible that 'EventSourceEventWrittenCallback'
- /// events for a particular eventSource to occur BEFORE the EventSourceCreatedCallback is issued.
+ /// Gets the arguments for the callback.
///
- public event EventHandler? EventSourceCreated
+ public IDictionary? Arguments { get; internal set; }
+
+ ///
+ /// Enables the event that has the specified identifier.
+ ///
+ /// Event ID of event to be enabled
+ /// true if eventId is in range
+ public bool EnableEvent(int eventId)
{
- add
- {
- CallBackForExistingEventSources(false, value);
+ if (Command != EventCommand.Enable && Command != EventCommand.Disable)
+ throw new InvalidOperationException();
+ return eventSource.EnableEventForDispatcher(dispatcher, eventProviderType, eventId, true);
+ }
- this._EventSourceCreated = (EventHandler?)Delegate.Combine(_EventSourceCreated, value);
- }
- remove
- {
- this._EventSourceCreated = (EventHandler?)Delegate.Remove(_EventSourceCreated, value);
- }
+ ///
+ /// Disables the event that have the specified identifier.
+ ///
+ /// Event ID of event to be disabled
+ /// true if eventId is in range
+ public bool DisableEvent(int eventId)
+ {
+ if (Command != EventCommand.Enable && Command != EventCommand.Disable)
+ throw new InvalidOperationException();
+ return eventSource.EnableEventForDispatcher(dispatcher, eventProviderType, eventId, false);
+ }
+
+#region private
+
+ internal EventCommandEventArgs(EventCommand command, IDictionary? arguments, EventSource eventSource,
+ EventListener? listener, EventProviderType eventProviderType, int perEventSourceSessionId, bool enable, EventLevel level, EventKeywords matchAnyKeyword)
+ {
+ this.Command = command;
+ this.Arguments = arguments;
+ this.eventSource = eventSource;
+ this.listener = listener;
+ this.eventProviderType = eventProviderType;
+ this.perEventSourceSessionId = perEventSourceSessionId;
+ this.enable = enable;
+ this.level = level;
+ this.matchAnyKeyword = matchAnyKeyword;
}
+ internal EventSource eventSource;
+ internal EventDispatcher? dispatcher;
+ internal EventProviderType eventProviderType;
+
+ // These are the arguments of sendCommand and are only used for deferring commands until after we are fully initialized.
+ internal EventListener? listener;
+ internal int perEventSourceSessionId;
+ internal bool enable;
+ internal EventLevel level;
+ internal EventKeywords matchAnyKeyword;
+ internal EventCommandEventArgs? nextCommand; // We form a linked list of these deferred commands.
+
+#endregion
+ }
+
+ ///
+ /// EventSourceCreatedEventArgs is passed to
+ ///
+ public class EventSourceCreatedEventArgs : EventArgs
+ {
///
- /// This event is raised whenever an event has been written by a EventSource for which
- /// the EventListener has enabled events.
+ /// The EventSource that is attaching to the listener.
///
- public event EventHandler? EventWritten;
+ public EventSource? EventSource
+ {
+ get;
+ internal set;
+ }
+ }
+
+ ///
+ /// EventWrittenEventArgs is passed to the user-provided override for
+ /// when an event is fired.
+ ///
+ public class EventWrittenEventArgs : EventArgs
+ {
+ private ref EventSource.EventMetadata Metadata => ref CollectionsMarshal.GetValueRefOrNullRef(EventSource.m_eventData!, EventId);
///
- /// Create a new EventListener in which all events start off turned off (use EnableEvents to turn
- /// them on).
+ /// The name of the event.
///
- protected EventListener()
+ public string? EventName
{
- // This will cause the OnEventSourceCreated callback to fire.
- CallBackForExistingEventSources(true, (obj, args) =>
- args.EventSource!.AddListener((EventListener)obj!));
+ get => _moreInfo?.EventName ?? (EventId <= 0 ? null : Metadata.Name);
+ internal set => MoreInfo.EventName = value;
}
///
- /// Dispose should be called when the EventListener no longer desires 'OnEvent*' callbacks. Because
- /// there is an internal list of strong references to all EventListeners, calling 'Dispose' directly
- /// is the only way to actually make the listen die. Thus it is important that users of EventListener
- /// call Dispose when they are done with their logging.
+ /// Gets the event ID for the event that was written.
+ ///
+ public int EventId { get; }
+
+ private Guid _activityId;
+
+ ///
+ /// Gets the activity ID for the thread on which the event was written.
///
- public virtual void Dispose()
+ public Guid ActivityId
{
- lock (EventListenersLock)
+ get
{
- if (s_Listeners != null)
+ if (_activityId == Guid.Empty)
{
- if (this == s_Listeners)
- {
- EventListener cur = s_Listeners;
- s_Listeners = this.m_Next;
- RemoveReferencesToListenerInEventSources(cur);
- }
- else
- {
- // Find 'this' from the s_Listeners linked list.
- EventListener prev = s_Listeners;
- while (true)
- {
- EventListener? cur = prev.m_Next;
- if (cur == null)
- break;
- if (cur == this)
- {
- // Found our Listener, remove references to it in the eventSources
- prev.m_Next = cur.m_Next; // Remove entry.
- RemoveReferencesToListenerInEventSources(cur);
- break;
- }
- prev = cur;
- }
- }
+ _activityId = EventSource.CurrentThreadActivityId;
}
- Validate();
- }
-#if FEATURE_PERFTRACING
- // Remove the listener from the EventPipe dispatcher. EventCommand.Update with enable==false removes it.
- EventPipeEventDispatcher.Instance.SendCommand(this, EventCommand.Update, false, EventLevel.LogAlways, (EventKeywords)0);
-#endif // FEATURE_PERFTRACING
+ return _activityId;
+ }
}
- // We don't expose a Dispose(bool), because the contract is that you don't have any non-syncronous
- // 'cleanup' associated with this object
///
- /// Enable all events from the eventSource identified by 'eventSource' to the current
- /// dispatcher that have a verbosity level of 'level' or lower.
- ///
- /// This call can have the effect of REDUCING the number of events sent to the
- /// dispatcher if 'level' indicates a less verbose level than was previously enabled.
- ///
- /// This call never has an effect on other EventListeners.
- ///
+ /// Gets the related activity ID if one was specified when the event was written.
+ ///
+ public Guid RelatedActivityId => _moreInfo?.RelatedActivityId ?? default;
+
+ ///
+ /// Gets the payload for the event.
+ ///
+ public ReadOnlyCollection? Payload { get; internal set; }
+
+ ///
+ /// Gets the payload argument names.
///
- public void EnableEvents(EventSource eventSource, EventLevel level)
+ public ReadOnlyCollection? PayloadNames
{
- EnableEvents(eventSource, level, EventKeywords.None);
+ get => _moreInfo?.PayloadNames ?? (EventId <= 0 ? null : Metadata.ParameterNames);
+ internal set => MoreInfo.PayloadNames = value;
}
+
///
- /// Enable all events from the eventSource identified by 'eventSource' to the current
- /// dispatcher that have a verbosity level of 'level' or lower and have a event keyword
- /// matching any of the bits in 'matchAnyKeyword'.
- ///
- /// This call can have the effect of REDUCING the number of events sent to the
- /// dispatcher if 'level' indicates a less verbose level than was previously enabled or
- /// if 'matchAnyKeyword' has fewer keywords set than where previously set.
- ///
- /// This call never has an effect on other EventListeners.
+ /// Gets the event source object.
+ ///
+ public EventSource EventSource { get; }
+
+ ///
+ /// Gets the keywords for the event.
///
- public void EnableEvents(EventSource eventSource, EventLevel level, EventKeywords matchAnyKeyword)
+ public EventKeywords Keywords
{
- EnableEvents(eventSource, level, matchAnyKeyword, null);
+ get => EventId <= 0 ? (_moreInfo?.Keywords ?? default) : (EventKeywords)Metadata.Descriptor.Keywords;
+ internal set => MoreInfo.Keywords = value;
}
+
///
- /// Enable all events from the eventSource identified by 'eventSource' to the current
- /// dispatcher that have a verbosity level of 'level' or lower and have a event keyword
- /// matching any of the bits in 'matchAnyKeyword' as well as any (eventSource specific)
- /// effect passing additional 'key-value' arguments 'arguments' might have.
- ///
- /// This call can have the effect of REDUCING the number of events sent to the
- /// dispatcher if 'level' indicates a less verbose level than was previously enabled or
- /// if 'matchAnyKeyword' has fewer keywords set than where previously set.
- ///
- /// This call never has an effect on other EventListeners.
+ /// Gets the operation code for the event.
///
- public void EnableEvents(EventSource eventSource, EventLevel level, EventKeywords matchAnyKeyword, IDictionary? arguments)
+ public EventOpcode Opcode
{
- ArgumentNullException.ThrowIfNull(eventSource);
+ get => EventId <= 0 ? (_moreInfo?.Opcode ?? default) : (EventOpcode)Metadata.Descriptor.Opcode;
+ internal set => MoreInfo.Opcode = value;
+ }
- eventSource.SendCommand(this, EventProviderType.None, 0, EventCommand.Update, true, level, matchAnyKeyword, arguments);
+ ///
+ /// Gets the task for the event.
+ ///
+ public EventTask Task => EventId <= 0 ? EventTask.None : (EventTask)Metadata.Descriptor.Task;
-#if FEATURE_PERFTRACING
- if (eventSource.GetType() == typeof(NativeRuntimeEventSource))
- {
- EventPipeEventDispatcher.Instance.SendCommand(this, EventCommand.Update, true, level, matchAnyKeyword);
- }
-#endif // FEATURE_PERFTRACING
- }
///
- /// Disables all events coming from eventSource identified by 'eventSource'.
- ///
- /// This call never has an effect on other EventListeners.
+ /// Any provider/user defined options associated with the event.
///
- public void DisableEvents(EventSource eventSource)
+ public EventTags Tags
{
- ArgumentNullException.ThrowIfNull(eventSource);
-
- eventSource.SendCommand(this, EventProviderType.None, 0, EventCommand.Update, false, EventLevel.LogAlways, EventKeywords.None, null);
+ get => EventId <= 0 ? (_moreInfo?.Tags ?? default) : Metadata.Tags;
+ internal set => MoreInfo.Tags = value;
+ }
-#if FEATURE_PERFTRACING
- if (eventSource.GetType() == typeof(NativeRuntimeEventSource))
- {
- EventPipeEventDispatcher.Instance.SendCommand(this, EventCommand.Update, false, EventLevel.LogAlways, EventKeywords.None);
- }
-#endif // FEATURE_PERFTRACING
+ ///
+ /// Gets the message for the event. If the message has {N} parameters they are NOT substituted.
+ ///
+ public string? Message
+ {
+ get => _moreInfo?.Message ?? (EventId <= 0 ? null : Metadata.Message);
+ internal set => MoreInfo.Message = value;
}
///
- /// EventSourceIndex is small non-negative integer (suitable for indexing in an array)
- /// identifying EventSource. It is unique per-appdomain. Some EventListeners might find
- /// it useful to store additional information about each eventSource connected to it,
- /// and EventSourceIndex allows this extra information to be efficiently stored in a
- /// (growable) array (eg List(T)).
+ /// Gets the channel for the event.
///
- protected internal static int EventSourceIndex(EventSource eventSource) { return eventSource.m_id; }
+ public EventChannel Channel => EventId <= 0 ? EventChannel.None : (EventChannel)Metadata.Descriptor.Channel;
///
- /// This method is called whenever a new eventSource is 'attached' to the dispatcher.
- /// This can happen for all existing EventSources when the EventListener is created
- /// as well as for any EventSources that come into existence after the EventListener
- /// has been created.
- ///
- /// These 'catch up' events are called during the construction of the EventListener.
- /// Subclasses need to be prepared for that.
- ///
- /// In a multi-threaded environment, it is possible that 'OnEventWritten' callbacks
- /// for a particular eventSource to occur BEFORE the OnEventSourceCreated is issued.
+ /// Gets the version of the event.
///
- ///
- protected internal virtual void OnEventSourceCreated(EventSource eventSource)
- {
- EventHandler? callBack = this._EventSourceCreated;
- if (callBack != null)
- {
- EventSourceCreatedEventArgs args = new EventSourceCreatedEventArgs();
- args.EventSource = eventSource;
- callBack(this, args);
- }
- }
+ public byte Version => EventId <= 0 ? (byte)0 : Metadata.Descriptor.Version;
///
- /// This method is called whenever an event has been written by a EventSource for which
- /// the EventListener has enabled events.
+ /// Gets the level for the event.
///
- ///
- protected internal virtual void OnEventWritten(EventWrittenEventArgs eventData)
+ public EventLevel Level
{
- this.EventWritten?.Invoke(this, eventData);
+ get => EventId <= 0 ? (_moreInfo?.Level ?? default) : (EventLevel)Metadata.Descriptor.Level;
+ internal set => MoreInfo.Level = value;
}
-#region private
///
- /// This routine adds newEventSource to the global list of eventSources, it also assigns the
- /// ID to the eventSource (which is simply the ordinal in the global list).
- ///
- /// EventSources currently do not pro-actively remove themselves from this list. Instead
- /// when eventSources's are GCed, the weak handle in this list naturally gets nulled, and
- /// we will reuse the slot. Today this list never shrinks (but we do reuse entries
- /// that are in the list). This seems OK since the expectation is that EventSources
- /// tend to live for the lifetime of the appdomain anyway (they tend to be used in
- /// global variables).
+ /// Gets the identifier for the OS thread that wrote the event.
///
- ///
- internal static void AddEventSource(EventSource newEventSource)
+ public long OSThreadId
{
- lock (EventListenersLock)
+ get
{
- Debug.Assert(s_EventSources != null);
-
- // Periodically search the list for existing entries to reuse, this avoids
- // unbounded memory use if we keep recycling eventSources (an unlikely thing).
- int newIndex = -1;
- if (s_EventSources.Count % 64 == 63) // on every block of 64, fill up the block before continuing
- {
- int i = s_EventSources.Count; // Work from the top down.
- while (0 < i)
- {
- --i;
- WeakReference weakRef = s_EventSources[i];
- if (!weakRef.TryGetTarget(out _))
- {
- newIndex = i;
- weakRef.SetTarget(newEventSource);
- break;
- }
- }
- }
- if (newIndex < 0)
- {
- newIndex = s_EventSources.Count;
- s_EventSources.Add(new WeakReference(newEventSource));
- }
- newEventSource.m_id = newIndex;
-
-#if DEBUG
- // Disable validation of EventSource/EventListener connections in case a call to EventSource.AddListener
- // causes a recursive call into this method.
- bool previousValue = s_ConnectingEventSourcesAndListener;
- s_ConnectingEventSourcesAndListener = true;
- try
- {
-#endif
- // Add every existing dispatcher to the new EventSource
- for (EventListener? listener = s_Listeners; listener != null; listener = listener.m_Next)
- newEventSource.AddListener(listener);
-#if DEBUG
- }
- finally
+ ref long? osThreadId = ref MoreInfo.OsThreadId;
+ if (!osThreadId.HasValue)
{
- s_ConnectingEventSourcesAndListener = previousValue;
+ osThreadId = (long)Thread.CurrentOSThreadId;
}
-#endif
- Validate();
+ return osThreadId.Value;
}
+ internal set => MoreInfo.OsThreadId = value;
+ }
+
+ ///
+ /// Gets a UTC DateTime that specifies when the event was written.
+ ///
+ public DateTime TimeStamp { get; internal set; }
+
+ internal EventWrittenEventArgs(EventSource eventSource, int eventId)
+ {
+ EventSource = eventSource;
+ EventId = eventId;
+ TimeStamp = DateTime.UtcNow;
}
- // Whenever we have async callbacks from native code, there is an ugly issue where
- // during .NET shutdown native code could be calling the callback, but the CLR
- // has already prohibited callbacks to managed code in the appdomain, causing the CLR
- // to throw a COMPLUS_BOOT_EXCEPTION. The guideline we give is that you must unregister
- // such callbacks on process shutdown or appdomain so that unmanaged code will never
- // do this. This is what this callback is for.
- // See bug 724140 for more
- internal static void DisposeOnShutdown()
+ internal unsafe EventWrittenEventArgs(EventSource eventSource, int eventId, Guid* pActivityID, Guid* pChildActivityID)
+ : this(eventSource, eventId)
{
- Debug.Assert(EventSource.IsSupported);
- List sourcesToDispose = new List();
- lock (EventListenersLock)
+ if (pActivityID != null)
{
- Debug.Assert(s_EventSources != null);
- foreach (WeakReference esRef in s_EventSources)
- {
- if (esRef.TryGetTarget(out EventSource? es))
- {
- sourcesToDispose.Add(es);
- }
- }
+ _activityId = *pActivityID;
}
- // Do not invoke Dispose under the lock as this can lead to a deadlock.
- // See https://github.com/dotnet/runtime/issues/48342 for details.
- Debug.Assert(!Monitor.IsEntered(EventListenersLock));
- foreach (EventSource es in sourcesToDispose)
+ if (pChildActivityID != null)
{
- es.Dispose();
+ MoreInfo.RelatedActivityId = *pChildActivityID;
}
}
- // If an EventListener calls Dispose without calling DisableEvents first we want to issue the Disable command now
- private static void CallDisableEventsIfNecessary(EventDispatcher eventDispatcher, EventSource eventSource)
- {
-#if DEBUG
- // Disable validation of EventSource/EventListener connections in case a call to EventSource.AddListener
- // causes a recursive call into this method.
- bool previousValue = s_ConnectingEventSourcesAndListener;
- s_ConnectingEventSourcesAndListener = true;
- try
- {
-#endif
- if (eventDispatcher.m_EventEnabled == null)
- {
- return;
- }
+ private MoreEventInfo? _moreInfo;
+ private MoreEventInfo MoreInfo => _moreInfo ??= new MoreEventInfo();
- foreach (bool value in eventDispatcher.m_EventEnabled.Values)
- {
- if (value)
- {
- eventDispatcher.m_Listener.DisableEvents(eventSource);
- }
- }
-#if DEBUG
- }
- finally
- {
- s_ConnectingEventSourcesAndListener = previousValue;
- }
-#endif
+ private sealed class MoreEventInfo
+ {
+ public string? Message;
+ public string? EventName;
+ public ReadOnlyCollection? PayloadNames;
+ public Guid RelatedActivityId;
+ public long? OsThreadId;
+ public EventTags Tags;
+ public EventOpcode Opcode;
+ public EventLevel Level;
+ public EventKeywords Keywords;
}
+ }
+ ///
+ /// Allows customizing defaults and specifying localization support for the event source class to which it is applied.
+ ///
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class EventSourceAttribute : Attribute
+ {
///
- /// Helper used in code:Dispose that removes any references to 'listenerToRemove' in any of the
- /// eventSources in the appdomain.
- ///
- /// The EventListenersLock must be held before calling this routine.
+ /// Overrides the ETW name of the event source (which defaults to the class name)
///
- private static void RemoveReferencesToListenerInEventSources(EventListener listenerToRemove)
- {
- Debug.Assert(Monitor.IsEntered(EventListenersLock));
- // Foreach existing EventSource in the appdomain
- Debug.Assert(s_EventSources != null);
-
- // First pass to call DisableEvents
- WeakReference[] eventSourcesSnapshot = s_EventSources.ToArray();
- foreach (WeakReference eventSourceRef in eventSourcesSnapshot)
- {
- if (eventSourceRef.TryGetTarget(out EventSource? eventSource))
- {
- EventDispatcher? cur = eventSource.m_Dispatchers;
- while (cur != null)
- {
- if (cur.m_Listener == listenerToRemove)
- {
- CallDisableEventsIfNecessary(cur, eventSource);
- }
-
- cur = cur.m_Next;
- }
- }
- }
-
- // DisableEvents can call back to user code and we have to start over since s_EventSources and
- // eventSource.m_Dispatchers could have mutated
- foreach (WeakReference eventSourceRef in s_EventSources)
- {
- if (eventSourceRef.TryGetTarget(out EventSource? eventSource)
- && eventSource.m_Dispatchers != null)
- {
- // Is the first output dispatcher the dispatcher we are removing?
- if (eventSource.m_Dispatchers.m_Listener == listenerToRemove)
- {
- eventSource.m_Dispatchers = eventSource.m_Dispatchers.m_Next;
- }
- else
- {
- // Remove 'listenerToRemove' from the eventSource.m_Dispatchers linked list.
- EventDispatcher? prev = eventSource.m_Dispatchers;
- while (true)
- {
- EventDispatcher? cur = prev.m_Next;
- if (cur == null)
- {
- Debug.Fail("EventSource did not have a registered EventListener!");
- break;
- }
- if (cur.m_Listener == listenerToRemove)
- {
- prev.m_Next = cur.m_Next; // Remove entry.
- break;
- }
- prev = cur;
- }
- }
- }
- }
- }
-
+ public string? Name { get; set; }
///
- /// Checks internal consistency of EventSources/Listeners.
+ /// Overrides the default (calculated) Guid of an EventSource type. Explicitly defining a GUID is discouraged,
+ /// except when upgrading existing ETW providers to using event sources.
///
- [Conditional("DEBUG")]
- internal static void Validate()
- {
-#if DEBUG
- // Don't run validation code if we're in the middle of modifying the connections between EventSources and EventListeners.
- if (s_ConnectingEventSourcesAndListener)
- {
- return;
- }
-#endif
-
- lock (EventListenersLock)
- {
- Debug.Assert(s_EventSources != null);
- // Get all listeners
- Dictionary allListeners = new Dictionary();
- EventListener? cur = s_Listeners;
- while (cur != null)
- {
- allListeners.Add(cur, true);
- cur = cur.m_Next;
- }
-
- // For all eventSources
- int id = -1;
- foreach (WeakReference eventSourceRef in s_EventSources)
- {
- id++;
- if (!eventSourceRef.TryGetTarget(out EventSource? eventSource))
- continue;
- Debug.Assert(eventSource.m_id == id, "Unexpected event source ID.");
-
- // None listeners on eventSources exist in the dispatcher list.
- EventDispatcher? dispatcher = eventSource.m_Dispatchers;
- while (dispatcher != null)
- {
- Debug.Assert(allListeners.ContainsKey(dispatcher.m_Listener), "EventSource has a listener not on the global list.");
- dispatcher = dispatcher.m_Next;
- }
-
- // Every dispatcher is on Dispatcher List of every eventSource.
- foreach (EventListener listener in allListeners.Keys)
- {
- dispatcher = eventSource.m_Dispatchers;
- while (true)
- {
- Debug.Assert(dispatcher != null, "Listener is not on all eventSources.");
- if (dispatcher.m_Listener == listener)
- break;
- dispatcher = dispatcher.m_Next;
- }
- }
- }
- }
- }
+ public string? Guid { get; set; }
///
- /// Gets a global lock that is intended to protect the code:s_Listeners linked list and the
- /// code:s_EventSources list. (We happen to use the s_EventSources list as the lock object)
+ ///
+ /// EventSources support localization of events. The names used for events, opcodes, tasks, keywords and maps
+ /// can be localized to several languages if desired. This works by creating a ResX style string table
+ /// (by simply adding a 'Resource File' to your project). This resource file is given a name e.g.
+ /// 'DefaultNameSpace.ResourceFileName' which can be passed to the ResourceManager constructor to read the
+ /// resources. This name is the value of the LocalizationResources property.
+ ///
+ /// If LocalizationResources property is non-null, then EventSource will look up the localized strings for events by
+ /// using the following resource naming scheme
+ ///
+ /// * event_EVENTNAME
+ /// * task_TASKNAME
+ /// * keyword_KEYWORDNAME
+ /// * map_MAPNAME
+ ///
+ /// where the capitalized name is the name of the event, task, keyword, or map value that should be localized.
+ /// Note that the localized string for an event corresponds to the Message string, and can have {0} values
+ /// which represent the payload values.
+ ///
///
- internal static object EventListenersLock
+ public string? LocalizationResources { get; set; }
+ }
+
+ ///
+ /// Any instance methods in a class that subclasses and that return void are
+ /// assumed by default to be methods that generate an ETW event. Enough information can be deduced from the
+ /// name of the method and its signature to generate basic schema information for the event. The
+ /// class allows you to specify additional event schema information for an event if
+ /// desired.
+ ///
+ [AttributeUsage(AttributeTargets.Method)]
+ public sealed class EventAttribute : Attribute
+ {
+ /// Construct an EventAttribute with specified eventId
+ /// ID of the ETW event (an integer between 1 and 65535)
+ public EventAttribute(int eventId)
{
- get
- {
- if (s_EventSources == null)
- {
- Interlocked.CompareExchange(ref s_EventSources, new List>(2), null);
- }
- return s_EventSources;
- }
+ EventId = eventId;
+ Level = EventLevel.Informational;
}
- private void CallBackForExistingEventSources(bool addToListenersList, EventHandler? callback)
+ /// Event's ID
+ public int EventId { get; }
+ /// Event's severity level: indicates the severity or verbosity of the event
+ public EventLevel Level { get; set; }
+ /// Event's keywords: allows classification of events by "categories"
+ public EventKeywords Keywords { get; set; }
+ /// Event's operation code: allows defining operations, generally used with Tasks
+ public EventOpcode Opcode
{
- // Pre-registered EventSources may not have been constructed yet but we need to do so now to ensure they are
- // reported to the EventListener.
- EventSourceInitHelper.EnsurePreregisteredEventSourcesExist();
-
- lock (EventListenersLock)
+ get => m_opcode;
+ set
{
- Debug.Assert(s_EventSources != null);
-
- // Disallow creating EventListener reentrancy.
- if (s_CreatingListener)
- {
- throw new InvalidOperationException(SR.EventSource_ListenerCreatedInsideCallback);
- }
-
- try
- {
- s_CreatingListener = true;
-
- if (addToListenersList)
- {
- // Add to list of listeners in the system, do this BEFORE firing the 'OnEventSourceCreated' so that
- // Those added sources see this listener.
- this.m_Next = s_Listeners;
- s_Listeners = this;
- }
-
- if (callback != null)
- {
- // Find all existing eventSources call OnEventSourceCreated to 'catchup'
- // Note that we DO have reentrancy here because 'AddListener' calls out to user code (via OnEventSourceCreated callback)
- // We tolerate this by iterating over a copy of the list here. New event sources will take care of adding listeners themselves
- // EventSources are not guaranteed to be added at the end of the s_EventSource list -- We re-use slots when a new source
- // is created.
- WeakReference[] eventSourcesSnapshot = s_EventSources.ToArray();
-
-#if DEBUG
- bool previousValue = s_ConnectingEventSourcesAndListener;
- s_ConnectingEventSourcesAndListener = true;
- try
- {
-#endif
- for (int i = 0; i < eventSourcesSnapshot.Length; i++)
- {
- WeakReference eventSourceRef = eventSourcesSnapshot[i];
- if (eventSourceRef.TryGetTarget(out EventSource? eventSource))
- {
- EventSourceCreatedEventArgs args = new EventSourceCreatedEventArgs();
- args.EventSource = eventSource;
- callback(this, args);
- }
- }
-#if DEBUG
- }
- finally
- {
- s_ConnectingEventSourcesAndListener = previousValue;
- }
-#endif
- }
-
- Validate();
- }
- finally
- {
- s_CreatingListener = false;
- }
+ m_opcode = value;
+ m_opcodeSet = true;
}
}
- // Instance fields
- internal volatile EventListener? m_Next; // These form a linked list in s_Listeners
+ internal bool IsOpcodeSet => m_opcodeSet;
- // static fields
+ /// Event's task: allows logical grouping of events
+ public EventTask Task { get; set; }
+
+ /// Event's channel: defines an event log as an additional destination for the event
+ public EventChannel Channel { get; set; }
+
+ /// Event's version
+ public byte Version { get; set; }
///
- /// The list of all listeners in the appdomain. Listeners must be explicitly disposed to remove themselves
- /// from this list. Note that EventSources point to their listener but NOT the reverse.
- ///
- internal static EventListener? s_Listeners;
- ///
- /// The list of all active eventSources in the appdomain. Note that eventSources do NOT
- /// remove themselves from this list this is a weak list and the GC that removes them may
- /// not have happened yet. Thus it can contain event sources that are dead (thus you have
- /// to filter those out.
+ /// This can be specified to enable formatting and localization of the event's payload. You can
+ /// use standard .NET substitution operators (eg {1}) in the string and they will be replaced
+ /// with the 'ToString()' of the corresponding part of the event payload.
///
- internal static List>? s_EventSources;
+ public string? Message { get; set; }
///
- /// Used to disallow reentrancy.
+ /// User defined options associated with the event. These do not have meaning to the EventSource but
+ /// are passed through to listeners which given them semantics.
///
- private static bool s_CreatingListener;
+ public EventTags Tags { get; set; }
-#if DEBUG
///
- /// Used to disable validation of EventSource and EventListener connectivity.
- /// This is needed when an EventListener is in the middle of being published to all EventSources
- /// and another EventSource is created as part of the process.
+ /// Allows fine control over the Activity IDs generated by start and stop events
///
- [ThreadStatic]
- private static bool s_ConnectingEventSourcesAndListener;
-#endif
+ public EventActivityOptions ActivityOptions { get; set; }
+#region private
+ private EventOpcode m_opcode;
+ private bool m_opcodeSet;
#endregion
}
///
- /// Passed to the code:EventSource.OnEventCommand callback
+ /// By default all instance methods in a class that subclasses code:EventSource that and return
+ /// void are assumed to be methods that generate an event. This default can be overridden by specifying
+ /// the code:NonEventAttribute
///
- public class EventCommandEventArgs : EventArgs
+ [AttributeUsage(AttributeTargets.Method)]
+ public sealed class NonEventAttribute : Attribute
{
///
- /// Gets the command for the callback.
- ///
- public EventCommand Command { get; internal set; }
-
- ///
- /// Gets the arguments for the callback.
+ /// Constructs a default NonEventAttribute
///
- public IDictionary? Arguments { get; internal set; }
+ public NonEventAttribute() { }
+ }
+ ///
+ /// EventChannelAttribute allows customizing channels supported by an EventSource. This attribute must be
+ /// applied to an member of type EventChannel defined in a Channels class nested in the EventSource class:
+ ///
+ /// public static class Channels
+ /// {
+ /// [Channel(Enabled = true, EventChannelType = EventChannelType.Admin)]
+ /// public const EventChannel Admin = (EventChannel)16;
+ ///
+ /// [Channel(Enabled = false, EventChannelType = EventChannelType.Operational)]
+ /// public const EventChannel Operational = (EventChannel)17;
+ /// }
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ internal sealed class EventChannelAttribute : Attribute
+ {
///
- /// Enables the event that has the specified identifier.
+ /// Specified whether the channel is enabled by default
///
- /// Event ID of event to be enabled
- /// true if eventId is in range
- public bool EnableEvent(int eventId)
- {
- if (Command != EventCommand.Enable && Command != EventCommand.Disable)
- throw new InvalidOperationException();
- return eventSource.EnableEventForDispatcher(dispatcher, eventProviderType, eventId, true);
- }
+ public bool Enabled { get; set; }
///
- /// Disables the event that have the specified identifier.
+ /// Legal values are in EventChannelType
///
- /// Event ID of event to be disabled
- /// true if eventId is in range
- public bool DisableEvent(int eventId)
- {
- if (Command != EventCommand.Enable && Command != EventCommand.Disable)
- throw new InvalidOperationException();
- return eventSource.EnableEventForDispatcher(dispatcher, eventProviderType, eventId, false);
- }
+ public EventChannelType EventChannelType { get; set; }
-#region private
-
- internal EventCommandEventArgs(EventCommand command, IDictionary? arguments, EventSource eventSource,
- EventListener? listener, EventProviderType eventProviderType, int perEventSourceSessionId, bool enable, EventLevel level, EventKeywords matchAnyKeyword)
- {
- this.Command = command;
- this.Arguments = arguments;
- this.eventSource = eventSource;
- this.listener = listener;
- this.eventProviderType = eventProviderType;
- this.perEventSourceSessionId = perEventSourceSessionId;
- this.enable = enable;
- this.level = level;
- this.matchAnyKeyword = matchAnyKeyword;
- }
-
- internal EventSource eventSource;
- internal EventDispatcher? dispatcher;
- internal EventProviderType eventProviderType;
-
- // These are the arguments of sendCommand and are only used for deferring commands until after we are fully initialized.
- internal EventListener? listener;
- internal int perEventSourceSessionId;
- internal bool enable;
- internal EventLevel level;
- internal EventKeywords matchAnyKeyword;
- internal EventCommandEventArgs? nextCommand; // We form a linked list of these deferred commands.
-
-#endregion
+ // TODO: there is a convention that the name is the Provider/Type Should we provide an override?
+ // public string Name { get; set; }
}
///
- /// EventSourceCreatedEventArgs is passed to
+ /// Allowed channel types
///
- public class EventSourceCreatedEventArgs : EventArgs
+ internal enum EventChannelType
{
- ///
- /// The EventSource that is attaching to the listener.
- ///
- public EventSource? EventSource
- {
- get;
- internal set;
- }
+ /// The admin channel
+ Admin = 1,
+ /// The operational channel
+ Operational,
+ /// The Analytic channel
+ Analytic,
+ /// The debug channel
+ Debug,
}
///
- /// EventWrittenEventArgs is passed to the user-provided override for
- /// when an event is fired.
+ /// Describes the pre-defined command (EventCommandEventArgs.Command property) that is passed to the OnEventCommand callback.
///
- public class EventWrittenEventArgs : EventArgs
+ public enum EventCommand
{
- private ref EventSource.EventMetadata Metadata => ref CollectionsMarshal.GetValueRefOrNullRef(EventSource.m_eventData!, EventId);
-
- ///
- /// The name of the event.
- ///
- public string? EventName
- {
- get => _moreInfo?.EventName ?? (EventId <= 0 ? null : Metadata.Name);
- internal set => MoreInfo.EventName = value;
- }
-
///
- /// Gets the event ID for the event that was written.
+ /// Update EventSource state
///
- public int EventId { get; }
-
- private Guid _activityId;
-
+ Update = 0,
///
- /// Gets the activity ID for the thread on which the event was written.
+ /// Request EventSource to generate and send its manifest
///
- public Guid ActivityId
- {
- get
- {
- if (_activityId == Guid.Empty)
- {
- _activityId = EventSource.CurrentThreadActivityId;
- }
-
- return _activityId;
- }
- }
-
+ SendManifest = -1,
///
- /// Gets the related activity ID if one was specified when the event was written.
+ /// Enable event
///
- public Guid RelatedActivityId => _moreInfo?.RelatedActivityId ?? default;
-
+ Enable = -2,
///
- /// Gets the payload for the event.
+ /// Disable event
///
- public ReadOnlyCollection? Payload { get; internal set; }
+ Disable = -3
+ }
- ///
- /// Gets the payload argument names.
- ///
- public ReadOnlyCollection? PayloadNames
- {
- get => _moreInfo?.PayloadNames ?? (EventId <= 0 ? null : Metadata.ParameterNames);
- internal set => MoreInfo.PayloadNames = value;
- }
+#region private classes
- ///
- /// Gets the event source object.
- ///
- public EventSource EventSource { get; }
+ // holds a bitfield representing a session mask
+ ///
+ /// A SessionMask represents a set of (at most MAX) sessions as a bit mask. The perEventSourceSessionId
+ /// is the index in the SessionMask of the bit that will be set. These can translate to
+ /// EventSource's reserved keywords bits using the provided ToEventKeywords() and
+ /// FromEventKeywords() methods.
+ ///
+ internal struct SessionMask
+ {
+ public SessionMask(SessionMask m)
+ { m_mask = m.m_mask; }
- ///
- /// Gets the keywords for the event.
- ///
- public EventKeywords Keywords
- {
- get => EventId <= 0 ? (_moreInfo?.Keywords ?? default) : (EventKeywords)Metadata.Descriptor.Keywords;
- internal set => MoreInfo.Keywords = value;
- }
+ public SessionMask(uint mask = 0)
+ { m_mask = mask & MASK; }
- ///
- /// Gets the operation code for the event.
- ///
- public EventOpcode Opcode
+ public bool IsEqualOrSupersetOf(SessionMask m)
{
- get => EventId <= 0 ? (_moreInfo?.Opcode ?? default) : (EventOpcode)Metadata.Descriptor.Opcode;
- internal set => MoreInfo.Opcode = value;
+ return (this.m_mask | m.m_mask) == this.m_mask;
}
- ///
- /// Gets the task for the event.
- ///
- public EventTask Task => EventId <= 0 ? EventTask.None : (EventTask)Metadata.Descriptor.Task;
+ public static SessionMask All => new SessionMask(MASK);
- ///
- /// Any provider/user defined options associated with the event.
- ///
- public EventTags Tags
+ public static SessionMask FromId(int perEventSourceSessionId)
{
- get => EventId <= 0 ? (_moreInfo?.Tags ?? default) : Metadata.Tags;
- internal set => MoreInfo.Tags = value;
+ Debug.Assert(perEventSourceSessionId < MAX);
+ return new SessionMask((uint)1 << perEventSourceSessionId);
}
- ///
- /// Gets the message for the event. If the message has {N} parameters they are NOT substituted.
- ///
- public string? Message
+ public ulong ToEventKeywords()
{
- get => _moreInfo?.Message ?? (EventId <= 0 ? null : Metadata.Message);
- internal set => MoreInfo.Message = value;
+ return (ulong)m_mask << SHIFT_SESSION_TO_KEYWORD;
}
- ///
- /// Gets the channel for the event.
- ///
- public EventChannel Channel => EventId <= 0 ? EventChannel.None : (EventChannel)Metadata.Descriptor.Channel;
-
- ///
- /// Gets the version of the event.
- ///
- public byte Version => EventId <= 0 ? (byte)0 : Metadata.Descriptor.Version;
-
- ///
- /// Gets the level for the event.
- ///
- public EventLevel Level
+ public static SessionMask FromEventKeywords(ulong m)
{
- get => EventId <= 0 ? (_moreInfo?.Level ?? default) : (EventLevel)Metadata.Descriptor.Level;
- internal set => MoreInfo.Level = value;
+ return new SessionMask((uint)(m >> SHIFT_SESSION_TO_KEYWORD));
}
- ///
- /// Gets the identifier for the OS thread that wrote the event.
- ///
- public long OSThreadId
+ public bool this[int perEventSourceSessionId]
{
get
{
- ref long? osThreadId = ref MoreInfo.OsThreadId;
- if (!osThreadId.HasValue)
- {
- osThreadId = (long)Thread.CurrentOSThreadId;
- }
-
- return osThreadId.Value;
+ Debug.Assert(perEventSourceSessionId < MAX);
+ return (m_mask & (1 << perEventSourceSessionId)) != 0;
+ }
+ set
+ {
+ Debug.Assert(perEventSourceSessionId < MAX);
+ if (value) m_mask |= ((uint)1 << perEventSourceSessionId);
+ else m_mask &= ~((uint)1 << perEventSourceSessionId);
}
- internal set => MoreInfo.OsThreadId = value;
}
- ///
- /// Gets a UTC DateTime that specifies when the event was written.
- ///
- public DateTime TimeStamp { get; internal set; }
+ public static SessionMask operator |(SessionMask m1, SessionMask m2) =>
+ new SessionMask(m1.m_mask | m2.m_mask);
- internal EventWrittenEventArgs(EventSource eventSource, int eventId)
- {
- EventSource = eventSource;
- EventId = eventId;
- TimeStamp = DateTime.UtcNow;
- }
+ public static SessionMask operator &(SessionMask m1, SessionMask m2) =>
+ new SessionMask(m1.m_mask & m2.m_mask);
- internal unsafe EventWrittenEventArgs(EventSource eventSource, int eventId, Guid* pActivityID, Guid* pChildActivityID)
- : this(eventSource, eventId)
- {
- if (pActivityID != null)
- {
- _activityId = *pActivityID;
- }
+ public static SessionMask operator ^(SessionMask m1, SessionMask m2) =>
+ new SessionMask(m1.m_mask ^ m2.m_mask);
- if (pChildActivityID != null)
- {
- MoreInfo.RelatedActivityId = *pChildActivityID;
- }
- }
+ public static SessionMask operator ~(SessionMask m) =>
+ new SessionMask(MASK & ~(m.m_mask));
- private MoreEventInfo? _moreInfo;
- private MoreEventInfo MoreInfo => _moreInfo ??= new MoreEventInfo();
+ public static explicit operator ulong(SessionMask m) => m.m_mask;
- private sealed class MoreEventInfo
- {
- public string? Message;
- public string? EventName;
- public ReadOnlyCollection? PayloadNames;
- public Guid RelatedActivityId;
- public long? OsThreadId;
- public EventTags Tags;
- public EventOpcode Opcode;
- public EventLevel Level;
- public EventKeywords Keywords;
- }
+ public static explicit operator uint(SessionMask m) => m.m_mask;
+
+ private uint m_mask;
+
+ internal const int SHIFT_SESSION_TO_KEYWORD = 44; // bits 44-47 inclusive are reserved
+ internal const uint MASK = 0x0fU; // the mask of 4 reserved bits
+ internal const uint MAX = 4; // maximum number of simultaneous ETW sessions supported
}
///
- /// Allows customizing defaults and specifying localization support for the event source class to which it is applied.
- ///
- [AttributeUsage(AttributeTargets.Class)]
- public sealed class EventSourceAttribute : Attribute
- {
- ///
- /// Overrides the ETW name of the event source (which defaults to the class name)
- ///
- public string? Name { get; set; }
-
- ///
- /// Overrides the default (calculated) Guid of an EventSource type. Explicitly defining a GUID is discouraged,
- /// except when upgrading existing ETW providers to using event sources.
- ///
- public string? Guid { get; set; }
-
- ///
- ///
- /// EventSources support localization of events. The names used for events, opcodes, tasks, keywords and maps
- /// can be localized to several languages if desired. This works by creating a ResX style string table
- /// (by simply adding a 'Resource File' to your project). This resource file is given a name e.g.
- /// 'DefaultNameSpace.ResourceFileName' which can be passed to the ResourceManager constructor to read the
- /// resources. This name is the value of the LocalizationResources property.
- ///
- /// If LocalizationResources property is non-null, then EventSource will look up the localized strings for events by
- /// using the following resource naming scheme
- ///
- /// * event_EVENTNAME
- /// * task_TASKNAME
- /// * keyword_KEYWORDNAME
- /// * map_MAPNAME
- ///
- /// where the capitalized name is the name of the event, task, keyword, or map value that should be localized.
- /// Note that the localized string for an event corresponds to the Message string, and can have {0} values
- /// which represent the payload values.
- ///
- ///
- public string? LocalizationResources { get; set; }
- }
-
- ///
- /// Any instance methods in a class that subclasses and that return void are
- /// assumed by default to be methods that generate an ETW event. Enough information can be deduced from the
- /// name of the method and its signature to generate basic schema information for the event. The
- /// class allows you to specify additional event schema information for an event if
- /// desired.
+ /// code:EventDispatchers are a simple 'helper' structure that holds the filtering state
+ /// (m_EventEnabled) for a particular EventSource X EventListener tuple
+ ///
+ /// Thus a single EventListener may have many EventDispatchers (one for every EventSource
+ /// that EventListener has activate) and a Single EventSource may also have many
+ /// event Dispatchers (one for every EventListener that has activated it).
+ ///
+ /// Logically a particular EventDispatcher belongs to exactly one EventSource and exactly
+ /// one EventListener (although EventDispatcher does not 'remember' the EventSource it is
+ /// associated with.
///
- [AttributeUsage(AttributeTargets.Method)]
- public sealed class EventAttribute : Attribute
+ internal sealed class EventDispatcher
{
- /// Construct an EventAttribute with specified eventId
- /// ID of the ETW event (an integer between 1 and 65535)
- public EventAttribute(int eventId)
- {
- EventId = eventId;
- Level = EventLevel.Informational;
- }
-
- /// Event's ID
- public int EventId { get; }
- /// Event's severity level: indicates the severity or verbosity of the event
- public EventLevel Level { get; set; }
- /// Event's keywords: allows classification of events by "categories"
- public EventKeywords Keywords { get; set; }
- /// Event's operation code: allows defining operations, generally used with Tasks
- public EventOpcode Opcode
+ internal EventDispatcher(EventDispatcher? next, Dictionary? eventEnabled, EventListener listener)
{
- get => m_opcode;
- set
- {
- m_opcode = value;
- m_opcodeSet = true;
- }
+ m_Next = next;
+ m_EventEnabled = eventEnabled;
+ m_Listener = listener;
}
- internal bool IsOpcodeSet => m_opcodeSet;
-
- /// Event's task: allows logical grouping of events
- public EventTask Task { get; set; }
-
- /// Event's channel: defines an event log as an additional destination for the event
- public EventChannel Channel { get; set; }
-
- /// Event's version
- public byte Version { get; set; }
-
- ///
- /// This can be specified to enable formatting and localization of the event's payload. You can
- /// use standard .NET substitution operators (eg {1}) in the string and they will be replaced
- /// with the 'ToString()' of the corresponding part of the event payload.
- ///
- public string? Message { get; set; }
-
- ///
- /// User defined options associated with the event. These do not have meaning to the EventSource but
- /// are passed through to listeners which given them semantics.
- ///
- public EventTags Tags { get; set; }
-
- ///
- /// Allows fine control over the Activity IDs generated by start and stop events
- ///
- public EventActivityOptions ActivityOptions { get; set; }
-
-#region private
- private EventOpcode m_opcode;
- private bool m_opcodeSet;
-#endregion
- }
+ // Instance fields
+ internal readonly EventListener m_Listener; // The dispatcher this entry is for
+ internal Dictionary? m_EventEnabled; // For every event in a the eventSource, is it enabled?
- ///
- /// By default all instance methods in a class that subclasses code:EventSource that and return
- /// void are assumed to be methods that generate an event. This default can be overridden by specifying
- /// the code:NonEventAttribute
- ///
- [AttributeUsage(AttributeTargets.Method)]
- public sealed class NonEventAttribute : Attribute
- {
- ///
- /// Constructs a default NonEventAttribute
- ///
- public NonEventAttribute() { }
+ // Only guaranteed to exist after a EnsureInit()
+ internal EventDispatcher? m_Next; // These form a linked list in code:EventSource.m_Dispatchers
+ // Of all listeners for that eventSource.
}
///
- /// EventChannelAttribute allows customizing channels supported by an EventSource. This attribute must be
- /// applied to an member of type EventChannel defined in a Channels class nested in the EventSource class:
- ///
- /// public static class Channels
- /// {
- /// [Channel(Enabled = true, EventChannelType = EventChannelType.Admin)]
- /// public const EventChannel Admin = (EventChannel)16;
- ///
- /// [Channel(Enabled = false, EventChannelType = EventChannelType.Operational)]
- /// public const EventChannel Operational = (EventChannel)17;
- /// }
- ///
+ /// Flags that can be used with EventSource.GenerateManifest to control how the ETW manifest for the EventSource is
+ /// generated.
///
- [AttributeUsage(AttributeTargets.Field)]
- internal sealed class EventChannelAttribute : Attribute
+ [Flags]
+ public enum EventManifestOptions
{
///
- /// Specified whether the channel is enabled by default
- ///
- public bool Enabled { get; set; }
-
- ///
- /// Legal values are in EventChannelType
+ /// Only the resources associated with current UI culture are included in the manifest
///
- public EventChannelType EventChannelType { get; set; }
-
- // TODO: there is a convention that the name is the Provider/Type Should we provide an override?
- // public string Name { get; set; }
- }
-
- ///
- /// Allowed channel types
- ///
- internal enum EventChannelType
- {
- /// The admin channel
- Admin = 1,
- /// The operational channel
- Operational,
- /// The Analytic channel
- Analytic,
- /// The debug channel
- Debug,
- }
-
- ///
- /// Describes the pre-defined command (EventCommandEventArgs.Command property) that is passed to the OnEventCommand callback.
- ///
- public enum EventCommand
- {
+ None = 0x0,
///
- /// Update EventSource state
+ /// Throw exceptions for any inconsistency encountered
///
- Update = 0,
+ Strict = 0x1,
///
- /// Request EventSource to generate and send its manifest
+ /// Generate a "resources" node under "localization" for every satellite assembly provided
///
- SendManifest = -1,
+ AllCultures = 0x2,
///
- /// Enable event
+ /// Generate the manifest only if the event source needs to be registered on the machine,
+ /// otherwise return null (but still perform validation if Strict is specified)
///
- Enable = -2,
+ OnlyIfNeededForRegistration = 0x4,
///
- /// Disable event
- ///
- Disable = -3
- }
-
-#region private classes
-
- // holds a bitfield representing a session mask
- ///
- /// A SessionMask represents a set of (at most MAX) sessions as a bit mask. The perEventSourceSessionId
- /// is the index in the SessionMask of the bit that will be set. These can translate to
- /// EventSource's reserved keywords bits using the provided ToEventKeywords() and
- /// FromEventKeywords() methods.
- ///
- internal struct SessionMask
- {
- public SessionMask(SessionMask m)
- { m_mask = m.m_mask; }
-
- public SessionMask(uint mask = 0)
- { m_mask = mask & MASK; }
-
- public bool IsEqualOrSupersetOf(SessionMask m)
- {
- return (this.m_mask | m.m_mask) == this.m_mask;
- }
-
- public static SessionMask All => new SessionMask(MASK);
-
- public static SessionMask FromId(int perEventSourceSessionId)
- {
- Debug.Assert(perEventSourceSessionId < MAX);
- return new SessionMask((uint)1 << perEventSourceSessionId);
- }
-
- public ulong ToEventKeywords()
- {
- return (ulong)m_mask << SHIFT_SESSION_TO_KEYWORD;
- }
-
- public static SessionMask FromEventKeywords(ulong m)
- {
- return new SessionMask((uint)(m >> SHIFT_SESSION_TO_KEYWORD));
- }
-
- public bool this[int perEventSourceSessionId]
- {
- get
- {
- Debug.Assert(perEventSourceSessionId < MAX);
- return (m_mask & (1 << perEventSourceSessionId)) != 0;
- }
- set
- {
- Debug.Assert(perEventSourceSessionId < MAX);
- if (value) m_mask |= ((uint)1 << perEventSourceSessionId);
- else m_mask &= ~((uint)1 << perEventSourceSessionId);
- }
- }
-
- public static SessionMask operator |(SessionMask m1, SessionMask m2) =>
- new SessionMask(m1.m_mask | m2.m_mask);
-
- public static SessionMask operator &(SessionMask m1, SessionMask m2) =>
- new SessionMask(m1.m_mask & m2.m_mask);
-
- public static SessionMask operator ^(SessionMask m1, SessionMask m2) =>
- new SessionMask(m1.m_mask ^ m2.m_mask);
-
- public static SessionMask operator ~(SessionMask m) =>
- new SessionMask(MASK & ~(m.m_mask));
-
- public static explicit operator ulong(SessionMask m) => m.m_mask;
-
- public static explicit operator uint(SessionMask m) => m.m_mask;
-
- private uint m_mask;
-
- internal const int SHIFT_SESSION_TO_KEYWORD = 44; // bits 44-47 inclusive are reserved
- internal const uint MASK = 0x0fU; // the mask of 4 reserved bits
- internal const uint MAX = 4; // maximum number of simultaneous ETW sessions supported
- }
-
- ///
- /// code:EventDispatchers are a simple 'helper' structure that holds the filtering state
- /// (m_EventEnabled) for a particular EventSource X EventListener tuple
- ///
- /// Thus a single EventListener may have many EventDispatchers (one for every EventSource
- /// that EventListener has activate) and a Single EventSource may also have many
- /// event Dispatchers (one for every EventListener that has activated it).
- ///
- /// Logically a particular EventDispatcher belongs to exactly one EventSource and exactly
- /// one EventListener (although EventDispatcher does not 'remember' the EventSource it is
- /// associated with.
- ///
- internal sealed class EventDispatcher
- {
- internal EventDispatcher(EventDispatcher? next, Dictionary? eventEnabled, EventListener listener)
- {
- m_Next = next;
- m_EventEnabled = eventEnabled;
- m_Listener = listener;
- }
-
- // Instance fields
- internal readonly EventListener m_Listener; // The dispatcher this entry is for
- internal Dictionary? m_EventEnabled; // For every event in a the eventSource, is it enabled?
-
- // Only guaranteed to exist after a EnsureInit()
- internal EventDispatcher? m_Next; // These form a linked list in code:EventSource.m_Dispatchers
- // Of all listeners for that eventSource.
- }
-
- ///
- /// Flags that can be used with EventSource.GenerateManifest to control how the ETW manifest for the EventSource is
- /// generated.
- ///
- [Flags]
- public enum EventManifestOptions
- {
- ///
- /// Only the resources associated with current UI culture are included in the manifest
- ///
- None = 0x0,
- ///
- /// Throw exceptions for any inconsistency encountered
- ///
- Strict = 0x1,
- ///
- /// Generate a "resources" node under "localization" for every satellite assembly provided
- ///
- AllCultures = 0x2,
- ///
- /// Generate the manifest only if the event source needs to be registered on the machine,
- /// otherwise return null (but still perform validation if Strict is specified)
- ///
- OnlyIfNeededForRegistration = 0x4,
- ///
- /// When generating the manifest do *not* enforce the rule that the current EventSource class
- /// must be the base class for the user-defined type passed in. This allows validation of .net
- /// event sources using the new validation code
+ /// When generating the manifest do *not* enforce the rule that the current EventSource class
+ /// must be the base class for the user-defined type passed in. This allows validation of .net
+ /// event sources using the new validation code
///
AllowEventSourceOverride = 0x8,
}
- ///
- /// ManifestBuilder is designed to isolate the details of the message of the event from the
- /// rest of EventSource. This one happens to create XML.
- ///
- internal sealed class ManifestBuilder
- {
- ///
- /// Build a manifest for 'providerName' with the given GUID, which will be packaged into 'dllName'.
- /// 'resources, is a resource manager. If specified all messages are localized using that manager.
- ///
- public ManifestBuilder(string providerName, Guid providerGuid, string? dllName, ResourceManager? resources,
- EventManifestOptions flags) : this(resources, flags)
- {
- this.providerName = providerName;
-
- sb = new StringBuilder();
- events = new StringBuilder();
- templates = new StringBuilder();
- sb.AppendLine("");
- sb.AppendLine(" ");
- sb.AppendLine(" ");
- sb.Append($"");
- }
-
- ///
- /// Will NOT build a manifest! If the intention is to build a manifest don't use this constructor.
- ///'resources, is a resource manager. If specified all messages are localized using that manager.
- ///
- internal ManifestBuilder(ResourceManager? resources, EventManifestOptions flags)
- {
- providerName = "";
-
- this.flags = flags;
-
- this.resources = resources;
- sb = null;
- events = null;
- templates = null;
- opcodeTab = new Dictionary();
- stringTab = new Dictionary();
- errors = new List();
- perEventByteArrayArgIndices = new Dictionary>();
- }
-
- public void AddOpcode(string name, int value)
- {
- if ((flags & EventManifestOptions.Strict) != 0)
- {
- if (value <= 10 || value >= 239)
- {
- ManifestError(SR.Format(SR.EventSource_IllegalOpcodeValue, name, value));
- }
-
- if (opcodeTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
- {
- ManifestError(SR.Format(SR.EventSource_OpcodeCollision, name, prevName, value));
- }
- }
-
- opcodeTab[value] = name;
- }
-
- public void AddTask(string name, int value)
- {
- if ((flags & EventManifestOptions.Strict) != 0)
- {
- if (value <= 0 || value >= 65535)
- {
- ManifestError(SR.Format(SR.EventSource_IllegalTaskValue, name, value));
- }
-
- if (taskTab != null && taskTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
- {
- ManifestError(SR.Format(SR.EventSource_TaskCollision, name, prevName, value));
- }
- }
-
- taskTab ??= new Dictionary();
- taskTab[value] = name;
- }
-
- public void AddKeyword(string name, ulong value)
- {
- if ((value & (value - 1)) != 0) // Must be zero or a power of 2
- {
- ManifestError(SR.Format(SR.EventSource_KeywordNeedPowerOfTwo, $"0x{value:x}", name), true);
- }
- if ((flags & EventManifestOptions.Strict) != 0)
- {
- if (value >= 0x0000100000000000UL && !name.StartsWith("Session", StringComparison.Ordinal))
- {
- ManifestError(SR.Format(SR.EventSource_IllegalKeywordsValue, name, $"0x{value:x}"));
- }
-
- if (keywordTab != null && keywordTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
- {
- ManifestError(SR.Format(SR.EventSource_KeywordCollision, name, prevName, $"0x{value:x}"));
- }
- }
-
- keywordTab ??= new Dictionary();
- keywordTab[value] = name;
- }
-
- ///
- /// Add a channel. channelAttribute can be null
- ///
- public void AddChannel(string? name, int value, EventChannelAttribute? channelAttribute)
- {
- EventChannel chValue = (EventChannel)value;
- if (value < (int)EventChannel.Admin || value > 255)
- ManifestError(SR.Format(SR.EventSource_EventChannelOutOfRange, name, value));
- else if (chValue >= EventChannel.Admin && chValue <= EventChannel.Debug &&
- channelAttribute != null && EventChannelToChannelType(chValue) != channelAttribute.EventChannelType)
- {
- // we want to ensure developers do not define EventChannels that conflict with the builtin ones,
- // but we want to allow them to override the default ones...
- ManifestError(SR.Format(SR.EventSource_ChannelTypeDoesNotMatchEventChannelValue,
- name, ((EventChannel)value).ToString()));
- }
-
- // TODO: validate there are no conflicting manifest exposed names (generally following the format "provider/type")
-
- ulong kwd = GetChannelKeyword(chValue);
-
- channelTab ??= new Dictionary(4);
- channelTab[value] = new ChannelInfo { Name = name, Keywords = kwd, Attribs = channelAttribute };
- }
-
- private static EventChannelType EventChannelToChannelType(EventChannel channel)
- {
- Debug.Assert(channel >= EventChannel.Admin && channel <= EventChannel.Debug);
- return (EventChannelType)((int)channel - (int)EventChannel.Admin + (int)EventChannelType.Admin);
- }
-
- private static EventChannelAttribute GetDefaultChannelAttribute(EventChannel channel)
- {
- EventChannelAttribute attrib = new EventChannelAttribute();
- attrib.EventChannelType = EventChannelToChannelType(channel);
- if (attrib.EventChannelType <= EventChannelType.Operational)
- attrib.Enabled = true;
- return attrib;
- }
-
- public ulong[] GetChannelData()
- {
- if (this.channelTab == null)
- {
- return [];
- }
-
- // We create an array indexed by the channel id for fast look up.
- // E.g. channelMask[Admin] will give you the bit mask for Admin channel.
- int maxkey = -1;
- foreach (int item in this.channelTab.Keys)
- {
- if (item > maxkey)
- {
- maxkey = item;
- }
- }
-
- ulong[] channelMask = new ulong[maxkey + 1];
- foreach (KeyValuePair item in this.channelTab)
- {
- channelMask[item.Key] = item.Value.Keywords;
- }
-
- return channelMask;
- }
-
- public void StartEvent(string eventName, EventAttribute eventAttribute)
- {
- Debug.Assert(numParams == 0);
- Debug.Assert(this.eventName == null);
- this.eventName = eventName;
- numParams = 0;
- byteArrArgIndices = null;
-
- events?.Append(" ");
- if (type == typeof(byte[]))
- {
- // mark this index as "extraneous" (it has no parallel in the managed signature)
- // we use these values in TranslateToManifestConvention()
- byteArrArgIndices ??= new List(4);
- byteArrArgIndices.Add(numParams);
-
- // add an extra field to the template representing the length of the binary blob
- numParams++;
- templates?.Append(" ");
- }
- numParams++;
- templates?.Append(" ();
- mapsTab.TryAdd(type.Name, type); // Remember that we need to dump the type enumeration
- }
-
- templates?.AppendLine("/>");
- }
- public void EndEvent()
- {
- Debug.Assert(eventName != null);
-
- if (numParams > 0)
- {
- templates?.AppendLine(" ");
- events?.Append(" template=\"").Append(eventName).Append("Args\"");
- }
- events?.AppendLine("/>");
-
- if (byteArrArgIndices != null)
- perEventByteArrayArgIndices[eventName] = byteArrArgIndices;
-
- // at this point we have all the information we need to translate the C# Message
- // to the manifest string we'll put in the stringTab
- string prefixedEventName = "event_" + eventName;
- if (stringTab.TryGetValue(prefixedEventName, out string? msg))
- {
- msg = TranslateToManifestConvention(msg, eventName);
- stringTab[prefixedEventName] = msg;
- }
-
- eventName = null;
- numParams = 0;
- byteArrArgIndices = null;
- }
-
- // Channel keywords are generated one per channel to allow channel based filtering in event viewer. These keywords are autogenerated
- // by mc.exe for compiling a manifest and are based on the order of the channels (fields) in the Channels inner class (when advanced
- // channel support is enabled), or based on the order the predefined channels appear in the EventAttribute properties (for simple
- // support). The manifest generated *MUST* have the channels specified in the same order (that's how our computed keywords are mapped
- // to channels by the OS infrastructure).
- // If channelKeyworkds is present, and has keywords bits in the ValidPredefinedChannelKeywords then it is
- // assumed that the keyword for that channel should be that bit.
- // otherwise we allocate a channel bit for the channel.
- // explicit channel bits are only used by WCF to mimic an existing manifest,
- // so we don't dont do error checking.
- public ulong GetChannelKeyword(EventChannel channel, ulong channelKeyword = 0)
- {
- // strip off any non-channel keywords, since we are only interested in channels here.
- channelKeyword &= ValidPredefinedChannelKeywords;
- channelTab ??= new Dictionary(4);
-
- if (channelTab.Count == MaxCountChannels)
- ManifestError(SR.EventSource_MaxChannelExceeded);
-
- if (!channelTab.TryGetValue((int)channel, out ChannelInfo? info))
- {
- // If we were not given an explicit channel, allocate one.
- if (channelKeyword == 0)
- {
- channelKeyword = nextChannelKeywordBit;
- nextChannelKeywordBit >>= 1;
- }
- }
- else
- {
- channelKeyword = info.Keywords;
- }
-
- return channelKeyword;
- }
-
- public byte[] CreateManifest()
- {
- string str = CreateManifestString();
- return (str != "") ? Encoding.UTF8.GetBytes(str) : [];
- }
-
- public IList Errors => errors;
-
- public bool HasResources => resources != null;
-
- ///
- /// When validating an event source it adds the error to the error collection.
- /// When not validating it throws an exception if runtimeCritical is "true".
- /// Otherwise the error is ignored.
- ///
- ///
- ///
- public void ManifestError(string msg, bool runtimeCritical = false)
- {
- if ((flags & EventManifestOptions.Strict) != 0)
- errors.Add(msg);
- else if (runtimeCritical)
- throw new ArgumentException(msg);
- }
-
- private string CreateManifestString()
- {
- Span ulongHexScratch = stackalloc char[16]; // long enough for ulong.MaxValue formatted as hex
-
- // Write out the channels
- if (channelTab != null)
- {
- sb?.AppendLine(" ");
- var sortedChannels = new List>();
- foreach (KeyValuePair p in channelTab) { sortedChannels.Add(p); }
- sortedChannels.Sort((p1, p2) => -Comparer.Default.Compare(p1.Value.Keywords, p2.Value.Keywords));
-
- foreach (KeyValuePair kvpair in sortedChannels)
- {
- int channel = kvpair.Key;
- ChannelInfo channelInfo = kvpair.Value;
-
- string? channelType = null;
- bool enabled = false;
- string? fullName = null;
-
- if (channelInfo.Attribs != null)
- {
- EventChannelAttribute attribs = channelInfo.Attribs;
- if (Enum.IsDefined(attribs.EventChannelType))
- channelType = attribs.EventChannelType.ToString();
- enabled = attribs.Enabled;
- }
-
- fullName ??= providerName + "/" + channelInfo.Name;
-
- sb?.Append(" ");
- }
- sb?.AppendLine(" ");
- }
-
- // Write out the tasks
- if (taskTab != null)
- {
- sb?.AppendLine(" ");
- var sortedTasks = new List(taskTab.Keys);
- sortedTasks.Sort();
-
- foreach (int task in sortedTasks)
- {
- sb?.Append(" ");
- }
- sb?.AppendLine(" ");
- }
-
- // Write out the maps
-
- // Scoping the call to enum GetFields to a local function to limit the trimming suppressions
- [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
- Justification = "Trimmer does not trim enums")]
- static FieldInfo[] GetEnumFields(Type localEnumType)
- {
- Debug.Assert(localEnumType.IsEnum);
- return localEnumType.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
- }
-
- if (mapsTab != null)
- {
- sb?.AppendLine(" ");
- foreach (Type enumType in mapsTab.Values)
- {
- bool isbitmap = EventSource.IsCustomAttributeDefinedHelper(enumType, typeof(FlagsAttribute), flags);
- string mapKind = isbitmap ? "bitMap" : "valueMap";
- sb?.Append(" <").Append(mapKind).Append(" name=\"").Append(enumType.Name).AppendLine("\">");
-
- // write out each enum value
- FieldInfo[] staticFields = GetEnumFields(enumType);
- bool anyValuesWritten = false;
- foreach (FieldInfo staticField in staticFields)
- {
- object? constantValObj = staticField.GetRawConstantValue();
-
- if (constantValObj != null)
- {
- ulong hexValue;
- if (constantValObj is ulong)
- hexValue = (ulong)constantValObj; // This is the only integer type that can't be represented by a long.
- else
- hexValue = (ulong)Convert.ToInt64(constantValObj); // Handles all integer types except ulong.
-
- // ETW requires all bitmap values to be powers of 2. Skip the ones that are not.
- // TODO: Warn people about the dropping of values.
- if (isbitmap && !BitOperations.IsPow2(hexValue))
- continue;
-
- hexValue.TryFormat(ulongHexScratch, out int charsWritten, "x");
- ReadOnlySpan hexValueFormatted = ulongHexScratch.Slice(0, charsWritten);
-
- sb?.Append(" ");
- anyValuesWritten = true;
- }
- }
-
- // the OS requires that bitmaps and valuemaps have at least one value or it reject the whole manifest.
- // To avoid that put a 'None' entry if there are no other values.
- if (!anyValuesWritten)
- {
- sb?.Append(" ");
- }
- sb?.Append(" ").Append(mapKind).AppendLine(">");
- }
- sb?.AppendLine(" ");
- }
-
- // Write out the opcodes
- sb?.AppendLine(" ");
- var sortedOpcodes = new List(opcodeTab.Keys);
- sortedOpcodes.Sort();
-
- foreach (int opcode in sortedOpcodes)
- {
- sb?.Append(" ");
- }
- sb?.AppendLine(" ");
-
- // Write out the keywords
- if (keywordTab != null)
- {
- sb?.AppendLine(" ");
- var sortedKeywords = new List(keywordTab.Keys);
- sortedKeywords.Sort();
-
- foreach (ulong keyword in sortedKeywords)
- {
- sb?.Append(" keywordFormatted = ulongHexScratch.Slice(0, charsWritten);
- sb?.Append(" mask=\"0x").Append(keywordFormatted).AppendLine("\"/>");
- }
- sb?.AppendLine(" ");
- }
-
- sb?.AppendLine(" ");
- sb?.Append(events);
- sb?.AppendLine(" ");
-
- sb?.AppendLine(" ");
- if (templates?.Length > 0)
- {
- sb?.Append(templates);
- }
- else
- {
- // Work around a corner-case ETW issue where a manifest with no templates causes
- // ETW events to not get sent to their associated channel.
- sb?.AppendLine(" ");
- }
- sb?.AppendLine(" ");
-
- sb?.AppendLine(" ");
- sb?.AppendLine(" ");
- sb?.AppendLine(" ");
-
- // Output the localization information.
- sb?.AppendLine("");
-
- var sortedStrings = new string[stringTab.Keys.Count];
- stringTab.Keys.CopyTo(sortedStrings, 0);
- Array.Sort(sortedStrings, StringComparer.Ordinal);
-
- CultureInfo ci = CultureInfo.CurrentUICulture;
- sb?.Append(" ");
- sb?.AppendLine(" ");
- foreach (string stringKey in sortedStrings)
- {
- string? val = GetLocalizedMessage(stringKey, ci, etwFormat: true);
- sb?.Append(" ");
- }
- sb?.AppendLine(" ");
- sb?.AppendLine(" ");
-
- sb?.AppendLine(" ");
- sb?.AppendLine(" ");
- return sb?.ToString() ?? "";
- }
-
-#region private
- private void WriteNameAndMessageAttribs(StringBuilder? stringBuilder, string elementName, string name)
- {
- stringBuilder?.Append(" name=\"").Append(name).Append('"');
- WriteMessageAttrib(sb, elementName, name, name);
- }
- private void WriteMessageAttrib(StringBuilder? stringBuilder, string elementName, string name, string? value)
- {
- string? key = null;
-
- // See if the user wants things localized.
- if (resources != null)
- {
- // resource fallback: strings in the neutral culture will take precedence over inline strings
- key = elementName + "_" + name;
- if (resources.GetString(key, CultureInfo.InvariantCulture) is string localizedString)
- value = localizedString;
- }
-
- if (value == null)
- return;
-
- key ??= elementName + "_" + name;
- stringBuilder?.Append(" message=\"$(string.").Append(key).Append(")\"");
-
- if (stringTab.TryGetValue(key, out string? prevValue) && !prevValue.Equals(value))
- {
- ManifestError(SR.Format(SR.EventSource_DuplicateStringKey, key), true);
- return;
- }
-
- stringTab[key] = value;
- }
- internal string? GetLocalizedMessage(string key, CultureInfo ci, bool etwFormat)
- {
- string? value = null;
- if (resources != null)
- {
- string? localizedString = resources.GetString(key, ci);
- if (localizedString != null)
- {
- value = localizedString;
- if (etwFormat && key.StartsWith("event_", StringComparison.Ordinal))
- {
- string evtName = key.Substring("event_".Length);
- value = TranslateToManifestConvention(value, evtName);
- }
- }
- }
- if (etwFormat && value == null)
- stringTab.TryGetValue(key, out value);
-
- return value;
- }
-
- private static void AppendLevelName(StringBuilder? sb, EventLevel level)
- {
- if ((int)level < 16)
- {
- sb?.Append("win:");
- }
-
- sb?.Append(level switch // avoid boxing that comes from level.ToString()
- {
- EventLevel.LogAlways => nameof(EventLevel.LogAlways),
- EventLevel.Critical => nameof(EventLevel.Critical),
- EventLevel.Error => nameof(EventLevel.Error),
- EventLevel.Warning => nameof(EventLevel.Warning),
- EventLevel.Informational => nameof(EventLevel.Informational),
- EventLevel.Verbose => nameof(EventLevel.Verbose),
- _ => ((int)level).ToString()
- });
- }
-
- private string? GetChannelName(EventChannel channel, string eventName, string? eventMessage)
- {
- if (channelTab == null || !channelTab.TryGetValue((int)channel, out ChannelInfo? info))
- {
- if (channel < EventChannel.Admin) // || channel > EventChannel.Debug)
- ManifestError(SR.Format(SR.EventSource_UndefinedChannel, channel, eventName));
-
- // allow channels to be auto-defined. The well known ones get their well known names, and the
- // rest get names Channel. This allows users to modify the Manifest if they want more advanced features.
- channelTab ??= new Dictionary(4);
-
- string channelName = channel.ToString(); // For well know channels this is a nice name, otherwise a number
- if (EventChannel.Debug < channel)
- channelName = "Channel" + channelName; // Add a 'Channel' prefix for numbers.
-
- AddChannel(channelName, (int)channel, GetDefaultChannelAttribute(channel));
- if (!channelTab.TryGetValue((int)channel, out info))
- ManifestError(SR.Format(SR.EventSource_UndefinedChannel, channel, eventName));
- }
- // events that specify admin channels *must* have non-null "Message" attributes
- if (resources != null)
- eventMessage ??= resources.GetString("event_" + eventName, CultureInfo.InvariantCulture);
-
- Debug.Assert(info!.Attribs != null);
- if (info.Attribs.EventChannelType == EventChannelType.Admin && eventMessage == null)
- ManifestError(SR.Format(SR.EventSource_EventWithAdminChannelMustHaveMessage, eventName, info.Name));
- return info.Name;
- }
- private string GetTaskName(EventTask task, string eventName)
- {
- if (task == EventTask.None)
- return "";
-
- taskTab ??= new Dictionary();
- if (!taskTab.TryGetValue((int)task, out string? ret))
- ret = taskTab[(int)task] = eventName;
- return ret;
- }
-
- private string? GetOpcodeName(EventOpcode opcode, string eventName)
- {
- switch (opcode)
- {
- case EventOpcode.Info:
- return "win:Info";
- case EventOpcode.Start:
- return "win:Start";
- case EventOpcode.Stop:
- return "win:Stop";
- case EventOpcode.DataCollectionStart:
- return "win:DC_Start";
- case EventOpcode.DataCollectionStop:
- return "win:DC_Stop";
- case EventOpcode.Extension:
- return "win:Extension";
- case EventOpcode.Reply:
- return "win:Reply";
- case EventOpcode.Resume:
- return "win:Resume";
- case EventOpcode.Suspend:
- return "win:Suspend";
- case EventOpcode.Send:
- return "win:Send";
- case EventOpcode.Receive:
- return "win:Receive";
- }
-
- if (opcodeTab == null || !opcodeTab.TryGetValue((int)opcode, out string? ret))
- {
- ManifestError(SR.Format(SR.EventSource_UndefinedOpcode, opcode, eventName), true);
- ret = null;
- }
-
- return ret;
- }
-
- private void AppendKeywords(StringBuilder? sb, ulong keywords, string eventName)
- {
- // ignore keywords associate with channels
- // See ValidPredefinedChannelKeywords def for more.
- keywords &= ~ValidPredefinedChannelKeywords;
-
- bool appended = false;
- for (ulong bit = 1; bit != 0; bit <<= 1)
- {
- if ((keywords & bit) != 0)
- {
- string? keyword = null;
- if ((keywordTab == null || !keywordTab.TryGetValue(bit, out keyword)) &&
- (bit >= (ulong)0x1000000000000))
- {
- // do not report Windows reserved keywords in the manifest (this allows the code
- // to be resilient to potential renaming of these keywords)
- keyword = string.Empty;
- }
- if (keyword == null)
- {
- ManifestError(SR.Format(SR.EventSource_UndefinedKeyword, "0x" + bit.ToString("x", CultureInfo.CurrentCulture), eventName), true);
- keyword = string.Empty;
- }
-
- if (keyword.Length != 0)
- {
- if (appended)
- {
- sb?.Append(' ');
- }
-
- sb?.Append(keyword);
- appended = true;
- }
- }
- }
- }
-
- private string GetTypeName(Type type)
- {
- if (type.IsEnum)
- {
- string typeName = GetTypeName(type.GetEnumUnderlyingType());
- return typeName switch // ETW requires enums to be unsigned.
- {
- "win:Int8" => "win:UInt8",
- "win:Int16" => "win:UInt16",
- "win:Int32" => "win:UInt32",
- "win:Int64" => "win:UInt64",
- _ => typeName,
- };
- }
-
- switch (Type.GetTypeCode(type))
- {
- case TypeCode.Boolean:
- return "win:Boolean";
- case TypeCode.Byte:
- return "win:UInt8";
- case TypeCode.Char:
- case TypeCode.UInt16:
- return "win:UInt16";
- case TypeCode.UInt32:
- return "win:UInt32";
- case TypeCode.UInt64:
- return "win:UInt64";
- case TypeCode.SByte:
- return "win:Int8";
- case TypeCode.Int16:
- return "win:Int16";
- case TypeCode.Int32:
- return "win:Int32";
- case TypeCode.Int64:
- return "win:Int64";
- case TypeCode.String:
- return "win:UnicodeString";
- case TypeCode.Single:
- return "win:Float";
- case TypeCode.Double:
- return "win:Double";
- case TypeCode.DateTime:
- return "win:FILETIME";
- default:
- if (type == typeof(Guid))
- return "win:GUID";
- else if (type == typeof(IntPtr))
- return "win:Pointer";
- else if ((type.IsArray || type.IsPointer) && type.GetElementType() == typeof(byte))
- return "win:Binary";
-
- ManifestError(SR.Format(SR.EventSource_UnsupportedEventTypeInManifest, type.Name), true);
- return string.Empty;
- }
- }
-
- private static void UpdateStringBuilder([NotNull] ref StringBuilder? stringBuilder, string eventMessage, int startIndex, int count)
- {
- stringBuilder ??= new StringBuilder();
- stringBuilder.Append(eventMessage, startIndex, count);
- }
-
- private static readonly string[] s_escapes = ["&", "<", ">", "'", """, "%r", "%n", "%t"];
- // Manifest messages use %N conventions for their message substitutions. Translate from
- // .NET conventions. We can't use RegEx for this (we are in mscorlib), so we do it 'by hand'
- private string TranslateToManifestConvention(string eventMessage, string evtName)
- {
- StringBuilder? stringBuilder = null; // We lazily create this
- int writtenSoFar = 0;
- for (int i = 0; ;)
- {
- if (i >= eventMessage.Length)
- {
- if (stringBuilder == null)
- return eventMessage;
- UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
- return stringBuilder.ToString();
- }
-
- int chIdx;
- if (eventMessage[i] == '%')
- {
- // handle format message escaping character '%' by escaping it
- UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
- stringBuilder.Append("%%");
- i++;
- writtenSoFar = i;
- }
- else if (i < eventMessage.Length - 1 &&
- (eventMessage[i] == '{' && eventMessage[i + 1] == '{' || eventMessage[i] == '}' && eventMessage[i + 1] == '}'))
- {
- // handle C# escaped '{" and '}'
- UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
- stringBuilder.Append(eventMessage[i]);
- i++; i++;
- writtenSoFar = i;
- }
- else if (eventMessage[i] == '{')
- {
- int leftBracket = i;
- i++;
- int argNum = 0;
- while (i < eventMessage.Length && char.IsDigit(eventMessage[i]))
- {
- argNum = argNum * 10 + eventMessage[i] - '0';
- i++;
- }
- if (i < eventMessage.Length && eventMessage[i] == '}')
- {
- i++;
- UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, leftBracket - writtenSoFar);
- int manIndex = TranslateIndexToManifestConvention(argNum, evtName);
- stringBuilder.Append('%').Append(manIndex);
- // An '!' after the insert specifier {n} will be interpreted as a literal.
- // We'll escape it so that mc.exe does not attempt to consider it the
- // beginning of a format string.
- if (i < eventMessage.Length && eventMessage[i] == '!')
- {
- i++;
- stringBuilder.Append("%!");
- }
- writtenSoFar = i;
- }
- else
- {
- ManifestError(SR.Format(SR.EventSource_UnsupportedMessageProperty, evtName, eventMessage));
- }
- }
- else if ((chIdx = "&<>'\"\r\n\t".IndexOf(eventMessage[i])) >= 0)
- {
- UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
- i++;
- stringBuilder.Append(s_escapes[chIdx]);
- writtenSoFar = i;
- }
- else
- i++;
- }
- }
-
- private int TranslateIndexToManifestConvention(int idx, string evtName)
- {
- if (perEventByteArrayArgIndices.TryGetValue(evtName, out List? byteArrArgIndices))
- {
- foreach (int byArrIdx in byteArrArgIndices)
- {
- if (idx >= byArrIdx)
- ++idx;
- else
- break;
- }
- }
- return idx + 1;
- }
-
- private sealed class ChannelInfo
- {
- public string? Name;
- public ulong Keywords;
- public EventChannelAttribute? Attribs;
- }
-
- private readonly Dictionary opcodeTab;
- private Dictionary? taskTab;
- private Dictionary? channelTab;
- private Dictionary? keywordTab;
- private Dictionary? mapsTab;
- private readonly Dictionary stringTab; // Maps unlocalized strings to localized ones
-
- // WCF used EventSource to mimic a existing ETW manifest. To support this
- // in just their case, we allowed them to specify the keywords associated
- // with their channels explicitly. ValidPredefinedChannelKeywords is
- // this set of channel keywords that we allow to be explicitly set. You
- // can ignore these bits otherwise.
- internal const ulong ValidPredefinedChannelKeywords = 0xF000000000000000;
- private ulong nextChannelKeywordBit = 0x8000000000000000; // available Keyword bit to be used for next channel definition, grows down
- private const int MaxCountChannels = 8; // a manifest can defined at most 8 ETW channels
-
- private readonly StringBuilder? sb; // Holds the provider information.
- private readonly StringBuilder? events; // Holds the events.
- private readonly StringBuilder? templates;
- private readonly string providerName;
- private readonly ResourceManager? resources; // Look up localized strings here.
- private readonly EventManifestOptions flags;
- private readonly List errors; // list of currently encountered errors
- private readonly Dictionary> perEventByteArrayArgIndices; // "event_name" -> List_of_Indices_of_Byte[]_Arg
-
- // State we track between StartEvent and EndEvent.
- private string? eventName; // Name of the event currently being processed.
- private int numParams; // keeps track of the number of args the event has.
- private List? byteArrArgIndices; // keeps track of the index of each byte[] argument
-#endregion
- }
-
///
/// Used to send the m_rawManifest into the event dispatcher as a series of events.
///
diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/ManifestBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/ManifestBuilder.cs
new file mode 100644
index 00000000000000..7f78c1d8538ee9
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/ManifestBuilder.cs
@@ -0,0 +1,939 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Numerics;
+using System.Reflection;
+using System.Resources;
+using System.Text;
+
+namespace System.Diagnostics.Tracing;
+
+///
+/// ManifestBuilder is designed to isolate the details of the message of the event from the
+/// rest of EventSource. This one happens to create XML.
+///
+internal sealed class ManifestBuilder
+{
+ ///
+ /// Build a manifest for 'providerName' with the given GUID, which will be packaged into 'dllName'.
+ /// 'resources, is a resource manager. If specified all messages are localized using that manager.
+ ///
+ public ManifestBuilder(string providerName, Guid providerGuid, string? dllName, ResourceManager? resources,
+ EventManifestOptions flags) : this(resources, flags)
+ {
+ this.providerName = providerName;
+
+ sb = new StringBuilder();
+ events = new StringBuilder();
+ templates = new StringBuilder();
+ sb.AppendLine("");
+ sb.AppendLine(" ");
+ sb.AppendLine(" ");
+ sb.Append($"");
+ }
+
+ ///
+ /// Will NOT build a manifest! If the intention is to build a manifest don't use this constructor.
+ ///'resources, is a resource manager. If specified all messages are localized using that manager.
+ ///
+ internal ManifestBuilder(ResourceManager? resources, EventManifestOptions flags)
+ {
+ providerName = "";
+
+ this.flags = flags;
+
+ this.resources = resources;
+ sb = null;
+ events = null;
+ templates = null;
+ opcodeTab = new Dictionary();
+ stringTab = new Dictionary();
+ errors = new List();
+ perEventByteArrayArgIndices = new Dictionary>();
+ }
+
+ public void AddOpcode(string name, int value)
+ {
+ if ((flags & EventManifestOptions.Strict) != 0)
+ {
+ if (value <= 10 || value >= 239)
+ {
+ ManifestError(SR.Format(SR.EventSource_IllegalOpcodeValue, name, value));
+ }
+
+ if (opcodeTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
+ {
+ ManifestError(SR.Format(SR.EventSource_OpcodeCollision, name, prevName, value));
+ }
+ }
+
+ opcodeTab[value] = name;
+ }
+
+ public void AddTask(string name, int value)
+ {
+ if ((flags & EventManifestOptions.Strict) != 0)
+ {
+ if (value <= 0 || value >= 65535)
+ {
+ ManifestError(SR.Format(SR.EventSource_IllegalTaskValue, name, value));
+ }
+
+ if (taskTab != null && taskTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
+ {
+ ManifestError(SR.Format(SR.EventSource_TaskCollision, name, prevName, value));
+ }
+ }
+
+ taskTab ??= new Dictionary();
+ taskTab[value] = name;
+ }
+
+ public void AddKeyword(string name, ulong value)
+ {
+ if ((value & (value - 1)) != 0) // Must be zero or a power of 2
+ {
+ ManifestError(SR.Format(SR.EventSource_KeywordNeedPowerOfTwo, $"0x{value:x}", name), true);
+ }
+ if ((flags & EventManifestOptions.Strict) != 0)
+ {
+ if (value >= 0x0000100000000000UL && !name.StartsWith("Session", StringComparison.Ordinal))
+ {
+ ManifestError(SR.Format(SR.EventSource_IllegalKeywordsValue, name, $"0x{value:x}"));
+ }
+
+ if (keywordTab != null && keywordTab.TryGetValue(value, out string? prevName) && !name.Equals(prevName, StringComparison.Ordinal))
+ {
+ ManifestError(SR.Format(SR.EventSource_KeywordCollision, name, prevName, $"0x{value:x}"));
+ }
+ }
+
+ keywordTab ??= new Dictionary();
+ keywordTab[value] = name;
+ }
+
+ ///
+ /// Add a channel. channelAttribute can be null
+ ///
+ public void AddChannel(string? name, int value, EventChannelAttribute? channelAttribute)
+ {
+ EventChannel chValue = (EventChannel)value;
+ if (value < (int)EventChannel.Admin || value > 255)
+ ManifestError(SR.Format(SR.EventSource_EventChannelOutOfRange, name, value));
+ else if (chValue >= EventChannel.Admin && chValue <= EventChannel.Debug &&
+ channelAttribute != null && EventChannelToChannelType(chValue) != channelAttribute.EventChannelType)
+ {
+ // we want to ensure developers do not define EventChannels that conflict with the builtin ones,
+ // but we want to allow them to override the default ones...
+ ManifestError(SR.Format(SR.EventSource_ChannelTypeDoesNotMatchEventChannelValue,
+ name, ((EventChannel)value).ToString()));
+ }
+
+ // TODO: validate there are no conflicting manifest exposed names (generally following the format "provider/type")
+
+ ulong kwd = GetChannelKeyword(chValue);
+
+ channelTab ??= new Dictionary(4);
+ channelTab[value] = new ChannelInfo { Name = name, Keywords = kwd, Attribs = channelAttribute };
+ }
+
+ private static EventChannelType EventChannelToChannelType(EventChannel channel)
+ {
+ Debug.Assert(channel >= EventChannel.Admin && channel <= EventChannel.Debug);
+ return (EventChannelType)((int)channel - (int)EventChannel.Admin + (int)EventChannelType.Admin);
+ }
+
+ private static EventChannelAttribute GetDefaultChannelAttribute(EventChannel channel)
+ {
+ EventChannelAttribute attrib = new EventChannelAttribute();
+ attrib.EventChannelType = EventChannelToChannelType(channel);
+ if (attrib.EventChannelType <= EventChannelType.Operational)
+ attrib.Enabled = true;
+ return attrib;
+ }
+
+ public ulong[] GetChannelData()
+ {
+ if (this.channelTab == null)
+ {
+ return [];
+ }
+
+ // We create an array indexed by the channel id for fast look up.
+ // E.g. channelMask[Admin] will give you the bit mask for Admin channel.
+ int maxkey = -1;
+ foreach (int item in this.channelTab.Keys)
+ {
+ if (item > maxkey)
+ {
+ maxkey = item;
+ }
+ }
+
+ ulong[] channelMask = new ulong[maxkey + 1];
+ foreach (KeyValuePair item in this.channelTab)
+ {
+ channelMask[item.Key] = item.Value.Keywords;
+ }
+
+ return channelMask;
+ }
+
+ public void StartEvent(string eventName, EventAttribute eventAttribute)
+ {
+ Debug.Assert(numParams == 0);
+ Debug.Assert(this.eventName == null);
+ this.eventName = eventName;
+ numParams = 0;
+ byteArrArgIndices = null;
+
+ events?.Append(" ");
+ if (type == typeof(byte[]))
+ {
+ // mark this index as "extraneous" (it has no parallel in the managed signature)
+ // we use these values in TranslateToManifestConvention()
+ byteArrArgIndices ??= new List(4);
+ byteArrArgIndices.Add(numParams);
+
+ // add an extra field to the template representing the length of the binary blob
+ numParams++;
+ templates?.Append(" ");
+ }
+ numParams++;
+ templates?.Append(" ();
+ mapsTab.TryAdd(type.Name, type); // Remember that we need to dump the type enumeration
+ }
+
+ templates?.AppendLine("/>");
+ }
+ public void EndEvent()
+ {
+ Debug.Assert(eventName != null);
+
+ if (numParams > 0)
+ {
+ templates?.AppendLine(" ");
+ events?.Append(" template=\"").Append(eventName).Append("Args\"");
+ }
+ events?.AppendLine("/>");
+
+ if (byteArrArgIndices != null)
+ perEventByteArrayArgIndices[eventName] = byteArrArgIndices;
+
+ // at this point we have all the information we need to translate the C# Message
+ // to the manifest string we'll put in the stringTab
+ string prefixedEventName = "event_" + eventName;
+ if (stringTab.TryGetValue(prefixedEventName, out string? msg))
+ {
+ msg = TranslateToManifestConvention(msg, eventName);
+ stringTab[prefixedEventName] = msg;
+ }
+
+ eventName = null;
+ numParams = 0;
+ byteArrArgIndices = null;
+ }
+
+ // Channel keywords are generated one per channel to allow channel based filtering in event viewer. These keywords are autogenerated
+ // by mc.exe for compiling a manifest and are based on the order of the channels (fields) in the Channels inner class (when advanced
+ // channel support is enabled), or based on the order the predefined channels appear in the EventAttribute properties (for simple
+ // support). The manifest generated *MUST* have the channels specified in the same order (that's how our computed keywords are mapped
+ // to channels by the OS infrastructure).
+ // If channelKeyworkds is present, and has keywords bits in the ValidPredefinedChannelKeywords then it is
+ // assumed that the keyword for that channel should be that bit.
+ // otherwise we allocate a channel bit for the channel.
+ // explicit channel bits are only used by WCF to mimic an existing manifest,
+ // so we don't dont do error checking.
+ public ulong GetChannelKeyword(EventChannel channel, ulong channelKeyword = 0)
+ {
+ // strip off any non-channel keywords, since we are only interested in channels here.
+ channelKeyword &= ValidPredefinedChannelKeywords;
+ channelTab ??= new Dictionary(4);
+
+ if (channelTab.Count == MaxCountChannels)
+ ManifestError(SR.EventSource_MaxChannelExceeded);
+
+ if (!channelTab.TryGetValue((int)channel, out ChannelInfo? info))
+ {
+ // If we were not given an explicit channel, allocate one.
+ if (channelKeyword == 0)
+ {
+ channelKeyword = nextChannelKeywordBit;
+ nextChannelKeywordBit >>= 1;
+ }
+ }
+ else
+ {
+ channelKeyword = info.Keywords;
+ }
+
+ return channelKeyword;
+ }
+
+ public byte[] CreateManifest()
+ {
+ string str = CreateManifestString();
+ return (str != "") ? Encoding.UTF8.GetBytes(str) : [];
+ }
+
+ public IList Errors => errors;
+
+ public bool HasResources => resources != null;
+
+ ///
+ /// When validating an event source it adds the error to the error collection.
+ /// When not validating it throws an exception if runtimeCritical is "true".
+ /// Otherwise the error is ignored.
+ ///
+ ///
+ ///
+ public void ManifestError(string msg, bool runtimeCritical = false)
+ {
+ if ((flags & EventManifestOptions.Strict) != 0)
+ errors.Add(msg);
+ else if (runtimeCritical)
+ throw new ArgumentException(msg);
+ }
+
+ private string CreateManifestString()
+ {
+ Span ulongHexScratch = stackalloc char[16]; // long enough for ulong.MaxValue formatted as hex
+
+ // Write out the channels
+ if (channelTab != null)
+ {
+ sb?.AppendLine(" ");
+ var sortedChannels = new List>();
+ foreach (KeyValuePair p in channelTab) { sortedChannels.Add(p); }
+ sortedChannels.Sort((p1, p2) => -Comparer.Default.Compare(p1.Value.Keywords, p2.Value.Keywords));
+
+ foreach (KeyValuePair kvpair in sortedChannels)
+ {
+ int channel = kvpair.Key;
+ ChannelInfo channelInfo = kvpair.Value;
+
+ string? channelType = null;
+ bool enabled = false;
+ string? fullName = null;
+
+ if (channelInfo.Attribs != null)
+ {
+ EventChannelAttribute attribs = channelInfo.Attribs;
+ if (Enum.IsDefined(attribs.EventChannelType))
+ channelType = attribs.EventChannelType.ToString();
+ enabled = attribs.Enabled;
+ }
+
+ fullName ??= providerName + "/" + channelInfo.Name;
+
+ sb?.Append(" ");
+ }
+ sb?.AppendLine(" ");
+ }
+
+ // Write out the tasks
+ if (taskTab != null)
+ {
+ sb?.AppendLine(" ");
+ var sortedTasks = new List(taskTab.Keys);
+ sortedTasks.Sort();
+
+ foreach (int task in sortedTasks)
+ {
+ sb?.Append(" ");
+ }
+ sb?.AppendLine(" ");
+ }
+
+ // Write out the maps
+
+ // Scoping the call to enum GetFields to a local function to limit the trimming suppressions
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
+ Justification = "Trimmer does not trim enums")]
+ static FieldInfo[] GetEnumFields(Type localEnumType)
+ {
+ Debug.Assert(localEnumType.IsEnum);
+ return localEnumType.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
+ }
+
+ if (mapsTab != null)
+ {
+ sb?.AppendLine(" ");
+ foreach (Type enumType in mapsTab.Values)
+ {
+ bool isbitmap = EventSource.IsCustomAttributeDefinedHelper(enumType, typeof(FlagsAttribute), flags);
+ string mapKind = isbitmap ? "bitMap" : "valueMap";
+ sb?.Append(" <").Append(mapKind).Append(" name=\"").Append(enumType.Name).AppendLine("\">");
+
+ // write out each enum value
+ FieldInfo[] staticFields = GetEnumFields(enumType);
+ bool anyValuesWritten = false;
+ foreach (FieldInfo staticField in staticFields)
+ {
+ object? constantValObj = staticField.GetRawConstantValue();
+
+ if (constantValObj != null)
+ {
+ ulong hexValue;
+ if (constantValObj is ulong)
+ hexValue = (ulong)constantValObj; // This is the only integer type that can't be represented by a long.
+ else
+ hexValue = (ulong)Convert.ToInt64(constantValObj); // Handles all integer types except ulong.
+
+ // ETW requires all bitmap values to be powers of 2. Skip the ones that are not.
+ // TODO: Warn people about the dropping of values.
+ if (isbitmap && !BitOperations.IsPow2(hexValue))
+ continue;
+
+ hexValue.TryFormat(ulongHexScratch, out int charsWritten, "x");
+ ReadOnlySpan hexValueFormatted = ulongHexScratch.Slice(0, charsWritten);
+
+ sb?.Append(" ");
+ anyValuesWritten = true;
+ }
+ }
+
+ // the OS requires that bitmaps and valuemaps have at least one value or it reject the whole manifest.
+ // To avoid that put a 'None' entry if there are no other values.
+ if (!anyValuesWritten)
+ {
+ sb?.Append(" ");
+ }
+ sb?.Append(" ").Append(mapKind).AppendLine(">");
+ }
+ sb?.AppendLine(" ");
+ }
+
+ // Write out the opcodes
+ sb?.AppendLine(" ");
+ var sortedOpcodes = new List(opcodeTab.Keys);
+ sortedOpcodes.Sort();
+
+ foreach (int opcode in sortedOpcodes)
+ {
+ sb?.Append(" ");
+ }
+ sb?.AppendLine(" ");
+
+ // Write out the keywords
+ if (keywordTab != null)
+ {
+ sb?.AppendLine(" ");
+ var sortedKeywords = new List(keywordTab.Keys);
+ sortedKeywords.Sort();
+
+ foreach (ulong keyword in sortedKeywords)
+ {
+ sb?.Append(" keywordFormatted = ulongHexScratch.Slice(0, charsWritten);
+ sb?.Append(" mask=\"0x").Append(keywordFormatted).AppendLine("\"/>");
+ }
+ sb?.AppendLine(" ");
+ }
+
+ sb?.AppendLine(" ");
+ sb?.Append(events);
+ sb?.AppendLine(" ");
+
+ sb?.AppendLine(" ");
+ if (templates?.Length > 0)
+ {
+ sb?.Append(templates);
+ }
+ else
+ {
+ // Work around a corner-case ETW issue where a manifest with no templates causes
+ // ETW events to not get sent to their associated channel.
+ sb?.AppendLine(" ");
+ }
+ sb?.AppendLine(" ");
+
+ sb?.AppendLine(" ");
+ sb?.AppendLine(" ");
+ sb?.AppendLine(" ");
+
+ // Output the localization information.
+ sb?.AppendLine("");
+
+ var sortedStrings = new string[stringTab.Keys.Count];
+ stringTab.Keys.CopyTo(sortedStrings, 0);
+ Array.Sort(sortedStrings, StringComparer.Ordinal);
+
+ CultureInfo ci = CultureInfo.CurrentUICulture;
+ sb?.Append(" ");
+ sb?.AppendLine(" ");
+ foreach (string stringKey in sortedStrings)
+ {
+ string? val = GetLocalizedMessage(stringKey, ci, etwFormat: true);
+ sb?.Append(" ");
+ }
+ sb?.AppendLine(" ");
+ sb?.AppendLine(" ");
+
+ sb?.AppendLine(" ");
+ sb?.AppendLine(" ");
+ return sb?.ToString() ?? "";
+ }
+
+#region private
+ private void WriteNameAndMessageAttribs(StringBuilder? stringBuilder, string elementName, string name)
+ {
+ stringBuilder?.Append(" name=\"").Append(name).Append('"');
+ WriteMessageAttrib(sb, elementName, name, name);
+ }
+ private void WriteMessageAttrib(StringBuilder? stringBuilder, string elementName, string name, string? value)
+ {
+ string? key = null;
+
+ // See if the user wants things localized.
+ if (resources != null)
+ {
+ // resource fallback: strings in the neutral culture will take precedence over inline strings
+ key = elementName + "_" + name;
+ if (resources.GetString(key, CultureInfo.InvariantCulture) is string localizedString)
+ value = localizedString;
+ }
+
+ if (value == null)
+ return;
+
+ key ??= elementName + "_" + name;
+ stringBuilder?.Append(" message=\"$(string.").Append(key).Append(")\"");
+
+ if (stringTab.TryGetValue(key, out string? prevValue) && !prevValue.Equals(value))
+ {
+ ManifestError(SR.Format(SR.EventSource_DuplicateStringKey, key), true);
+ return;
+ }
+
+ stringTab[key] = value;
+ }
+ internal string? GetLocalizedMessage(string key, CultureInfo ci, bool etwFormat)
+ {
+ string? value = null;
+ if (resources != null)
+ {
+ string? localizedString = resources.GetString(key, ci);
+ if (localizedString != null)
+ {
+ value = localizedString;
+ if (etwFormat && key.StartsWith("event_", StringComparison.Ordinal))
+ {
+ string evtName = key.Substring("event_".Length);
+ value = TranslateToManifestConvention(value, evtName);
+ }
+ }
+ }
+ if (etwFormat && value == null)
+ stringTab.TryGetValue(key, out value);
+
+ return value;
+ }
+
+ private static void AppendLevelName(StringBuilder? sb, EventLevel level)
+ {
+ if ((int)level < 16)
+ {
+ sb?.Append("win:");
+ }
+
+ sb?.Append(level switch // avoid boxing that comes from level.ToString()
+ {
+ EventLevel.LogAlways => nameof(EventLevel.LogAlways),
+ EventLevel.Critical => nameof(EventLevel.Critical),
+ EventLevel.Error => nameof(EventLevel.Error),
+ EventLevel.Warning => nameof(EventLevel.Warning),
+ EventLevel.Informational => nameof(EventLevel.Informational),
+ EventLevel.Verbose => nameof(EventLevel.Verbose),
+ _ => ((int)level).ToString()
+ });
+ }
+
+ private string? GetChannelName(EventChannel channel, string eventName, string? eventMessage)
+ {
+ if (channelTab == null || !channelTab.TryGetValue((int)channel, out ChannelInfo? info))
+ {
+ if (channel < EventChannel.Admin) // || channel > EventChannel.Debug)
+ ManifestError(SR.Format(SR.EventSource_UndefinedChannel, channel, eventName));
+
+ // allow channels to be auto-defined. The well known ones get their well known names, and the
+ // rest get names Channel. This allows users to modify the Manifest if they want more advanced features.
+ channelTab ??= new Dictionary(4);
+
+ string channelName = channel.ToString(); // For well know channels this is a nice name, otherwise a number
+ if (EventChannel.Debug < channel)
+ channelName = "Channel" + channelName; // Add a 'Channel' prefix for numbers.
+
+ AddChannel(channelName, (int)channel, GetDefaultChannelAttribute(channel));
+ if (!channelTab.TryGetValue((int)channel, out info))
+ ManifestError(SR.Format(SR.EventSource_UndefinedChannel, channel, eventName));
+ }
+ // events that specify admin channels *must* have non-null "Message" attributes
+ if (resources != null)
+ eventMessage ??= resources.GetString("event_" + eventName, CultureInfo.InvariantCulture);
+
+ Debug.Assert(info!.Attribs != null);
+ if (info.Attribs.EventChannelType == EventChannelType.Admin && eventMessage == null)
+ ManifestError(SR.Format(SR.EventSource_EventWithAdminChannelMustHaveMessage, eventName, info.Name));
+ return info.Name;
+ }
+ private string GetTaskName(EventTask task, string eventName)
+ {
+ if (task == EventTask.None)
+ return "";
+
+ taskTab ??= new Dictionary();
+ if (!taskTab.TryGetValue((int)task, out string? ret))
+ ret = taskTab[(int)task] = eventName;
+ return ret;
+ }
+
+ private string? GetOpcodeName(EventOpcode opcode, string eventName)
+ {
+ switch (opcode)
+ {
+ case EventOpcode.Info:
+ return "win:Info";
+ case EventOpcode.Start:
+ return "win:Start";
+ case EventOpcode.Stop:
+ return "win:Stop";
+ case EventOpcode.DataCollectionStart:
+ return "win:DC_Start";
+ case EventOpcode.DataCollectionStop:
+ return "win:DC_Stop";
+ case EventOpcode.Extension:
+ return "win:Extension";
+ case EventOpcode.Reply:
+ return "win:Reply";
+ case EventOpcode.Resume:
+ return "win:Resume";
+ case EventOpcode.Suspend:
+ return "win:Suspend";
+ case EventOpcode.Send:
+ return "win:Send";
+ case EventOpcode.Receive:
+ return "win:Receive";
+ }
+
+ if (opcodeTab == null || !opcodeTab.TryGetValue((int)opcode, out string? ret))
+ {
+ ManifestError(SR.Format(SR.EventSource_UndefinedOpcode, opcode, eventName), true);
+ ret = null;
+ }
+
+ return ret;
+ }
+
+ private void AppendKeywords(StringBuilder? sb, ulong keywords, string eventName)
+ {
+ // ignore keywords associate with channels
+ // See ValidPredefinedChannelKeywords def for more.
+ keywords &= ~ValidPredefinedChannelKeywords;
+
+ bool appended = false;
+ for (ulong bit = 1; bit != 0; bit <<= 1)
+ {
+ if ((keywords & bit) != 0)
+ {
+ string? keyword = null;
+ if ((keywordTab == null || !keywordTab.TryGetValue(bit, out keyword)) &&
+ (bit >= (ulong)0x1000000000000))
+ {
+ // do not report Windows reserved keywords in the manifest (this allows the code
+ // to be resilient to potential renaming of these keywords)
+ keyword = string.Empty;
+ }
+ if (keyword == null)
+ {
+ ManifestError(SR.Format(SR.EventSource_UndefinedKeyword, "0x" + bit.ToString("x", CultureInfo.CurrentCulture), eventName), true);
+ keyword = string.Empty;
+ }
+
+ if (keyword.Length != 0)
+ {
+ if (appended)
+ {
+ sb?.Append(' ');
+ }
+
+ sb?.Append(keyword);
+ appended = true;
+ }
+ }
+ }
+ }
+
+ private string GetTypeName(Type type)
+ {
+ if (type.IsEnum)
+ {
+ string typeName = GetTypeName(type.GetEnumUnderlyingType());
+ return typeName switch // ETW requires enums to be unsigned.
+ {
+ "win:Int8" => "win:UInt8",
+ "win:Int16" => "win:UInt16",
+ "win:Int32" => "win:UInt32",
+ "win:Int64" => "win:UInt64",
+ _ => typeName,
+ };
+ }
+
+ switch (Type.GetTypeCode(type))
+ {
+ case TypeCode.Boolean:
+ return "win:Boolean";
+ case TypeCode.Byte:
+ return "win:UInt8";
+ case TypeCode.Char:
+ case TypeCode.UInt16:
+ return "win:UInt16";
+ case TypeCode.UInt32:
+ return "win:UInt32";
+ case TypeCode.UInt64:
+ return "win:UInt64";
+ case TypeCode.SByte:
+ return "win:Int8";
+ case TypeCode.Int16:
+ return "win:Int16";
+ case TypeCode.Int32:
+ return "win:Int32";
+ case TypeCode.Int64:
+ return "win:Int64";
+ case TypeCode.String:
+ return "win:UnicodeString";
+ case TypeCode.Single:
+ return "win:Float";
+ case TypeCode.Double:
+ return "win:Double";
+ case TypeCode.DateTime:
+ return "win:FILETIME";
+ default:
+ if (type == typeof(Guid))
+ return "win:GUID";
+ else if (type == typeof(IntPtr))
+ return "win:Pointer";
+ else if ((type.IsArray || type.IsPointer) && type.GetElementType() == typeof(byte))
+ return "win:Binary";
+
+ ManifestError(SR.Format(SR.EventSource_UnsupportedEventTypeInManifest, type.Name), true);
+ return string.Empty;
+ }
+ }
+
+ private static void UpdateStringBuilder([NotNull] ref StringBuilder? stringBuilder, string eventMessage, int startIndex, int count)
+ {
+ stringBuilder ??= new StringBuilder();
+ stringBuilder.Append(eventMessage, startIndex, count);
+ }
+
+ private static readonly string[] s_escapes = ["&", "<", ">", "'", """, "%r", "%n", "%t"];
+ // Manifest messages use %N conventions for their message substitutions. Translate from
+ // .NET conventions. We can't use RegEx for this (we are in mscorlib), so we do it 'by hand'
+ private string TranslateToManifestConvention(string eventMessage, string evtName)
+ {
+ StringBuilder? stringBuilder = null; // We lazily create this
+ int writtenSoFar = 0;
+ for (int i = 0; ;)
+ {
+ if (i >= eventMessage.Length)
+ {
+ if (stringBuilder == null)
+ return eventMessage;
+ UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
+ return stringBuilder.ToString();
+ }
+
+ int chIdx;
+ if (eventMessage[i] == '%')
+ {
+ // handle format message escaping character '%' by escaping it
+ UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
+ stringBuilder.Append("%%");
+ i++;
+ writtenSoFar = i;
+ }
+ else if (i < eventMessage.Length - 1 &&
+ (eventMessage[i] == '{' && eventMessage[i + 1] == '{' || eventMessage[i] == '}' && eventMessage[i + 1] == '}'))
+ {
+ // handle C# escaped '{" and '}'
+ UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
+ stringBuilder.Append(eventMessage[i]);
+ i++; i++;
+ writtenSoFar = i;
+ }
+ else if (eventMessage[i] == '{')
+ {
+ int leftBracket = i;
+ i++;
+ int argNum = 0;
+ while (i < eventMessage.Length && char.IsDigit(eventMessage[i]))
+ {
+ argNum = argNum * 10 + eventMessage[i] - '0';
+ i++;
+ }
+ if (i < eventMessage.Length && eventMessage[i] == '}')
+ {
+ i++;
+ UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, leftBracket - writtenSoFar);
+ int manIndex = TranslateIndexToManifestConvention(argNum, evtName);
+ stringBuilder.Append('%').Append(manIndex);
+ // An '!' after the insert specifier {n} will be interpreted as a literal.
+ // We'll escape it so that mc.exe does not attempt to consider it the
+ // beginning of a format string.
+ if (i < eventMessage.Length && eventMessage[i] == '!')
+ {
+ i++;
+ stringBuilder.Append("%!");
+ }
+ writtenSoFar = i;
+ }
+ else
+ {
+ ManifestError(SR.Format(SR.EventSource_UnsupportedMessageProperty, evtName, eventMessage));
+ }
+ }
+ else if ((chIdx = "&<>'\"\r\n\t".IndexOf(eventMessage[i])) >= 0)
+ {
+ UpdateStringBuilder(ref stringBuilder, eventMessage, writtenSoFar, i - writtenSoFar);
+ i++;
+ stringBuilder.Append(s_escapes[chIdx]);
+ writtenSoFar = i;
+ }
+ else
+ i++;
+ }
+ }
+
+ private int TranslateIndexToManifestConvention(int idx, string evtName)
+ {
+ if (perEventByteArrayArgIndices.TryGetValue(evtName, out List? byteArrArgIndices))
+ {
+ foreach (int byArrIdx in byteArrArgIndices)
+ {
+ if (idx >= byArrIdx)
+ ++idx;
+ else
+ break;
+ }
+ }
+ return idx + 1;
+ }
+
+ private sealed class ChannelInfo
+ {
+ public string? Name;
+ public ulong Keywords;
+ public EventChannelAttribute? Attribs;
+ }
+
+ private readonly Dictionary opcodeTab;
+ private Dictionary? taskTab;
+ private Dictionary? channelTab;
+ private Dictionary? keywordTab;
+ private Dictionary? mapsTab;
+ private readonly Dictionary stringTab; // Maps unlocalized strings to localized ones
+
+ // WCF used EventSource to mimic a existing ETW manifest. To support this
+ // in just their case, we allowed them to specify the keywords associated
+ // with their channels explicitly. ValidPredefinedChannelKeywords is
+ // this set of channel keywords that we allow to be explicitly set. You
+ // can ignore these bits otherwise.
+ internal const ulong ValidPredefinedChannelKeywords = 0xF000000000000000;
+ private ulong nextChannelKeywordBit = 0x8000000000000000; // available Keyword bit to be used for next channel definition, grows down
+ private const int MaxCountChannels = 8; // a manifest can defined at most 8 ETW channels
+
+ private readonly StringBuilder? sb; // Holds the provider information.
+ private readonly StringBuilder? events; // Holds the events.
+ private readonly StringBuilder? templates;
+ private readonly string providerName;
+ private readonly ResourceManager? resources; // Look up localized strings here.
+ private readonly EventManifestOptions flags;
+ private readonly List errors; // list of currently encountered errors
+ private readonly Dictionary> perEventByteArrayArgIndices; // "event_name" -> List_of_Indices_of_Byte[]_Arg
+
+ // State we track between StartEvent and EndEvent.
+ private string? eventName; // Name of the event currently being processed.
+ private int numParams; // keeps track of the number of args the event has.
+ private List? byteArrArgIndices; // keeps track of the index of each byte[] argument
+#endregion
+}