diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Static.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Static.cs index 11cb2672..6b12901c 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Static.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDevice.Static.cs @@ -54,6 +54,11 @@ public partial class YubiKeyDevice /// using their serial number. If they cannot be matched, /// each connection will be returned as a separate . /// + /// + /// If your application no longer needs to watch for insertion or removal notifications, + /// you can call to release resources + /// and avoid the logging and other actions from the listeners. + /// /// /// /// Argument controls which devices are searched for. Values diff --git a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceListener.cs b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceListener.cs index fd9c2830..89a1c856 100644 --- a/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceListener.cs +++ b/Yubico.YubiKey/src/Yubico/YubiKey/YubiKeyDeviceListener.cs @@ -47,10 +47,32 @@ public class YubiKeyDeviceListener : IDisposable /// public static YubiKeyDeviceListener Instance => _lazyInstance ??= new YubiKeyDeviceListener(); + internal static bool IsListenerRunning => !(_lazyInstance is null); + /// /// Disposes and closes the singleton instance of . /// - public static void ResetInstance() + /// + /// + /// Enumerating YubiKeys is actually done via a cache. As such, this cache must be maintained + /// and kept up-to-date. This is done by starting several listeners that run in the background. + /// These listen for the relevant OS device arrival and removal events. + /// + /// + /// Normally, these background listeners will run starting with the first enumeration call to the + /// SDK and remain active until the process shuts down. But there are cases where you may not want + /// the overhead of these listeners running all the time. While they do their best to not consume + /// excessive resources, they can sometimes generate log noise, exceptions, etc. + /// + /// + /// This method allows you to stop these + /// background listeners and reclaim resources, as possible. This will not invalidate any existing + /// IYubiKeyDevice instances, however you will not receive any additional events regarding that device. + /// Any subsequent calls to , , + /// or will restart the listeners. + /// + /// + public static void StopListening() { if (_lazyInstance != null) { @@ -63,7 +85,7 @@ public static void ResetInstance() private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - private readonly Logger _log = Log.GetLogger(); + private readonly ILogger _log = Log.GetLogger(); private readonly Dictionary _internalCache = new Dictionary(); private readonly HidDeviceListener _hidListener = HidDeviceListener.Create(); private readonly SmartCardDeviceListener _smartCardListener = SmartCardDeviceListener.Create(); @@ -77,7 +99,7 @@ public static void ResetInstance() private YubiKeyDeviceListener() { - _log.LogInformation("Creating YubiKeyDeviceListener instance."); + _log.LogInformation($"Creating {nameof(YubiKeyDeviceListener)} instance."); SetupDeviceListeners(); @@ -89,14 +111,45 @@ private YubiKeyDeviceListener() internal List GetAll() => _internalCache.Keys.ToList(); - private void ListenerHandler(object? sender, EventArgs e) => _semaphore.Release(); + private void ArriveHandler(object? sender, EventArgs e) => ListenerHandler("Arrival", e); + + private void RemoveHandler(object? sender, EventArgs e) => ListenerHandler("Removal", e); + + private void ListenerHandler(string eventType, EventArgs e) + { + object? device; + string deviceType; + if (e is SmartCardDeviceEventArgs se) + { + deviceType = "smart card"; + device = se.Device; + } + else if (e is HidDeviceEventArgs he) + { + deviceType = "HID"; + device = he.Device; + } + else + { + // Given this is a private method, this case isn't likely. + deviceType = "unknown"; + device = "undefined"; + } + + _log.LogInformation( + "{EventType} of {DeviceType} {Device} is triggering update.", + eventType, + deviceType, + device); + _ = _semaphore.Release(); + } private void SetupDeviceListeners() { - _smartCardListener.Arrived += ListenerHandler; - _smartCardListener.Removed += ListenerHandler; - _hidListener.Arrived += ListenerHandler; - _hidListener.Removed += ListenerHandler; + _smartCardListener.Arrived += ArriveHandler; + _smartCardListener.Removed += RemoveHandler; + _hidListener.Arrived += ArriveHandler; + _hidListener.Removed += RemoveHandler; } private async Task ListenForChanges() @@ -368,21 +421,21 @@ protected virtual void Dispose(bool disposing) _tokenSource.Cancel(); // Shut down the listener handlers. - _hidListener.Arrived -= ListenerHandler; - _hidListener.Removed -= ListenerHandler; + _hidListener.Arrived -= ArriveHandler; + _hidListener.Removed -= RemoveHandler; if (_hidListener is IDisposable hidDisp) { hidDisp.Dispose(); } - _smartCardListener.Arrived -= ListenerHandler; - _smartCardListener.Removed -= ListenerHandler; + _smartCardListener.Arrived -= ArriveHandler; + _smartCardListener.Removed -= RemoveHandler; if (_smartCardListener is IDisposable scDisp) { scDisp.Dispose(); } - // Give the listen thread a moment (likely is already done). + // Give the listen task a moment (likely is already done). _ = !_listenTask.Wait(100); _listenTask.Dispose(); @@ -390,6 +443,11 @@ protected virtual void Dispose(bool disposing) _rwLock.Dispose(); _semaphore.Dispose(); _tokenSource.Dispose(); + + if (ReferenceEquals(_lazyInstance, this)) + { + _lazyInstance = null; + } } _isDisposed = true; } diff --git a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiKeyTests.cs b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiKeyTests.cs index e73a7d54..bf5de8ab 100644 --- a/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiKeyTests.cs +++ b/Yubico.YubiKey/tests/integration/Yubico/YubiKey/YubiKeyTests.cs @@ -221,5 +221,29 @@ public void GetYubiKeys_SingleTransport_RapidSwitching() _testOutputHelper.WriteLine($"\t({keys.Count}) -{sw.ElapsedMilliseconds,5}ms"); } } + + [Fact] + public void TestResettingDeviceListener() + { + // Get devices (if any) and ensure the listeners are running. + List beforeDevices = YubiKeyDevice.FindAll().ToList(); + _testOutputHelper.WriteLine($"Found {beforeDevices.Count} YubiKey devices before reset"); + + // Test that the listeners are running. + Assert.True(YubiKeyDeviceListener.IsListenerRunning, $"{nameof(YubiKeyDeviceListener.Instance)} is not active"); + + // Stop the listeners. + YubiKeyDeviceListener.StopListening(); + + // Test that we really stopped it. + Assert.False(YubiKeyDeviceListener.IsListenerRunning, $"{nameof(YubiKeyDeviceListener.Instance)} is still active"); + + // Make sure we can still enumerate devices. + List afterDevices = YubiKeyDevice.FindAll().ToList(); + _testOutputHelper.WriteLine($"Found {afterDevices.Count} YubiKey devices after reset"); + + // Check that we have the same devices as the first check. + Assert.True(afterDevices.SequenceEqual(beforeDevices), "Before and after aren't the same."); + } } }