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(" "); - } - 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(" "); + } + 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 +}