13
13
// limitations under the License.
14
14
15
15
using System ;
16
+ using System . Collections . Concurrent ;
16
17
using System . Globalization ;
17
- using System . Linq ;
18
18
using System . Runtime . InteropServices ;
19
19
using System . Text ;
20
20
using Yubico . Core . Buffers ;
@@ -35,8 +35,12 @@ internal sealed class MacOSHidIOReportConnection : IHidConnection
35
35
private readonly MacOSHidDevice _device ;
36
36
private readonly IntPtr _loopId ;
37
37
private readonly Logger _log = Log . GetLogger ( ) ;
38
+
38
39
private readonly byte [ ] _readBuffer ;
39
- private readonly GCHandle _readHandle ;
40
+ private GCHandle _readHandle ;
41
+
42
+ private readonly ConcurrentQueue < byte [ ] > _reportsQueue ;
43
+ private GCHandle _pinnedReportsQueue ;
40
44
41
45
/// <summary>
42
46
/// The correct size, in bytes, for the data buffer to be transmitted to the device.
@@ -67,9 +71,15 @@ public MacOSHidIOReportConnection(MacOSHidDevice device, long entryId)
67
71
byte [ ] cstr = Encoding . UTF8 . GetBytes ( $ "fido2-loopid-{ entryId } ") ;
68
72
_loopId = CFStringCreateWithCString ( IntPtr . Zero , cstr , 0 ) ;
69
73
74
+ // The following buffer must be pinned because the native function must retain a pointer (i.e. the address)
70
75
_readBuffer = new byte [ 64 ] ;
71
76
_readHandle = GCHandle . Alloc ( _readBuffer , GCHandleType . Pinned ) ;
72
77
78
+ // This object is marshalled using a normal handle since .NET is on the other side and is capable of using
79
+ // the GCHandle (i.e. we do not need the address)
80
+ _reportsQueue = new ConcurrentQueue < byte [ ] > ( ) ;
81
+ _pinnedReportsQueue = GCHandle . Alloc ( _reportsQueue ) ;
82
+
73
83
SetupConnection ( ) ;
74
84
75
85
InputReportSize = IOKitHelpers . GetIntPropertyValue ( _deviceHandle , IOKitHidConstants . MaxInputReportSize ) ;
@@ -124,7 +134,7 @@ private void SetupConnection()
124
134
_readBuffer ,
125
135
_readBuffer . Length ,
126
136
reportCallback ,
127
- GCHandle . ToIntPtr ( _readHandle ) ) ;
137
+ GCHandle . ToIntPtr ( _pinnedReportsQueue ) ) ;
128
138
129
139
IntPtr callback = Marshal . GetFunctionPointerForDelegate < IOHIDCallback > ( RemovalCallback ) ;
130
140
IOHIDDeviceRegisterRemovalCallback ( _deviceHandle , callback , _deviceHandle ) ;
@@ -150,49 +160,50 @@ private void SetupConnection()
150
160
/// </exception>
151
161
public byte [ ] GetReport ( )
152
162
{
153
- try
163
+ if ( _reportsQueue . TryDequeue ( out byte [ ] report ) )
154
164
{
155
- IntPtr runLoop = CFRunLoopGetCurrent ( ) ;
165
+ // If there's already a report in the queue (i.e. the callback beat us to calling GetReport) return
166
+ // that one immediately.
167
+ _log . SensitiveLogInformation (
168
+ "GetReport returned buffer: {Report}" ,
169
+ Hex . BytesToHex ( report ) ) ;
156
170
157
- IOHIDDeviceScheduleWithRunLoop ( _deviceHandle , runLoop , _loopId ) ;
171
+ return report ;
172
+ }
158
173
159
- // The YubiKey has a reclaim timeout of 3 seconds. This can cause the SDK some trouble if we just
160
- // switched out of a different USB interface (like Keyboard or CCID). We previously used a fairly
161
- // tight timeout of 4 seconds, but that seemed to not always work. 6 seconds (double the timeout)
162
- // seems like a more reasonable timeout for the operating system.
163
- int runLoopResult = CFRunLoopRunInMode ( _loopId , 6 , true ) ;
174
+ // Otherwise start up the IO runloop and see if we find more reports to pick up.
175
+ IntPtr runLoop = CFRunLoopGetCurrent ( ) ;
164
176
165
- _device . LogDeviceAccessTime ( ) ;
177
+ IOHIDDeviceScheduleWithRunLoop ( _deviceHandle , runLoop , _loopId ) ;
166
178
167
- if ( runLoopResult != kCFRunLoopRunHandledSource )
168
- {
169
- throw new PlatformApiException (
170
- string . Format (
171
- CultureInfo . CurrentCulture ,
172
- ExceptionMessages . WrongIOKitRunLoopMode ,
173
- runLoopResult ) ) ;
174
- }
179
+ // The YubiKey has a reclaim timeout of 3 seconds. This can cause the SDK some trouble if we just
180
+ // switched out of a different USB interface (like Keyboard or CCID). We previously used a fairly
181
+ // tight timeout of 4 seconds, but that seemed to not always work. 6 seconds (double the timeout)
182
+ // seems like a more reasonable timeout for the operating system.
183
+ int runLoopResult = CFRunLoopRunInMode ( _loopId , 6 , true ) ;
175
184
176
- IOHIDDeviceUnscheduleFromRunLoop ( _deviceHandle , runLoop , _loopId ) ;
177
-
178
- _log . SensitiveLogInformation (
179
- "GetReport returned buffer: {Report}" ,
180
- Hex . BytesToHex ( _readBuffer ) ) ;
185
+ _device . LogDeviceAccessTime ( ) ;
181
186
182
- // Return a copy of the report
183
- return _readBuffer . ToArray ( ) ;
184
- }
185
- finally
187
+ if ( runLoopResult != kCFRunLoopRunHandledSource )
186
188
{
187
- IOHIDDeviceRegisterInputReportCallback (
188
- _deviceHandle ,
189
- _readBuffer ,
190
- _readBuffer . Length ,
191
- IntPtr . Zero ,
192
- IntPtr . Zero ) ;
193
-
194
- IOHIDDeviceRegisterRemovalCallback ( _deviceHandle , IntPtr . Zero , IntPtr . Zero ) ;
189
+ throw new PlatformApiException (
190
+ string . Format (
191
+ CultureInfo . CurrentCulture ,
192
+ ExceptionMessages . WrongIOKitRunLoopMode ,
193
+ runLoopResult ) ) ;
195
194
}
195
+
196
+ IOHIDDeviceUnscheduleFromRunLoop ( _deviceHandle , runLoop , _loopId ) ;
197
+
198
+ // We should be guaranteed to have a report here - otherwise the runloop would have timed out
199
+ // and the PlatformApiException above would have been thrown.
200
+ _ = _reportsQueue . TryDequeue ( out report ) ;
201
+
202
+ _log . SensitiveLogInformation (
203
+ "GetReport returned buffer: {Report}" ,
204
+ Hex . BytesToHex ( report ) ) ;
205
+
206
+ return report ;
196
207
}
197
208
198
209
/// <summary>
@@ -232,7 +243,7 @@ private static void ReportCallback(
232
243
233
244
log . LogInformation ( "MacOSHidIOReportConnection.ReportCallback has been called." ) ;
234
245
235
- if ( result != 0 || type != IOKitHidConstants . kIOHidReportTypeOutput || reportId != 0 || reportLength < 0 )
246
+ if ( result != 0 || type != IOKitHidConstants . kIOHidReportTypeInput || reportId != 0 || reportLength < 0 )
236
247
{
237
248
// Something went wrong. We don't currently signal, just continue.
238
249
log . LogWarning (
@@ -242,19 +253,10 @@ private static void ReportCallback(
242
253
type ,
243
254
reportId ,
244
255
reportLength ) ;
245
-
246
- return ;
247
256
}
248
257
249
- byte [ ] buffer = ( byte [ ] ) GCHandle . FromIntPtr ( context ) . Target ;
250
- long length = Math . Min ( buffer . Length , reportLength ) ;
251
- log . LogInformation (
252
- "Buffer length determined to be {Length} bytes. (buffer.Length was {BufferLength}, and reportLength was {ReportLength}" ,
253
- length ,
254
- buffer . Length ,
255
- reportLength ) ;
256
-
257
- Array . Copy ( report , buffer , length ) ;
258
+ var reportsQueue = ( ConcurrentQueue < byte [ ] > ) GCHandle . FromIntPtr ( context ) . Target ;
259
+ reportsQueue . Enqueue ( report ) ;
258
260
}
259
261
260
262
/// <summary>
@@ -324,11 +326,25 @@ private void Dispose(bool disposing)
324
326
// Dispose managed state here
325
327
}
326
328
329
+ IOHIDDeviceRegisterInputReportCallback (
330
+ _deviceHandle ,
331
+ _readBuffer ,
332
+ _readBuffer . Length ,
333
+ IntPtr . Zero ,
334
+ IntPtr . Zero ) ;
335
+
336
+ IOHIDDeviceRegisterRemovalCallback ( _deviceHandle , IntPtr . Zero , IntPtr . Zero ) ;
337
+
327
338
if ( _readHandle . IsAllocated )
328
339
{
329
340
_readHandle . Free ( ) ;
330
341
}
331
342
343
+ if ( _pinnedReportsQueue . IsAllocated )
344
+ {
345
+ _pinnedReportsQueue . Free ( ) ;
346
+ }
347
+
332
348
if ( _deviceHandle != IntPtr . Zero )
333
349
{
334
350
_ = IOHIDDeviceClose ( _deviceHandle , 0 ) ;
0 commit comments