-
Notifications
You must be signed in to change notification settings - Fork 62
Fix: High idle CPU in RDS/Terminal Server environments (SCARD_E_INVALID_HANDLE) #445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
fb88e38
be88315
6fa3e02
a9964fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -554,4 +554,5 @@ cython_debug/ | |
|
|
||
| # Coverage / Test Results | ||
| coveragereport/ | ||
| TestResults/ | ||
| TestResults/ | ||
| Directory.Packages.props | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||||
| // Copyright 2025 Yubico AB | ||||||||||||||
| // Copyright 2025 Yubico AB | ||||||||||||||
| // | ||||||||||||||
| // Licensed under the Apache License, Version 2.0 (the "License"). | ||||||||||||||
| // You may not use this file except in compliance with the License. | ||||||||||||||
|
|
@@ -20,8 +20,6 @@ | |||||||||||||
| using Microsoft.Extensions.Logging; | ||||||||||||||
| using Yubico.PlatformInterop; | ||||||||||||||
|
|
||||||||||||||
| using static Yubico.PlatformInterop.NativeMethods; | ||||||||||||||
|
|
||||||||||||||
| namespace Yubico.Core.Devices.SmartCard | ||||||||||||||
| { | ||||||||||||||
| /// <summary> | ||||||||||||||
|
|
@@ -31,6 +29,7 @@ internal class DesktopSmartCardDeviceListener : SmartCardDeviceListener | |||||||||||||
| { | ||||||||||||||
| private static readonly string[] readerNames = new[] { "\\\\?\\Pnp\\Notifications" }; | ||||||||||||||
| private readonly ILogger _log = Logging.Log.GetLogger<DesktopSmartCardDeviceListener>(); | ||||||||||||||
| private readonly ISCardInterop _scard; | ||||||||||||||
|
|
||||||||||||||
| // The resource manager context. | ||||||||||||||
| private SCardContext _context; | ||||||||||||||
|
|
@@ -46,19 +45,37 @@ internal class DesktopSmartCardDeviceListener : SmartCardDeviceListener | |||||||||||||
| private static readonly TimeSpan MaxDisposalWaitTime = TimeSpan.FromSeconds(8); | ||||||||||||||
| private static readonly TimeSpan CheckForChangesWaitTime = TimeSpan.FromMilliseconds(100); | ||||||||||||||
|
|
||||||||||||||
| // How long to back off after a recoverable SCard error before retrying. | ||||||||||||||
| // Prevents a tight polling loop when SCardGetStatusChange returns immediately (e.g. | ||||||||||||||
| // SCARD_E_INVALID_HANDLE in an RDS environment). See GitHub issue #434. | ||||||||||||||
| private static readonly TimeSpan RecoveryBackoffDelay = TimeSpan.FromMilliseconds(1000); | ||||||||||||||
|
|
||||||||||||||
| /// <summary> | ||||||||||||||
| /// Constructs a <see cref="SmartCardDeviceListener"/> using the system SCard implementation. | ||||||||||||||
| /// </summary> | ||||||||||||||
| public DesktopSmartCardDeviceListener() : this(new SCardInterop()) | ||||||||||||||
| { | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// <summary> | ||||||||||||||
| /// Constructs a <see cref="SmartCardDeviceListener"/>. | ||||||||||||||
| /// Internal constructor that accepts a test double for the SCard API surface. | ||||||||||||||
| /// </summary> | ||||||||||||||
| public DesktopSmartCardDeviceListener() | ||||||||||||||
| internal DesktopSmartCardDeviceListener(ISCardInterop scard) | ||||||||||||||
| { | ||||||||||||||
|
||||||||||||||
| { | |
| { | |
| if (scard is null) | |
| { | |
| throw new ArgumentNullException(nameof(scard)); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // Copyright 2025 Yubico AB | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"). | ||
| // You may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| namespace Yubico.PlatformInterop | ||
| { | ||
| /// <summary> | ||
| /// Abstraction over the WinSCard / PCSC smart card API surface used by the device listener. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Exists primarily to enable injection of test doubles so that error-handling paths in | ||
| /// <c>DesktopSmartCardDeviceListener</c> can be exercised without requiring real smart card | ||
| /// hardware or a Windows terminal-services environment. | ||
| /// </remarks> | ||
| internal interface ISCardInterop | ||
| { | ||
| /// <summary>Wraps SCardEstablishContext.</summary> | ||
| uint EstablishContext(SCARD_SCOPE scope, out SCardContext context); | ||
|
|
||
| /// <summary>Wraps SCardGetStatusChange.</summary> | ||
| uint GetStatusChange(SCardContext context, int timeout, SCARD_READER_STATE[] readerStates, int readerStatesCount); | ||
|
|
||
| /// <summary>Wraps the high-level SCardListReaders overload that handles the two-call Windows pattern.</summary> | ||
| uint ListReaders(SCardContext context, string[]? groups, out string[] readerNames); | ||
|
|
||
| /// <summary>Wraps SCardCancel.</summary> | ||
| uint Cancel(SCardContext context); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // Copyright 2025 Yubico AB | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"). | ||
| // You may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| namespace Yubico.PlatformInterop | ||
| { | ||
| /// <summary> | ||
| /// Production implementation of <see cref="ISCardInterop"/> that delegates directly to | ||
| /// <see cref="NativeMethods"/> P/Invoke declarations. | ||
| /// </summary> | ||
| internal sealed class SCardInterop : ISCardInterop | ||
| { | ||
| public uint EstablishContext(SCARD_SCOPE scope, out SCardContext context) => | ||
| NativeMethods.SCardEstablishContext(scope, out context); | ||
|
|
||
| public uint GetStatusChange(SCardContext context, int timeout, SCARD_READER_STATE[] readerStates, int readerStatesCount) => | ||
| NativeMethods.SCardGetStatusChange(context, timeout, readerStates, readerStatesCount); | ||
|
|
||
| public uint ListReaders(SCardContext context, string[]? groups, out string[] readerNames) => | ||
| NativeMethods.SCardListReaders(context, groups, out readerNames); | ||
|
|
||
| public uint Cancel(SCardContext context) => | ||
| NativeMethods.SCardCancel(context); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The special PC/SC PnP notification reader name literals look inconsistent and likely incorrect:
readerNamesuses"\\\\?\\Pnp\\Notifications"(pluralNotifications)GetReaderStateListprepends"\\\\?PnP?\\Notification"(different escaping/placement)WinSCard/PCSC uses the virtual reader
\\?\\PnP\\Notification(singular). Consider replacing both with a single shared constant using the canonical value to avoid relying on fallback behavior and to make behavior consistent across platforms.