19
19
using System ;
20
20
using System . Collections . Concurrent ;
21
21
using System . Globalization ;
22
- using System . IO ;
23
22
using System . Net . Http ;
24
- using System . Net . WebSockets ;
25
- using System . Text ;
26
23
using System . Threading ;
27
24
using System . Threading . Tasks ;
28
25
using Newtonsoft . Json ;
@@ -50,15 +47,14 @@ public class DevToolsSession : IDevToolsSession
50
47
private bool isDisposed = false ;
51
48
private string attachedTargetId ;
52
49
53
- private ClientWebSocket sessionSocket ;
50
+ private WebSocketConnection connection ;
54
51
private ConcurrentDictionary < long , DevToolsCommandData > pendingCommands = new ConcurrentDictionary < long , DevToolsCommandData > ( ) ;
52
+ private readonly BlockingCollection < string > messageQueue = new BlockingCollection < string > ( ) ;
53
+ private readonly Task messageQueueMonitorTask ;
55
54
private long currentCommandId = 0 ;
56
55
57
56
private DevToolsDomains domains ;
58
57
59
- private CancellationTokenSource receiveCancellationToken ;
60
- private Task receiveTask ;
61
-
62
58
/// <summary>
63
59
/// Initializes a new instance of the DevToolsSession class, using the specified WebSocket endpoint.
64
60
/// </summary>
@@ -76,6 +72,8 @@ public DevToolsSession(string endpointAddress)
76
72
{
77
73
this . websocketAddress = endpointAddress ;
78
74
}
75
+ this . messageQueueMonitorTask = Task . Run ( ( ) => this . MonitorMessageQueue ( ) ) ;
76
+ this . messageQueueMonitorTask . ConfigureAwait ( false ) ;
79
77
}
80
78
81
79
/// <summary>
@@ -213,15 +211,13 @@ public T GetVersionSpecificDomains<T>() where T : DevToolsSessionDomains
213
211
214
212
var message = new DevToolsCommandData ( Interlocked . Increment ( ref this . currentCommandId ) , this . ActiveSessionId , commandName , commandParameters ) ;
215
213
216
- if ( this . sessionSocket != null && this . sessionSocket . State == WebSocketState . Open )
214
+ if ( this . connection != null && this . connection . IsActive )
217
215
{
218
216
LogTrace ( "Sending {0} {1}: {2}" , message . CommandId , message . CommandName , commandParameters . ToString ( ) ) ;
219
217
220
- var contents = JsonConvert . SerializeObject ( message ) ;
221
- var contentBuffer = Encoding . UTF8 . GetBytes ( contents ) ;
222
-
218
+ string contents = JsonConvert . SerializeObject ( message ) ;
223
219
this . pendingCommands . TryAdd ( message . CommandId , message ) ;
224
- await this . sessionSocket . SendAsync ( new ArraySegment < byte > ( contentBuffer ) , WebSocketMessageType . Text , true , cancellationToken ) ;
220
+ await this . connection . SendData ( contents ) ;
225
221
226
222
var responseWasReceived = await Task . Run ( ( ) => message . SyncEvent . Wait ( millisecondsTimeout . Value , cancellationToken ) ) ;
227
223
@@ -230,8 +226,7 @@ public T GetVersionSpecificDomains<T>() where T : DevToolsSessionDomains
230
226
throw new InvalidOperationException ( $ "A command response was not received: { commandName } ") ;
231
227
}
232
228
233
- DevToolsCommandData modified ;
234
- if ( this . pendingCommands . TryRemove ( message . CommandId , out modified ) )
229
+ if ( this . pendingCommands . TryRemove ( message . CommandId , out DevToolsCommandData modified ) )
235
230
{
236
231
if ( modified . IsError )
237
232
{
@@ -256,10 +251,7 @@ public T GetVersionSpecificDomains<T>() where T : DevToolsSessionDomains
256
251
}
257
252
else
258
253
{
259
- if ( this . sessionSocket != null )
260
- {
261
- LogTrace ( "WebSocket is not connected (current state is {0}); not sending {1}" , this . sessionSocket . State , message . CommandName ) ;
262
- }
254
+ LogTrace ( "WebSocket is not connected; not sending {0}" , message . CommandName ) ;
263
255
}
264
256
265
257
return null ;
@@ -330,11 +322,7 @@ protected void Dispose(bool disposing)
330
322
{
331
323
this . Domains . Target . TargetDetached -= this . OnTargetDetached ;
332
324
this . pendingCommands . Clear ( ) ;
333
- this . TerminateSocketConnection ( ) ;
334
-
335
- // Note: Canceling the receive task will dispose of
336
- // the underlying ClientWebSocket instance.
337
- this . CancelReceiveTask ( ) ;
325
+ this . TerminateSocketConnection ( ) . GetAwaiter ( ) . GetResult ( ) ;
338
326
}
339
327
340
328
this . isDisposed = true ;
@@ -377,28 +365,6 @@ private async Task<int> InitializeProtocol(int requestedProtocolVersion)
377
365
return protocolVersion ;
378
366
}
379
367
380
- private async Task InitializeSocketConnection ( )
381
- {
382
- LogTrace ( "Creating WebSocket" ) ;
383
- this . sessionSocket = new ClientWebSocket ( ) ;
384
- this . sessionSocket . Options . KeepAliveInterval = TimeSpan . Zero ;
385
-
386
- try
387
- {
388
- var timeoutTokenSource = new CancellationTokenSource ( this . openConnectionWaitTimeSpan ) ;
389
- await this . sessionSocket . ConnectAsync ( new Uri ( this . websocketAddress ) , timeoutTokenSource . Token ) ;
390
- while ( this . sessionSocket . State != WebSocketState . Open && ! timeoutTokenSource . Token . IsCancellationRequested ) ;
391
- }
392
- catch ( OperationCanceledException e )
393
- {
394
- throw new WebDriverException ( string . Format ( CultureInfo . InvariantCulture , "Could not establish WebSocket connection within {0} seconds." , this . openConnectionWaitTimeSpan . TotalSeconds ) , e ) ;
395
- }
396
-
397
- LogTrace ( "WebSocket created; starting message listener" ) ;
398
- this . receiveCancellationToken = new CancellationTokenSource ( ) ;
399
- this . receiveTask = Task . Run ( ( ) => ReceiveMessage ( ) . ConfigureAwait ( false ) ) ;
400
- }
401
-
402
368
private async Task InitializeSession ( )
403
369
{
404
370
LogTrace ( "Creating session" ) ;
@@ -445,116 +411,56 @@ private void OnTargetDetached(object sender, TargetDetachedEventArgs e)
445
411
}
446
412
}
447
413
448
- private void TerminateSocketConnection ( )
414
+ private async Task InitializeSocketConnection ( )
449
415
{
450
- if ( this . sessionSocket != null && this . sessionSocket . State == WebSocketState . Open )
451
- {
452
- var closeConnectionTokenSource = new CancellationTokenSource ( this . closeConnectionWaitTimeSpan ) ;
453
- try
454
- {
455
- // Since Chromium-based DevTools does not respond to the close
456
- // request with a correctly echoed WebSocket close packet, but
457
- // rather just terminates the socket connection, so we have to
458
- // catch the exception thrown when the socket is terminated
459
- // unexpectedly. Also, because we are using async, waiting for
460
- // the task to complete might throw a TaskCanceledException,
461
- // which we should also catch. Additiionally, there are times
462
- // when mulitple failure modes can be seen, which will throw an
463
- // AggregateException, consolidating several exceptions into one,
464
- // and this too must be caught. Finally, the call to CloseAsync
465
- // will hang even though the connection is already severed.
466
- // Wait for the task to complete for a short time (since we're
467
- // restricted to localhost, the default of 2 seconds should be
468
- // plenty; if not, change the initialization of the timout),
469
- // and if the task is still running, then we assume the connection
470
- // is properly closed.
471
- LogTrace ( "Sending socket close request" ) ;
472
- Task closeTask = Task . Run ( async ( ) => await this . sessionSocket . CloseOutputAsync ( WebSocketCloseStatus . NormalClosure , string . Empty , closeConnectionTokenSource . Token ) ) ;
473
- closeTask . Wait ( ) ;
474
- }
475
- catch ( WebSocketException )
476
- {
477
- }
478
- catch ( TaskCanceledException )
479
- {
480
- }
481
- catch ( AggregateException )
482
- {
483
- }
484
- }
416
+ LogTrace ( "Creating WebSocket" ) ;
417
+ this . connection = new WebSocketConnection ( this . openConnectionWaitTimeSpan , this . closeConnectionWaitTimeSpan ) ;
418
+ connection . DataReceived += OnConnectionDataReceived ;
419
+ await connection . Start ( this . websocketAddress ) ;
420
+ LogTrace ( "WebSocket created" ) ;
485
421
}
486
422
487
- private void CancelReceiveTask ( )
423
+ private async Task TerminateSocketConnection ( )
488
424
{
489
- if ( this . receiveTask != null )
425
+ LogTrace ( "Closing WebSocket" ) ;
426
+ if ( this . connection != null && this . connection . IsActive )
490
427
{
491
- // Wait for the recieve task to be completely exited (for
492
- // whatever reason) before attempting to dispose it. Also
493
- // note that canceling the receive task will dispose of the
494
- // underlying WebSocket.
495
- this . receiveCancellationToken . Cancel ( ) ;
496
- this . receiveTask . Wait ( ) ;
497
- this . receiveTask . Dispose ( ) ;
498
- this . receiveTask = null ;
428
+ await this . connection . Stop ( ) ;
429
+ await this . ShutdownMessageQueue ( ) ;
499
430
}
431
+ LogTrace ( "WebSocket closed" ) ;
500
432
}
501
433
502
- private async Task ReceiveMessage ( )
434
+ private async Task ShutdownMessageQueue ( )
503
435
{
504
- var cancellationToken = this . receiveCancellationToken . Token ;
505
- try
506
- {
507
- var buffer = WebSocket . CreateClientBuffer ( 1024 , 1024 ) ;
508
- while ( this . sessionSocket . State != WebSocketState . Closed && ! cancellationToken . IsCancellationRequested )
509
- {
510
- WebSocketReceiveResult result = await this . sessionSocket . ReceiveAsync ( buffer , cancellationToken ) ;
511
- if ( ! cancellationToken . IsCancellationRequested )
512
- {
513
- if ( result . MessageType == WebSocketMessageType . Close && this . sessionSocket . State == WebSocketState . CloseReceived )
514
- {
515
- LogTrace ( "Got WebSocket close message from browser" ) ;
516
- await this . sessionSocket . CloseOutputAsync ( WebSocketCloseStatus . NormalClosure , string . Empty , cancellationToken ) ;
517
- }
518
- }
519
-
520
- if ( this . sessionSocket . State == WebSocketState . Open && result . MessageType != WebSocketMessageType . Close )
521
- {
522
- using ( var stream = new MemoryStream ( ) )
523
- {
524
- stream . Write ( buffer . Array , 0 , result . Count ) ;
525
- while ( ! result . EndOfMessage )
526
- {
527
- result = await this . sessionSocket . ReceiveAsync ( buffer , cancellationToken ) ;
528
- stream . Write ( buffer . Array , 0 , result . Count ) ;
529
- }
530
-
531
- stream . Seek ( 0 , SeekOrigin . Begin ) ;
532
- using ( var reader = new StreamReader ( stream , Encoding . UTF8 ) )
533
- {
534
- string message = reader . ReadToEnd ( ) ;
535
-
536
- // fire and forget
537
- // TODO: we need implement some kind of queue
538
- Task . Run ( ( ) => ProcessIncomingMessage ( message ) ) ;
539
- }
540
- }
541
- }
542
- }
543
- }
544
- catch ( OperationCanceledException )
436
+ // THe WebSockect connection is always closed before this method
437
+ // is called, so there will eventually be no more data written
438
+ // into the message queue, meaning this loop should be guaranteed
439
+ // to complete.
440
+ while ( this . connection . IsActive )
545
441
{
442
+ await Task . Delay ( TimeSpan . FromMilliseconds ( 10 ) ) ;
546
443
}
547
- catch ( WebSocketException )
548
- {
549
- }
550
- finally
444
+
445
+ this . messageQueue . CompleteAdding ( ) ;
446
+ await this . messageQueueMonitorTask ;
447
+ }
448
+
449
+ private void MonitorMessageQueue ( )
450
+ {
451
+ // GetConsumingEnumerable blocks until if BlockingCollection.IsCompleted
452
+ // is false (i.e., is still able to be written to), and there are no items
453
+ // in the collection. Once any items are added to the collection, the method
454
+ // unblocks and we can process any items in the collection at that moment.
455
+ // Once IsCompleted is true, the method unblocks with no items in returned
456
+ // in the IEnumerable, meaning the foreach loop will terminate gracefully.
457
+ foreach ( string message in this . messageQueue . GetConsumingEnumerable ( ) )
551
458
{
552
- this . sessionSocket . Dispose ( ) ;
553
- this . sessionSocket = null ;
459
+ this . ProcessMessage ( message ) ;
554
460
}
555
461
}
556
462
557
- private void ProcessIncomingMessage ( string message )
463
+ private void ProcessMessage ( string message )
558
464
{
559
465
var messageObject = JObject . Parse ( message ) ;
560
466
@@ -594,7 +500,12 @@ private void ProcessIncomingMessage(string message)
594
500
595
501
LogTrace ( "Recieved Event {0}: {1}" , method , eventData . ToString ( ) ) ;
596
502
597
- OnDevToolsEventReceived ( new DevToolsEventReceivedEventArgs ( methodParts [ 0 ] , methodParts [ 1 ] , eventData ) ) ;
503
+ // Dispatch the event on a new thread so that any event handlers
504
+ // responding to the event will not block this thread from processing
505
+ // DevTools commands that may be sent in the body of the attached
506
+ // event handler. If thread pool starvation seems to become a problem,
507
+ // we can switch to a channel-based queue.
508
+ Task . Run ( ( ) => OnDevToolsEventReceived ( new DevToolsEventReceivedEventArgs ( methodParts [ 0 ] , methodParts [ 1 ] , eventData ) ) ) ;
598
509
599
510
return ;
600
511
}
@@ -610,6 +521,11 @@ private void OnDevToolsEventReceived(DevToolsEventReceivedEventArgs e)
610
521
}
611
522
}
612
523
524
+ private void OnConnectionDataReceived ( object sender , WebSocketConnectionDataReceivedEventArgs e )
525
+ {
526
+ this . messageQueue . Add ( e . Data ) ;
527
+ }
528
+
613
529
private void LogTrace ( string message , params object [ ] args )
614
530
{
615
531
if ( LogMessage != null )
0 commit comments