// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis.ErrorReporting; namespace Roslyn.Utilities { internal class EventMap { private readonly NonReentrantLock _guard = new NonReentrantLock(); private readonly Dictionary _eventNameToRegistries = new Dictionary(); public EventMap() { } public void AddEventHandler(string eventName, TEventHandler eventHandler) where TEventHandler : class { using (_guard.DisposableWait()) { var registries = GetRegistries_NoLock(eventName); var newRegistries = registries.Add(new Registry(eventHandler)); SetRegistries_NoLock(eventName, newRegistries); } } public void RemoveEventHandler(string eventName, TEventHandler eventHandler) where TEventHandler : class { using (_guard.DisposableWait()) { var registries = GetRegistries_NoLock(eventName); // remove disabled registrations from list var newRegistries = registries.RemoveAll(r => r.HasHandler(eventHandler)); if (newRegistries != registries) { // disable all registrations of this handler (so pending raise events can be squelched) // This does not guarantee no race condition between Raise and Remove but greatly reduces it. foreach (var registry in registries.Where(r => r.HasHandler(eventHandler))) { registry.Unregister(); } SetRegistries_NoLock(eventName, newRegistries); } } } public EventHandlerSet GetEventHandlers(string eventName) where TEventHandler : class { return new EventHandlerSet(this.GetRegistries(eventName)); } private ImmutableArray> GetRegistries(string eventName) where TEventHandler : class { using (_guard.DisposableWait()) { return GetRegistries_NoLock(eventName); } } private ImmutableArray> GetRegistries_NoLock(string eventName) where TEventHandler : class { _guard.AssertHasLock(); if (_eventNameToRegistries.TryGetValue(eventName, out var registries)) { return (ImmutableArray>)registries; } return ImmutableArray.Create>(); } private void SetRegistries_NoLock(string eventName, ImmutableArray> registries) where TEventHandler : class { _guard.AssertHasLock(); _eventNameToRegistries[eventName] = registries; } private class Registry : IEquatable> where TEventHandler : class { private TEventHandler _handler; public Registry(TEventHandler handler) { _handler = handler; } public void Unregister() { _handler = null; } public void Invoke(Action invoker) { var handler = _handler; if (handler != null) { invoker(handler); } } public bool HasHandler(TEventHandler handler) { return handler.Equals(_handler); } public bool Equals(Registry other) { if (other == null) { return false; } if (other._handler == null && _handler == null) { return true; } if (other._handler == null || _handler == null) { return false; } return other._handler.Equals(_handler); } public override bool Equals(object obj) { return Equals(obj as Registry); } public override int GetHashCode() { return _handler == null ? 0 : _handler.GetHashCode(); } } internal struct EventHandlerSet where TEventHandler : class { private ImmutableArray> _registries; internal EventHandlerSet(object registries) { _registries = (ImmutableArray>)registries; } public bool HasHandlers { get { return _registries != null && _registries.Length > 0; } } public void RaiseEvent(Action invoker) { // The try/catch here is to find additional telemetry for https://devdiv.visualstudio.com/DevDiv/_queries/query/71ee8553-7220-4b2a-98cf-20edab701fd1/. // We've realized there's a problem with our eventing, where if an exception is encountered while calling into subscribers to Workspace events, // we won't notify all of the callers. The expectation is such an exception would be thrown to the SafeStartNew in the workspace's event queue that // will raise that as a fatal exception, but OperationCancelledExceptions might be able to propagate through and fault the task we are using in the // chain. I'm choosing to use ReportWithoutCrashAndPropagate, because if our theory here is correct, it seems the first exception isn't actually // causing crashes, and so if it turns out this is a very common situation I don't want to make a often-benign situation fatal. try { if (this.HasHandlers) { foreach (var registry in _registries) { registry.Invoke(invoker); } } } catch (Exception e) when (FatalError.ReportWithoutCrashAndPropagate(e)) { throw ExceptionUtilities.Unreachable; } } } } }