2016-04-29 12:11:27 -07:00
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
2015-12-09 09:57:45 -08:00
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.IO ;
using System.Net ;
using System.Net.Sockets ;
using System.Threading ;
using System.Threading.Tasks ;
using Newtonsoft.Json ;
using Xunit ;
namespace Microsoft.DotNet.ProjectModel.Server.Tests
{
public class DthTestClient : IDisposable
{
private readonly string _hostId ;
private readonly BinaryReader _reader ;
private readonly BinaryWriter _writer ;
private readonly NetworkStream _networkStream ;
private readonly BlockingCollection < DthMessage > _messageQueue ;
private readonly CancellationTokenSource _readCancellationToken ;
// Keeps track of initialized project contexts
// REVIEW: This needs to be exposed if we ever create 2 clients in order to simulate how build
// works in visual studio
private readonly Dictionary < string , int > _projectContexts = new Dictionary < string , int > ( ) ;
private int _nextContextId ;
2016-01-29 01:49:56 -08:00
private readonly Socket _socket ;
2015-12-09 09:57:45 -08:00
2016-04-28 10:01:30 -07:00
public DthTestClient ( DthTestServer server )
2015-12-09 09:57:45 -08:00
{
2016-02-17 16:49:34 -08:00
// Avoid Socket exception 10006 on Linux
Thread . Sleep ( 100 ) ;
2016-01-29 01:49:56 -08:00
_socket = new Socket ( AddressFamily . InterNetwork ,
SocketType . Stream ,
ProtocolType . Tcp ) ;
2015-12-09 09:57:45 -08:00
2016-01-29 01:49:56 -08:00
_socket . Connect ( new IPEndPoint ( IPAddress . Loopback , server . Port ) ) ;
2015-12-09 09:57:45 -08:00
_hostId = server . HostId ;
2016-01-29 01:49:56 -08:00
_networkStream = new NetworkStream ( _socket ) ;
2015-12-09 09:57:45 -08:00
_reader = new BinaryReader ( _networkStream ) ;
_writer = new BinaryWriter ( _networkStream ) ;
_messageQueue = new BlockingCollection < DthMessage > ( ) ;
_readCancellationToken = new CancellationTokenSource ( ) ;
Task . Run ( ( ) = > ReadMessage ( _readCancellationToken . Token ) , _readCancellationToken . Token ) ;
}
2016-05-02 11:32:24 -07:00
public void SendPayload ( Project project , string messageType )
2015-12-09 09:57:45 -08:00
{
2016-05-02 11:32:24 -07:00
SendPayload ( project . ProjectDirectory , messageType ) ;
2015-12-09 09:57:45 -08:00
}
2016-05-02 11:32:24 -07:00
public void SendPayload ( string projectPath , string messageType )
2015-12-09 09:57:45 -08:00
{
int contextId ;
if ( ! _projectContexts . TryGetValue ( projectPath , out contextId ) )
{
Assert . True ( false , $"Unable to resolve context for {projectPath}" ) ;
}
2016-05-02 11:32:24 -07:00
SendPayload ( contextId , messageType ) ;
2015-12-09 09:57:45 -08:00
}
2016-05-02 11:32:24 -07:00
public void SendPayload ( int contextId , string messageType )
2015-12-09 09:57:45 -08:00
{
2016-05-02 11:32:24 -07:00
SendPayload ( contextId , messageType , new { } ) ;
2015-12-09 09:57:45 -08:00
}
2016-05-02 11:32:24 -07:00
public void SendPayload ( int contextId , string messageType , object payload )
2015-12-09 09:57:45 -08:00
{
lock ( _writer )
{
var message = new
{
ContextId = contextId ,
HostId = _hostId ,
MessageType = messageType ,
Payload = payload
} ;
_writer . Write ( JsonConvert . SerializeObject ( message ) ) ;
}
}
public int Initialize ( string projectPath )
{
var contextId = _nextContextId + + ;
_projectContexts [ projectPath ] = contextId ;
2016-05-02 11:32:24 -07:00
SendPayload ( contextId , MessageTypes . Initialize , new { ProjectFolder = projectPath } ) ;
2015-12-09 09:57:45 -08:00
return contextId ;
}
public int Initialize ( string projectPath , int protocolVersion )
{
var contextId = _nextContextId + + ;
_projectContexts [ projectPath ] = contextId ;
2016-05-02 11:32:24 -07:00
SendPayload ( contextId , MessageTypes . Initialize , new { ProjectFolder = projectPath , Version = protocolVersion } ) ;
2015-12-09 09:57:45 -08:00
return contextId ;
}
public int Initialize ( string projectPath , int protocolVersion , string configuration )
{
var contextId = _nextContextId + + ;
_projectContexts [ projectPath ] = contextId ;
2016-05-02 11:32:24 -07:00
SendPayload ( contextId , MessageTypes . Initialize , new { ProjectFolder = projectPath , Version = protocolVersion , Configuration = configuration } ) ;
2015-12-09 09:57:45 -08:00
return contextId ;
}
public void SetProtocolVersion ( int version )
{
2016-05-02 11:32:24 -07:00
SendPayload ( 0 , MessageTypes . ProtocolVersion , new { Version = version } ) ;
2015-12-09 09:57:45 -08:00
}
public List < DthMessage > DrainMessage ( int count )
{
var result = new List < DthMessage > ( ) ;
while ( count > 0 )
{
result . Add ( GetResponse ( timeout : TimeSpan . FromSeconds ( 10 ) ) ) ;
count - - ;
}
return result ;
}
public List < DthMessage > DrainAllMessages ( )
{
return DrainAllMessages ( TimeSpan . FromSeconds ( 10 ) ) ;
}
/// <summary>
/// Read all messages from pipeline till timeout
/// </summary>
/// <param name="timeout">The timeout</param>
/// <returns>All the messages in a list</returns>
public List < DthMessage > DrainAllMessages ( TimeSpan timeout )
{
var result = new List < DthMessage > ( ) ;
while ( true )
{
try
{
result . Add ( GetResponse ( timeout ) ) ;
}
catch ( TimeoutException )
{
return result ;
}
catch ( Exception )
{
throw ;
}
}
}
/// <summary>
/// Read messages from pipeline until the first match
/// </summary>]
/// <param name="type">A message type</param>
/// <returns>The first match message</returns>
public DthMessage DrainTillFirst ( string type )
{
return DrainTillFirst ( type , TimeSpan . FromSeconds ( 10 ) ) ;
}
/// <summary>
/// Read messages from pipeline until the first match
/// </summary>
/// <param name="type">A message type</param>
/// <param name="timeout">Timeout for each read</param>
/// <returns>The first match message</returns>
public DthMessage DrainTillFirst ( string type , TimeSpan timeout )
{
while ( true )
{
var next = GetResponse ( timeout ) ;
if ( next . MessageType = = type )
{
return next ;
}
}
}
/// <summary>
/// Read messages from pipeline until the first match
/// </summary>
/// <param name="type">A message type</param>
/// <param name="timeout">Timeout</param>
/// <param name="leadingMessages">All the messages read before the first match</param>
/// <returns>The first match</returns>
public DthMessage DrainTillFirst ( string type , TimeSpan timeout , out List < DthMessage > leadingMessages )
{
leadingMessages = new List < DthMessage > ( ) ;
while ( true )
{
var next = GetResponse ( timeout ) ;
if ( next . MessageType = = type )
{
return next ;
}
else
{
leadingMessages . Add ( next ) ;
}
}
}
public void Dispose ( )
{
_reader . Dispose ( ) ;
_writer . Dispose ( ) ;
_networkStream . Dispose ( ) ;
_readCancellationToken . Cancel ( ) ;
2016-04-22 15:01:56 -07:00
2016-03-21 14:55:02 -07:00
try
{
_socket . Shutdown ( SocketShutdown . Both ) ;
}
2016-03-21 15:18:28 -07:00
catch ( SocketException ex )
2016-03-21 14:55:02 -07:00
{
// Swallow this error for now.
// This is a temporary fix for a random failure on CI. The issue happens on Windowx x86
// only.
2016-04-28 10:01:30 -07:00
Console . Error . WriteLine ( $"Exception thrown durning socket shutting down: {ex.SocketErrorCode}." ) ;
2016-03-21 14:55:02 -07:00
}
2015-12-09 09:57:45 -08:00
}
private void ReadMessage ( CancellationToken cancellationToken )
{
while ( true )
{
try
{
if ( cancellationToken . IsCancellationRequested )
{
return ;
}
var content = _reader . ReadString ( ) ;
var message = JsonConvert . DeserializeObject < DthMessage > ( content ) ;
_messageQueue . Add ( message ) ;
}
catch ( IOException )
{
// swallow
}
catch ( JsonSerializationException deserializException )
{
throw new InvalidOperationException (
$"Fail to deserailze data into {nameof(DthMessage)}." ,
deserializException ) ;
}
catch ( Exception ex )
{
throw ex ;
}
}
}
private DthMessage GetResponse ( TimeSpan timeout )
{
DthMessage message ;
if ( _messageQueue . TryTake ( out message , timeout ) )
{
return message ;
}
else
{
throw new TimeoutException ( $"Response time out after {timeout.TotalSeconds} seconds." ) ;
}
}
}
}