// 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; /// <summary> /// An EventListener represents a target for the events generated by EventSources (that is subclasses /// of <see cref="EventSource"/>), 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. /// <para> /// Once created, EventListeners can enable or disable on a per-eventSource basis using verbosity levels /// (<see cref="EventLevel"/>) and bitfields (<see cref="EventKeywords"/>) 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. /// </para><para> /// 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. /// </para><para> /// 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. /// </para><para> /// 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. /// </para><para> /// Listeners receive the events on their 'OnEventWritten' method. Thus subclasses of EventListener must /// override this method to do something useful with the data. /// </para><para> /// 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. /// </para> /// </summary> public abstract class EventListener : IDisposable { private event EventHandler<EventSourceCreatedEventArgs>? _EventSourceCreated; /// <summary> /// 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. /// </summary> public event EventHandler<EventSourceCreatedEventArgs>? EventSourceCreated { add { CallBackForExistingEventSources(false, value); this._EventSourceCreated = (EventHandler<EventSourceCreatedEventArgs>?)Delegate.Combine(_EventSourceCreated, value); } remove { this._EventSourceCreated = (EventHandler<EventSourceCreatedEventArgs>?)Delegate.Remove(_EventSourceCreated, value); } } /// <summary> /// This event is raised whenever an event has been written by a EventSource for which /// the EventListener has enabled events. /// </summary> public event EventHandler<EventWrittenEventArgs>? EventWritten; /// <summary> /// Create a new EventListener in which all events start off turned off (use EnableEvents to turn /// them on). /// </summary> protected EventListener() { // This will cause the OnEventSourceCreated callback to fire. CallBackForExistingEventSources(true, (obj, args) => args.EventSource!.AddListener((EventListener)obj!)); } /// <summary> /// 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. /// </summary> 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 /// <summary> /// 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. /// /// </summary> public void EnableEvents(EventSource eventSource, EventLevel level) { EnableEvents(eventSource, level, EventKeywords.None); } /// <summary> /// 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. /// </summary> public void EnableEvents(EventSource eventSource, EventLevel level, EventKeywords matchAnyKeyword) { EnableEvents(eventSource, level, matchAnyKeyword, null); } /// <summary> /// 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. /// </summary> public void EnableEvents(EventSource eventSource, EventLevel level, EventKeywords matchAnyKeyword, IDictionary<string, string?>? 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 } /// <summary> /// Disables all events coming from eventSource identified by 'eventSource'. /// /// This call never has an effect on other EventListeners. /// </summary> 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 } /// <summary> /// 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)). /// </summary> protected internal static int EventSourceIndex(EventSource eventSource) { return eventSource.m_id; } /// <summary> /// 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. /// </summary> /// <param name="eventSource"></param> protected internal virtual void OnEventSourceCreated(EventSource eventSource) { EventHandler<EventSourceCreatedEventArgs>? callBack = this._EventSourceCreated; if (callBack != null) { EventSourceCreatedEventArgs args = new EventSourceCreatedEventArgs(); args.EventSource = eventSource; callBack(this, args); } } /// <summary> /// This method is called whenever an event has been written by a EventSource for which /// the EventListener has enabled events. /// </summary> /// <param name="eventData"></param> protected internal virtual void OnEventWritten(EventWrittenEventArgs eventData) { this.EventWritten?.Invoke(this, eventData); } #region private /// <summary> /// 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). /// </summary> /// <param name="newEventSource"></param> 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<EventSource> 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<EventSource>(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<EventSource> sourcesToDispose = new List<EventSource>(); lock (EventListenersLock) { Debug.Assert(s_EventSources != null); foreach (WeakReference<EventSource> 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 } /// <summary> /// 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. /// </summary> 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<EventSource>[] eventSourcesSnapshot = s_EventSources.ToArray(); foreach (WeakReference<EventSource> 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<EventSource> 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; } } } } } /// <summary> /// Checks internal consistency of EventSources/Listeners. /// </summary> [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<EventListener, bool> allListeners = new Dictionary<EventListener, bool>(); EventListener? cur = s_Listeners; while (cur != null) { allListeners.Add(cur, true); cur = cur.m_Next; } // For all eventSources int id = -1; foreach (WeakReference<EventSource> 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; } } } } } /// <summary> /// 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) /// </summary> internal static object EventListenersLock { get { if (s_EventSources == null) { Interlocked.CompareExchange(ref s_EventSources, new List<WeakReference<EventSource>>(2), null); } return s_EventSources; } } private void CallBackForExistingEventSources(bool addToListenersList, EventHandler<EventSourceCreatedEventArgs>? 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<EventSource>[] eventSourcesSnapshot = s_EventSources.ToArray(); #if DEBUG bool previousValue = s_ConnectingEventSourcesAndListener; s_ConnectingEventSourcesAndListener = true; try { #endif for (int i = 0; i < eventSourcesSnapshot.Length; i++) { WeakReference<EventSource> 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 /// <summary> /// 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. /// </summary> internal static EventListener? s_Listeners; /// <summary> /// 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. /// </summary> internal static List<WeakReference<EventSource>>? s_EventSources; /// <summary> /// Used to disallow reentrancy. /// </summary> private static bool s_CreatingListener; #if DEBUG /// <summary> /// 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. /// </summary> [ThreadStatic] private static bool s_ConnectingEventSourcesAndListener; #endif #endregion }