e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
1560 lines
66 KiB
C#
1560 lines
66 KiB
C#
// <copyright>
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
|
|
namespace System.ServiceModel.Channels
|
|
{
|
|
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Net.Http;
|
|
using System.Net.WebSockets;
|
|
using System.Runtime;
|
|
using System.Runtime.Diagnostics;
|
|
using System.Security.Principal;
|
|
using System.ServiceModel.Diagnostics;
|
|
using System.ServiceModel.Diagnostics.Application;
|
|
using System.ServiceModel.Security;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
abstract class WebSocketTransportDuplexSessionChannel : TransportDuplexSessionChannel
|
|
{
|
|
static AsyncCallback streamedWriteCallback = Fx.ThunkCallback(StreamWriteCallback);
|
|
WebSocket webSocket = null;
|
|
WebSocketTransportSettings webSocketSettings;
|
|
TransferMode transferMode;
|
|
int maxBufferSize;
|
|
WaitCallback waitCallback;
|
|
object state;
|
|
WebSocketStream webSocketStream;
|
|
byte[] internalBuffer;
|
|
ConnectionBufferPool bufferPool;
|
|
int cleanupStatus = WebSocketHelper.OperationNotStarted;
|
|
ITransportFactorySettings transportFactorySettings;
|
|
WebSocketCloseDetails webSocketCloseDetails = new WebSocketCloseDetails();
|
|
bool shouldDisposeWebSocketAfterClosed = true;
|
|
Exception pendingWritingMessageException;
|
|
|
|
public WebSocketTransportDuplexSessionChannel(HttpChannelListener channelListener, EndpointAddress localAddress, Uri localVia, ConnectionBufferPool bufferPool)
|
|
: base(channelListener, channelListener, localAddress, localVia, EndpointAddress.AnonymousAddress, channelListener.MessageVersion.Addressing.AnonymousUri)
|
|
{
|
|
Fx.Assert(channelListener.WebSocketSettings != null, "channelListener.WebSocketTransportSettings should not be null.");
|
|
this.webSocketSettings = channelListener.WebSocketSettings;
|
|
this.transferMode = channelListener.TransferMode;
|
|
this.maxBufferSize = channelListener.MaxBufferSize;
|
|
this.bufferPool = bufferPool;
|
|
this.transportFactorySettings = channelListener;
|
|
}
|
|
|
|
public WebSocketTransportDuplexSessionChannel(HttpChannelFactory<IDuplexSessionChannel> channelFactory, EndpointAddress remoteAddresss, Uri via, ConnectionBufferPool bufferPool)
|
|
: base(channelFactory, channelFactory, EndpointAddress.AnonymousAddress, channelFactory.MessageVersion.Addressing.AnonymousUri, remoteAddresss, via)
|
|
{
|
|
Fx.Assert(channelFactory.WebSocketSettings != null, "channelFactory.WebSocketTransportSettings should not be null.");
|
|
this.webSocketSettings = channelFactory.WebSocketSettings;
|
|
this.transferMode = channelFactory.TransferMode;
|
|
this.maxBufferSize = channelFactory.MaxBufferSize;
|
|
this.bufferPool = bufferPool;
|
|
this.transportFactorySettings = channelFactory;
|
|
}
|
|
|
|
protected WebSocket WebSocket
|
|
{
|
|
get
|
|
{
|
|
return this.webSocket;
|
|
}
|
|
|
|
set
|
|
{
|
|
Fx.Assert(value != null, "value should not be null.");
|
|
Fx.Assert(this.webSocket == null, "webSocket should not be set before this set call.");
|
|
this.webSocket = value;
|
|
}
|
|
}
|
|
|
|
protected WebSocketTransportSettings WebSocketSettings
|
|
{
|
|
get { return this.webSocketSettings; }
|
|
}
|
|
|
|
protected TransferMode TransferMode
|
|
{
|
|
get { return this.transferMode; }
|
|
}
|
|
|
|
protected int MaxBufferSize
|
|
{
|
|
get
|
|
{
|
|
return this.maxBufferSize;
|
|
}
|
|
}
|
|
|
|
protected ITransportFactorySettings TransportFactorySettings
|
|
{
|
|
get
|
|
{
|
|
return this.transportFactorySettings;
|
|
}
|
|
}
|
|
|
|
protected byte[] InternalBuffer
|
|
{
|
|
get
|
|
{
|
|
return this.internalBuffer;
|
|
}
|
|
|
|
set
|
|
{
|
|
// We allow setting the property to null as long as we don't overwrite an existing non-null 'internalBuffer'. Because otherwise
|
|
// we get NullRefs in other places. So if you change/remove the assert below, make sure we still assert for this case.
|
|
Fx.Assert(this.internalBuffer == null, "internalBuffer should not be set twice.");
|
|
this.internalBuffer = value;
|
|
}
|
|
}
|
|
|
|
protected bool ShouldDisposeWebSocketAfterClosed
|
|
{
|
|
set
|
|
{
|
|
this.shouldDisposeWebSocketAfterClosed = value;
|
|
}
|
|
}
|
|
|
|
protected override void OnAbort()
|
|
{
|
|
if (TD.WebSocketConnectionAbortedIsEnabled())
|
|
{
|
|
TD.WebSocketConnectionAborted(
|
|
this.EventTraceActivity,
|
|
this.WebSocket != null ? this.WebSocket.GetHashCode() : -1);
|
|
}
|
|
|
|
this.Cleanup();
|
|
}
|
|
|
|
public override T GetProperty<T>()
|
|
{
|
|
if (typeof(T) == typeof(IWebSocketCloseDetails))
|
|
{
|
|
return this.webSocketCloseDetails as T;
|
|
}
|
|
|
|
return base.GetProperty<T>();
|
|
}
|
|
|
|
protected override void CompleteClose(TimeSpan timeout)
|
|
{
|
|
if (TD.WebSocketCloseSentIsEnabled())
|
|
{
|
|
TD.WebSocketCloseSent(
|
|
this.WebSocket.GetHashCode(),
|
|
this.webSocketCloseDetails.OutputCloseStatus.ToString(),
|
|
this.RemoteAddress != null ? this.RemoteAddress.ToString() : string.Empty);
|
|
}
|
|
|
|
Task closeTask = this.CloseAsync();
|
|
closeTask.Wait(timeout, WebSocketHelper.ThrowCorrectException, WebSocketHelper.CloseOperation);
|
|
|
|
if (TD.WebSocketConnectionClosedIsEnabled())
|
|
{
|
|
TD.WebSocketConnectionClosed(this.WebSocket.GetHashCode());
|
|
}
|
|
}
|
|
|
|
protected byte[] TakeBuffer()
|
|
{
|
|
Fx.Assert(this.bufferPool != null, "'bufferPool' MUST NOT be NULL.");
|
|
return this.bufferPool.Take();
|
|
}
|
|
|
|
protected override void CloseOutputSessionCore(TimeSpan timeout)
|
|
{
|
|
if (TD.WebSocketCloseOutputSentIsEnabled())
|
|
{
|
|
TD.WebSocketCloseOutputSent(
|
|
this.WebSocket.GetHashCode(),
|
|
this.webSocketCloseDetails.OutputCloseStatus.ToString(),
|
|
this.RemoteAddress != null ? this.RemoteAddress.ToString() : string.Empty);
|
|
}
|
|
|
|
Task task = this.CloseOutputAsync(CancellationToken.None);
|
|
task.Wait(timeout, WebSocketHelper.ThrowCorrectException, WebSocketHelper.CloseOperation);
|
|
}
|
|
|
|
protected override void OnClose(TimeSpan timeout)
|
|
{
|
|
try
|
|
{
|
|
base.OnClose(timeout);
|
|
}
|
|
finally
|
|
{
|
|
this.Cleanup();
|
|
}
|
|
}
|
|
|
|
protected override void ReturnConnectionIfNecessary(bool abort, TimeSpan timeout)
|
|
{
|
|
}
|
|
|
|
protected override AsyncCompletionResult StartWritingBufferedMessage(Message message, ArraySegment<byte> messageData, bool allowOutputBatching, TimeSpan timeout, Threading.WaitCallback callback, object state)
|
|
{
|
|
Fx.Assert(callback != null, "callback should not be null.");
|
|
|
|
TimeoutHelper helper = new TimeoutHelper(timeout);
|
|
WebSocketMessageType outgoingMessageType = GetWebSocketMessageType(message);
|
|
IOThreadCancellationTokenSource cancellationTokenSource = new IOThreadCancellationTokenSource(helper.RemainingTime());
|
|
|
|
if (TD.WebSocketAsyncWriteStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStart(
|
|
this.WebSocket.GetHashCode(),
|
|
messageData.Count,
|
|
this.RemoteAddress != null ? this.RemoteAddress.ToString() : string.Empty);
|
|
}
|
|
|
|
Task task = this.WebSocket.SendAsync(messageData, outgoingMessageType, true, cancellationTokenSource.Token);
|
|
Fx.Assert(this.pendingWritingMessageException == null, "'pendingWritingMessageException' MUST be NULL at this point.");
|
|
|
|
task.ContinueWith(t =>
|
|
{
|
|
try
|
|
{
|
|
if (TD.WebSocketAsyncWriteStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStop(this.webSocket.GetHashCode());
|
|
}
|
|
|
|
cancellationTokenSource.Dispose();
|
|
WebSocketHelper.ThrowExceptionOnTaskFailure(t, timeout, WebSocketHelper.SendOperation);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
// Intentionally not following the usual pattern to rethrow fatal exceptions.
|
|
// Any rethrown exception would just be ----ed, because nobody awaits the
|
|
// Task returned from ContinueWith in this case.
|
|
|
|
FxTrace.Exception.TraceHandledException(error, TraceEventType.Information);
|
|
this.pendingWritingMessageException = error;
|
|
}
|
|
finally
|
|
{
|
|
callback.Invoke(state);
|
|
}
|
|
}, CancellationToken.None);
|
|
|
|
return AsyncCompletionResult.Queued;
|
|
}
|
|
|
|
protected override void FinishWritingMessage()
|
|
{
|
|
ThrowOnPendingException(ref this.pendingWritingMessageException);
|
|
base.FinishWritingMessage();
|
|
}
|
|
|
|
protected override AsyncCompletionResult StartWritingStreamedMessage(Message message, TimeSpan timeout, WaitCallback callback, object state)
|
|
{
|
|
TimeoutHelper helper = new TimeoutHelper(timeout);
|
|
WebSocketMessageType outgoingMessageType = GetWebSocketMessageType(message);
|
|
WebSocketStream webSocketStream = new WebSocketStream(this.WebSocket, outgoingMessageType, helper.RemainingTime());
|
|
|
|
this.waitCallback = callback;
|
|
this.state = state;
|
|
this.webSocketStream = webSocketStream;
|
|
IAsyncResult result = this.MessageEncoder.BeginWriteMessage(message, new TimeoutStream(webSocketStream, ref helper), streamedWriteCallback, this);
|
|
|
|
if (!result.CompletedSynchronously)
|
|
{
|
|
return AsyncCompletionResult.Queued;
|
|
}
|
|
|
|
this.MessageEncoder.EndWriteMessage(result);
|
|
|
|
webSocketStream.WriteEndOfMessageAsync(helper.RemainingTime(), callback, state);
|
|
return AsyncCompletionResult.Queued;
|
|
}
|
|
|
|
protected override AsyncCompletionResult BeginCloseOutput(TimeSpan timeout, Threading.WaitCallback callback, object state)
|
|
{
|
|
Fx.Assert(callback != null, "callback should not be null.");
|
|
|
|
IOThreadCancellationTokenSource cancellationTokenSource = new IOThreadCancellationTokenSource(timeout);
|
|
Task task = this.CloseOutputAsync(cancellationTokenSource.Token);
|
|
Fx.Assert(this.pendingWritingMessageException == null, "'pendingWritingMessageException' MUST be NULL at this point.");
|
|
|
|
task.ContinueWith(t =>
|
|
{
|
|
try
|
|
{
|
|
cancellationTokenSource.Dispose();
|
|
WebSocketHelper.ThrowExceptionOnTaskFailure(t, timeout, WebSocketHelper.CloseOperation);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
// Intentionally not following the usual pattern to rethrow fatal exceptions.
|
|
// Any rethrown exception would just be ----ed, because nobody awaits the
|
|
// Task returned from ContinueWith in this case.
|
|
|
|
FxTrace.Exception.TraceHandledException(error, TraceEventType.Information);
|
|
this.pendingWritingMessageException = error;
|
|
}
|
|
finally
|
|
{
|
|
callback.Invoke(state);
|
|
}
|
|
});
|
|
|
|
return AsyncCompletionResult.Queued;
|
|
}
|
|
|
|
protected override void OnSendCore(Message message, TimeSpan timeout)
|
|
{
|
|
Fx.Assert(message != null, "message should not be null.");
|
|
|
|
TimeoutHelper helper = new TimeoutHelper(timeout);
|
|
WebSocketMessageType outgoingMessageType = GetWebSocketMessageType(message);
|
|
|
|
if (this.IsStreamedOutput)
|
|
{
|
|
WebSocketStream webSocketStream = new WebSocketStream(this.WebSocket, outgoingMessageType, helper.RemainingTime());
|
|
TimeoutStream timeoutStream = new TimeoutStream(webSocketStream, ref helper);
|
|
this.MessageEncoder.WriteMessage(message, timeoutStream);
|
|
webSocketStream.WriteEndOfMessage(helper.RemainingTime());
|
|
}
|
|
else
|
|
{
|
|
ArraySegment<byte> messageData = this.EncodeMessage(message);
|
|
bool success = false;
|
|
try
|
|
{
|
|
if (TD.WebSocketAsyncWriteStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStart(
|
|
this.WebSocket.GetHashCode(),
|
|
messageData.Count,
|
|
this.RemoteAddress != null ? this.RemoteAddress.ToString() : string.Empty);
|
|
}
|
|
|
|
Task task = this.WebSocket.SendAsync(messageData, outgoingMessageType, true, CancellationToken.None);
|
|
task.Wait(helper.RemainingTime(), WebSocketHelper.ThrowCorrectException, WebSocketHelper.SendOperation);
|
|
|
|
if (TD.WebSocketAsyncWriteStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStop(this.webSocket.GetHashCode());
|
|
}
|
|
|
|
success = true;
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
this.BufferManager.ReturnBuffer(messageData.Array);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (Fx.IsFatal(ex) || success)
|
|
{
|
|
throw;
|
|
}
|
|
|
|
FxTrace.Exception.TraceUnhandledException(ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override ArraySegment<byte> EncodeMessage(Message message)
|
|
{
|
|
return MessageEncoder.WriteMessage(message, int.MaxValue, this.BufferManager, 0);
|
|
}
|
|
|
|
protected void Cleanup()
|
|
{
|
|
if (Interlocked.CompareExchange(ref this.cleanupStatus, WebSocketHelper.OperationFinished, WebSocketHelper.OperationNotStarted) == WebSocketHelper.OperationNotStarted)
|
|
{
|
|
this.OnCleanup();
|
|
}
|
|
}
|
|
|
|
protected virtual void OnCleanup()
|
|
{
|
|
Fx.Assert(this.cleanupStatus == WebSocketHelper.OperationFinished,
|
|
"This method should only be called by this.Cleanup(). Make sure that you never call overriden OnCleanup()-methods directly in subclasses");
|
|
if (this.shouldDisposeWebSocketAfterClosed && this.webSocket != null)
|
|
{
|
|
this.webSocket.Dispose();
|
|
}
|
|
|
|
if (this.internalBuffer != null)
|
|
{
|
|
this.bufferPool.Return(this.internalBuffer);
|
|
this.internalBuffer = null;
|
|
}
|
|
}
|
|
|
|
private static void ThrowOnPendingException(ref Exception pendingException)
|
|
{
|
|
Exception exceptionToThrow = pendingException;
|
|
|
|
if (exceptionToThrow != null)
|
|
{
|
|
pendingException = null;
|
|
throw FxTrace.Exception.AsError(exceptionToThrow);
|
|
}
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage(FxCop.Category.ReliabilityBasic, FxCop.Rule.WrapExceptionsRule, Justification = "The exceptions thrown here are already wrapped.")]
|
|
private Task CloseAsync()
|
|
{
|
|
try
|
|
{
|
|
return this.WebSocket.CloseAsync(this.webSocketCloseDetails.OutputCloseStatus, this.webSocketCloseDetails.OutputCloseStatusDescription, CancellationToken.None);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (Fx.IsFatal(e))
|
|
{
|
|
throw;
|
|
}
|
|
|
|
throw WebSocketHelper.ConvertAndTraceException(e);
|
|
}
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage(FxCop.Category.ReliabilityBasic, FxCop.Rule.WrapExceptionsRule, Justification = "The exceptions thrown here are already wrapped.")]
|
|
private Task CloseOutputAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
return this.WebSocket.CloseOutputAsync(this.webSocketCloseDetails.OutputCloseStatus, this.webSocketCloseDetails.OutputCloseStatusDescription, cancellationToken);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (Fx.IsFatal(e))
|
|
{
|
|
throw;
|
|
}
|
|
|
|
throw WebSocketHelper.ConvertAndTraceException(e);
|
|
}
|
|
}
|
|
|
|
static WebSocketMessageType GetWebSocketMessageType(Message message)
|
|
{
|
|
WebSocketMessageType outgoingMessageType = WebSocketDefaults.DefaultWebSocketMessageType;
|
|
WebSocketMessageProperty webSocketMessageProperty;
|
|
if (message.Properties.TryGetValue<WebSocketMessageProperty>(WebSocketMessageProperty.Name, out webSocketMessageProperty))
|
|
{
|
|
outgoingMessageType = webSocketMessageProperty.MessageType;
|
|
}
|
|
return outgoingMessageType;
|
|
}
|
|
|
|
static void StreamWriteCallback(IAsyncResult ar)
|
|
{
|
|
if (ar.CompletedSynchronously)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WebSocketTransportDuplexSessionChannel thisPtr = (WebSocketTransportDuplexSessionChannel)ar.AsyncState;
|
|
|
|
try
|
|
{
|
|
thisPtr.MessageEncoder.EndWriteMessage(ar);
|
|
|
|
// We are goverend here by the TimeoutStream, no need to pass a CancellationToken here.
|
|
thisPtr.webSocketStream.WriteEndOfMessage(TimeSpan.MaxValue);
|
|
|
|
thisPtr.waitCallback.Invoke(thisPtr.state);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (Fx.IsFatal(ex))
|
|
{
|
|
throw;
|
|
}
|
|
thisPtr.AddPendingException(ex);
|
|
}
|
|
}
|
|
|
|
protected class WebSocketMessageSource : IMessageSource
|
|
{
|
|
static readonly Action<object> onAsyncReceiveCancelled = Fx.ThunkCallback<object>(OnAsyncReceiveCancelled);
|
|
MessageEncoder encoder;
|
|
BufferManager bufferManager;
|
|
EndpointAddress localAddress;
|
|
Message pendingMessage;
|
|
Exception pendingException;
|
|
WebSocketContext context;
|
|
WebSocket webSocket;
|
|
bool closureReceived = false;
|
|
bool useStreaming;
|
|
int receiveBufferSize;
|
|
int maxBufferSize;
|
|
long maxReceivedMessageSize;
|
|
TaskCompletionSource<object> streamWaitTask;
|
|
IDefaultCommunicationTimeouts defaultTimeouts;
|
|
RemoteEndpointMessageProperty remoteEndpointMessageProperty;
|
|
SecurityMessageProperty handshakeSecurityMessageProperty;
|
|
WebSocketCloseDetails closeDetails;
|
|
ReadOnlyDictionary<string, object> properties;
|
|
TimeSpan asyncReceiveTimeout;
|
|
TaskCompletionSource<object> receiveTask;
|
|
IOThreadTimer receiveTimer;
|
|
int asyncReceiveState;
|
|
|
|
public WebSocketMessageSource(WebSocketTransportDuplexSessionChannel webSocketTransportDuplexSessionChannel, WebSocket webSocket,
|
|
bool useStreaming, IDefaultCommunicationTimeouts defaultTimeouts)
|
|
{
|
|
this.Initialize(webSocketTransportDuplexSessionChannel, webSocket, useStreaming, defaultTimeouts);
|
|
|
|
this.StartNextReceiveAsync();
|
|
}
|
|
|
|
public WebSocketMessageSource(WebSocketTransportDuplexSessionChannel webSocketTransportDuplexSessionChannel, WebSocketContext context,
|
|
bool isStreamed, RemoteEndpointMessageProperty remoteEndpointMessageProperty, IDefaultCommunicationTimeouts defaultTimeouts, HttpRequestMessage requestMessage)
|
|
{
|
|
this.Initialize(webSocketTransportDuplexSessionChannel, context.WebSocket, isStreamed, defaultTimeouts);
|
|
|
|
IPrincipal user = requestMessage == null ? null : requestMessage.GetUserPrincipal();
|
|
this.context = new ServiceWebSocketContext(context, user);
|
|
this.remoteEndpointMessageProperty = remoteEndpointMessageProperty;
|
|
this.properties = requestMessage == null? null : new ReadOnlyDictionary<string, object>(requestMessage.Properties);
|
|
|
|
this.StartNextReceiveAsync();
|
|
}
|
|
|
|
void Initialize(WebSocketTransportDuplexSessionChannel webSocketTransportDuplexSessionChannel, WebSocket webSocket, bool useStreaming, IDefaultCommunicationTimeouts defaultTimeouts)
|
|
{
|
|
this.webSocket = webSocket;
|
|
this.encoder = webSocketTransportDuplexSessionChannel.MessageEncoder;
|
|
this.bufferManager = webSocketTransportDuplexSessionChannel.BufferManager;
|
|
this.localAddress = webSocketTransportDuplexSessionChannel.LocalAddress;
|
|
this.maxBufferSize = webSocketTransportDuplexSessionChannel.MaxBufferSize;
|
|
this.handshakeSecurityMessageProperty = webSocketTransportDuplexSessionChannel.RemoteSecurity;
|
|
this.maxReceivedMessageSize = webSocketTransportDuplexSessionChannel.TransportFactorySettings.MaxReceivedMessageSize;
|
|
this.receiveBufferSize = Math.Min(WebSocketHelper.GetReceiveBufferSize(this.maxReceivedMessageSize), this.maxBufferSize);
|
|
this.useStreaming = useStreaming;
|
|
this.defaultTimeouts = defaultTimeouts;
|
|
this.closeDetails = webSocketTransportDuplexSessionChannel.webSocketCloseDetails;
|
|
this.receiveTimer = new IOThreadTimer(onAsyncReceiveCancelled, this, true);
|
|
this.asyncReceiveState = AsyncReceiveState.Finished;
|
|
}
|
|
|
|
internal RemoteEndpointMessageProperty RemoteEndpointMessageProperty
|
|
{
|
|
get { return this.remoteEndpointMessageProperty; }
|
|
}
|
|
|
|
static void OnAsyncReceiveCancelled(object target)
|
|
{
|
|
WebSocketMessageSource messageSource = (WebSocketMessageSource)target;
|
|
messageSource.AsyncReceiveCancelled();
|
|
}
|
|
|
|
void AsyncReceiveCancelled()
|
|
{
|
|
if (Interlocked.CompareExchange(ref this.asyncReceiveState, AsyncReceiveState.Cancelled, AsyncReceiveState.Started) == AsyncReceiveState.Started)
|
|
{
|
|
this.receiveTask.SetResult(null);
|
|
}
|
|
}
|
|
|
|
public AsyncReceiveResult BeginReceive(TimeSpan timeout, WaitCallback callback, object state)
|
|
{
|
|
Fx.Assert(callback != null, "callback should not be null.");
|
|
|
|
if (this.receiveTask.Task.IsCompleted)
|
|
{
|
|
return AsyncReceiveResult.Completed;
|
|
}
|
|
else
|
|
{
|
|
this.asyncReceiveTimeout = timeout;
|
|
this.receiveTimer.Set(timeout);
|
|
this.receiveTask.Task.ContinueWith(t =>
|
|
{
|
|
callback.Invoke(state);
|
|
});
|
|
|
|
return AsyncReceiveResult.Pending;
|
|
}
|
|
}
|
|
|
|
public Message EndReceive()
|
|
{
|
|
if (this.asyncReceiveState == AsyncReceiveState.Cancelled)
|
|
{
|
|
throw FxTrace.Exception.AsError(WebSocketHelper.GetTimeoutException(null, this.asyncReceiveTimeout, WebSocketHelper.ReceiveOperation));
|
|
}
|
|
else
|
|
{
|
|
// IOThreadTimer.Cancel() will return false if we called IOThreadTimer.Set(Timespan.MaxValue) here, so we cannot reply on the return value of Cancel()
|
|
// call to see if Cancel() is fired or not. CSDMain 262179 filed for this.
|
|
this.receiveTimer.Cancel();
|
|
Fx.Assert(this.asyncReceiveState == AsyncReceiveState.Finished, "this.asyncReceiveState is not AsyncReceiveState.Finished: " + this.asyncReceiveState);
|
|
Message message = this.GetPendingMessage();
|
|
|
|
if (message != null)
|
|
{
|
|
// If we get any exception thrown out before that, the channel will be aborted thus no need to maintain the receive loop here.
|
|
this.StartNextReceiveAsync();
|
|
}
|
|
|
|
return message;
|
|
}
|
|
}
|
|
|
|
public Message Receive(TimeSpan timeout)
|
|
{
|
|
bool waitingResult = this.receiveTask.Task.Wait(timeout);
|
|
ThrowOnPendingException(ref this.pendingException);
|
|
|
|
if (!waitingResult)
|
|
{
|
|
throw FxTrace.Exception.AsError(new TimeoutException(
|
|
SR.GetString(SR.WaitForMessageTimedOut, timeout),
|
|
ThreadNeutralSemaphore.CreateEnterTimedOutException(timeout)));
|
|
}
|
|
|
|
Message message = this.GetPendingMessage();
|
|
|
|
if (message != null)
|
|
{
|
|
this.StartNextReceiveAsync();
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
public void UpdateOpenNotificationMessageProperties(MessageProperties messageProperties)
|
|
{
|
|
this.AddMessageProperties(messageProperties, WebSocketDefaults.DefaultWebSocketMessageType);
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage(FxCop.Category.ReliabilityBasic, "Reliability103",
|
|
Justification = "The exceptions are wrapped already.")]
|
|
async Task ReadBufferedMessageAsync()
|
|
{
|
|
byte[] internalBuffer = null;
|
|
try
|
|
{
|
|
internalBuffer = this.bufferManager.TakeBuffer(this.receiveBufferSize);
|
|
|
|
int receivedByteCount = 0;
|
|
bool endOfMessage = false;
|
|
WebSocketReceiveResult result = null;
|
|
do
|
|
{
|
|
try
|
|
{
|
|
|
|
if (TD.WebSocketAsyncReadStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncReadStart(this.webSocket.GetHashCode());
|
|
}
|
|
|
|
Task<WebSocketReceiveResult> receiveTask = this.webSocket.ReceiveAsync(
|
|
new ArraySegment<byte>(internalBuffer, receivedByteCount, internalBuffer.Length - receivedByteCount),
|
|
CancellationToken.None);
|
|
|
|
await receiveTask.ConfigureAwait(false);
|
|
|
|
result = receiveTask.Result;
|
|
this.CheckCloseStatus(result);
|
|
endOfMessage = result.EndOfMessage;
|
|
|
|
receivedByteCount += result.Count;
|
|
if (receivedByteCount >= internalBuffer.Length && !result.EndOfMessage)
|
|
{
|
|
if (internalBuffer.Length >= this.maxBufferSize)
|
|
{
|
|
this.pendingException = FxTrace.Exception.AsError(new QuotaExceededException(SR.GetString(SR.MaxReceivedMessageSizeExceeded, this.maxBufferSize)));
|
|
return;
|
|
}
|
|
|
|
int newSize = (int)Math.Min(((double)internalBuffer.Length) * 2, this.maxBufferSize);
|
|
Fx.Assert(newSize > 0, "buffer size should be larger than zero.");
|
|
byte[] newBuffer = this.bufferManager.TakeBuffer(newSize);
|
|
Buffer.BlockCopy(internalBuffer, 0, newBuffer, 0, receivedByteCount);
|
|
this.bufferManager.ReturnBuffer(internalBuffer);
|
|
internalBuffer = newBuffer;
|
|
}
|
|
|
|
if (TD.WebSocketAsyncReadStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncReadStop(
|
|
this.webSocket.GetHashCode(),
|
|
receivedByteCount,
|
|
TraceUtility.GetRemoteEndpointAddressPort(this.RemoteEndpointMessageProperty));
|
|
}
|
|
}
|
|
catch (AggregateException ex)
|
|
{
|
|
WebSocketHelper.ThrowCorrectException(ex, TimeSpan.MaxValue, WebSocketHelper.ReceiveOperation);
|
|
}
|
|
|
|
}
|
|
while (!endOfMessage && !this.closureReceived);
|
|
|
|
byte[] buffer = null;
|
|
bool success = false;
|
|
try
|
|
{
|
|
buffer = this.bufferManager.TakeBuffer(receivedByteCount);
|
|
Buffer.BlockCopy(internalBuffer, 0, buffer, 0, receivedByteCount);
|
|
Fx.Assert(result != null, "Result should not be null");
|
|
this.pendingMessage = this.PrepareMessage(result, buffer, receivedByteCount);
|
|
success = true;
|
|
}
|
|
finally
|
|
{
|
|
if (buffer != null && (!success || this.pendingMessage == null))
|
|
{
|
|
this.bufferManager.ReturnBuffer(buffer);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (Fx.IsFatal(ex))
|
|
{
|
|
throw;
|
|
}
|
|
|
|
this.pendingException = WebSocketHelper.ConvertAndTraceException(ex, TimeSpan.MaxValue, WebSocketHelper.ReceiveOperation);
|
|
}
|
|
finally
|
|
{
|
|
if (internalBuffer != null)
|
|
{
|
|
this.bufferManager.ReturnBuffer(internalBuffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage(FxCop.Category.ReliabilityBasic, "Reliability103",
|
|
Justification = "The exceptions are wrapped already.")]
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage(FxCop.Category.ReliabilityBasic, "Reliability104",
|
|
Justification = "The exceptions are traced already.")]
|
|
public AsyncReceiveResult BeginWaitForMessage(TimeSpan timeout, Threading.WaitCallback callback, object state)
|
|
{
|
|
try
|
|
{
|
|
return this.BeginReceive(timeout, callback, state);
|
|
}
|
|
catch (TimeoutException ex)
|
|
{
|
|
this.pendingException = FxTrace.Exception.AsError(ex);
|
|
return AsyncReceiveResult.Completed;
|
|
}
|
|
}
|
|
|
|
public bool EndWaitForMessage()
|
|
{
|
|
try
|
|
{
|
|
Message message = this.EndReceive();
|
|
this.pendingMessage = message;
|
|
return true;
|
|
}
|
|
catch (TimeoutException ex)
|
|
{
|
|
if (TD.ReceiveTimeoutIsEnabled())
|
|
{
|
|
TD.ReceiveTimeout(ex.Message);
|
|
}
|
|
|
|
DiagnosticUtility.TraceHandledException(ex, TraceEventType.Information);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool WaitForMessage(TimeSpan timeout)
|
|
{
|
|
try
|
|
{
|
|
Message message = this.Receive(timeout);
|
|
this.pendingMessage = message;
|
|
return true;
|
|
}
|
|
catch (TimeoutException exception)
|
|
{
|
|
if (TD.ReceiveTimeoutIsEnabled())
|
|
{
|
|
TD.ReceiveTimeout(exception.Message);
|
|
}
|
|
|
|
DiagnosticUtility.TraceHandledException(exception, TraceEventType.Information);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal void FinishUsingMessageStream(Exception ex)
|
|
{
|
|
//// The pattern of the task here is:
|
|
//// 1) Only one thread can get the stream and consume the stream. A new task will be created at the moment it takes the stream
|
|
//// 2) Only one another thread can enter the lock and wait on the task
|
|
//// 3) The cleanup on the stream will return the stream to message source. And the cleanup call is limited to be called only once.
|
|
if (ex != null && this.pendingException == null)
|
|
{
|
|
this.pendingException = ex;
|
|
}
|
|
|
|
this.streamWaitTask.SetResult(null);
|
|
}
|
|
|
|
internal void CheckCloseStatus(WebSocketReceiveResult result)
|
|
{
|
|
if (result.MessageType == WebSocketMessageType.Close)
|
|
{
|
|
if (TD.WebSocketCloseStatusReceivedIsEnabled())
|
|
{
|
|
TD.WebSocketCloseStatusReceived(
|
|
this.webSocket.GetHashCode(),
|
|
result.CloseStatus.ToString());
|
|
}
|
|
|
|
this.closureReceived = true;
|
|
this.closeDetails.InputCloseStatus = result.CloseStatus;
|
|
this.closeDetails.InputCloseStatusDescription = result.CloseStatusDescription;
|
|
}
|
|
}
|
|
|
|
async void StartNextReceiveAsync()
|
|
{
|
|
Fx.Assert(this.receiveTask == null || this.receiveTask.Task.IsCompleted, "this.receiveTask is not completed.");
|
|
this.receiveTask = new TaskCompletionSource<object>();
|
|
int currentState = Interlocked.CompareExchange(ref this.asyncReceiveState, AsyncReceiveState.Started, AsyncReceiveState.Finished);
|
|
Fx.Assert(currentState == AsyncReceiveState.Finished, "currentState is not AsyncReceiveState.Finished: " + currentState);
|
|
if (currentState != AsyncReceiveState.Finished)
|
|
{
|
|
throw FxTrace.Exception.AsError(new InvalidOperationException());
|
|
}
|
|
|
|
try
|
|
{
|
|
if (this.useStreaming)
|
|
{
|
|
if (this.streamWaitTask != null)
|
|
{
|
|
//// Wait until the previous stream message finished.
|
|
|
|
await this.streamWaitTask.Task.ConfigureAwait(false);
|
|
}
|
|
|
|
this.streamWaitTask = new TaskCompletionSource<object>();
|
|
}
|
|
|
|
if (this.pendingException == null)
|
|
{
|
|
if (!this.useStreaming)
|
|
{
|
|
await this.ReadBufferedMessageAsync().ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
byte[] buffer = this.bufferManager.TakeBuffer(this.receiveBufferSize);
|
|
bool success = false;
|
|
try
|
|
{
|
|
if (TD.WebSocketAsyncReadStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncReadStart(this.webSocket.GetHashCode());
|
|
}
|
|
|
|
try
|
|
{
|
|
Task<WebSocketReceiveResult> receiveTask = this.webSocket.ReceiveAsync(
|
|
new ArraySegment<byte>(buffer, 0, this.receiveBufferSize),
|
|
CancellationToken.None);
|
|
|
|
await receiveTask.ConfigureAwait(false);
|
|
|
|
WebSocketReceiveResult result = receiveTask.Result;
|
|
this.CheckCloseStatus(result);
|
|
this.pendingMessage = this.PrepareMessage(result, buffer, result.Count);
|
|
|
|
if (TD.WebSocketAsyncReadStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncReadStop(
|
|
this.webSocket.GetHashCode(),
|
|
result.Count,
|
|
TraceUtility.GetRemoteEndpointAddressPort(this.remoteEndpointMessageProperty));
|
|
}
|
|
}
|
|
catch (AggregateException ex)
|
|
{
|
|
WebSocketHelper.ThrowCorrectException(ex, this.asyncReceiveTimeout, WebSocketHelper.ReceiveOperation);
|
|
}
|
|
success = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (Fx.IsFatal(ex))
|
|
{
|
|
throw;
|
|
}
|
|
|
|
this.pendingException = WebSocketHelper.ConvertAndTraceException(ex, this.asyncReceiveTimeout, WebSocketHelper.ReceiveOperation);
|
|
}
|
|
finally
|
|
{
|
|
if (!success)
|
|
{
|
|
this.bufferManager.ReturnBuffer(buffer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (Interlocked.CompareExchange(ref this.asyncReceiveState, AsyncReceiveState.Finished, AsyncReceiveState.Started) == AsyncReceiveState.Started)
|
|
{
|
|
this.receiveTask.SetResult(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AddMessageProperties(MessageProperties messageProperties, WebSocketMessageType incomingMessageType)
|
|
{
|
|
Fx.Assert(messageProperties != null, "messageProperties should not be null.");
|
|
WebSocketMessageProperty messageProperty = new WebSocketMessageProperty(
|
|
this.context,
|
|
this.webSocket.SubProtocol,
|
|
incomingMessageType,
|
|
this.properties);
|
|
messageProperties.Add(WebSocketMessageProperty.Name, messageProperty);
|
|
|
|
if (this.remoteEndpointMessageProperty != null)
|
|
{
|
|
messageProperties.Add(RemoteEndpointMessageProperty.Name, this.remoteEndpointMessageProperty);
|
|
}
|
|
|
|
if (this.handshakeSecurityMessageProperty != null)
|
|
{
|
|
messageProperties.Security = (SecurityMessageProperty)this.handshakeSecurityMessageProperty.CreateCopy();
|
|
}
|
|
}
|
|
|
|
Message GetPendingMessage()
|
|
{
|
|
ThrowOnPendingException(ref this.pendingException);
|
|
|
|
if (this.pendingMessage != null)
|
|
{
|
|
Message pendingMessage = this.pendingMessage;
|
|
this.pendingMessage = null;
|
|
return pendingMessage;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
Message PrepareMessage(WebSocketReceiveResult result, byte[] buffer, int count)
|
|
{
|
|
if (result.MessageType != WebSocketMessageType.Close)
|
|
{
|
|
Message message;
|
|
if (this.useStreaming)
|
|
{
|
|
TimeoutHelper readTimeoutHelper = new TimeoutHelper(this.defaultTimeouts.ReceiveTimeout);
|
|
message = this.encoder.ReadMessage(
|
|
new MaxMessageSizeStream(
|
|
new TimeoutStream(
|
|
new WebSocketStream(
|
|
this,
|
|
new ArraySegment<byte>(buffer, 0, count),
|
|
this.webSocket,
|
|
result.EndOfMessage,
|
|
this.bufferManager,
|
|
this.defaultTimeouts.CloseTimeout),
|
|
ref readTimeoutHelper),
|
|
this.maxReceivedMessageSize),
|
|
this.maxBufferSize);
|
|
}
|
|
else
|
|
{
|
|
ArraySegment<byte> bytes = new ArraySegment<byte>(buffer, 0, count);
|
|
message = this.encoder.ReadMessage(bytes, this.bufferManager);
|
|
}
|
|
|
|
if (message.Version.Addressing != AddressingVersion.None || !this.localAddress.IsAnonymous)
|
|
{
|
|
this.localAddress.ApplyTo(message);
|
|
}
|
|
|
|
if (message.Version.Addressing == AddressingVersion.None && message.Headers.Action == null)
|
|
{
|
|
if (result.MessageType == WebSocketMessageType.Binary)
|
|
{
|
|
message.Headers.Action = WebSocketTransportSettings.BinaryMessageReceivedAction;
|
|
}
|
|
else
|
|
{
|
|
// WebSocketMesssageType should always be binary or text at this moment. The layer below us will help protect this.
|
|
Fx.Assert(result.MessageType == WebSocketMessageType.Text, "result.MessageType must be WebSocketMessageType.Text.");
|
|
message.Headers.Action = WebSocketTransportSettings.TextMessageReceivedAction;
|
|
}
|
|
}
|
|
|
|
if (message != null)
|
|
{
|
|
this.AddMessageProperties(message.Properties, result.MessageType);
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static class AsyncReceiveState
|
|
{
|
|
internal const int Started = 0;
|
|
internal const int Finished = 1;
|
|
internal const int Cancelled = 2;
|
|
}
|
|
}
|
|
|
|
class WebSocketStream : Stream
|
|
{
|
|
WebSocket webSocket;
|
|
WebSocketMessageSource messageSource;
|
|
TimeSpan closeTimeout;
|
|
ArraySegment<byte> initialReadBuffer;
|
|
bool endOfMessageReached = false;
|
|
bool isForRead;
|
|
bool endofMessageReceived;
|
|
WebSocketMessageType outgoingMessageType;
|
|
BufferManager bufferManager;
|
|
int messageSourceCleanState;
|
|
int endOfMessageWritten;
|
|
int readTimeout;
|
|
int writeTimeout;
|
|
|
|
public WebSocketStream(
|
|
WebSocketMessageSource messageSource,
|
|
ArraySegment<byte> initialBuffer,
|
|
WebSocket webSocket,
|
|
bool endofMessageReceived,
|
|
BufferManager bufferManager,
|
|
TimeSpan closeTimeout)
|
|
: this(webSocket, WebSocketDefaults.DefaultWebSocketMessageType, closeTimeout)
|
|
{
|
|
Fx.Assert(messageSource != null, "messageSource should not be null.");
|
|
this.messageSource = messageSource;
|
|
this.initialReadBuffer = initialBuffer;
|
|
this.isForRead = true;
|
|
this.endofMessageReceived = endofMessageReceived;
|
|
this.bufferManager = bufferManager;
|
|
this.messageSourceCleanState = WebSocketHelper.OperationNotStarted;
|
|
this.endOfMessageWritten = WebSocketHelper.OperationNotStarted;
|
|
}
|
|
|
|
public WebSocketStream(
|
|
WebSocket webSocket,
|
|
WebSocketMessageType outgoingMessageType,
|
|
TimeSpan closeTimeout)
|
|
{
|
|
Fx.Assert(webSocket != null, "webSocket should not be null.");
|
|
this.webSocket = webSocket;
|
|
this.isForRead = false;
|
|
this.outgoingMessageType = outgoingMessageType;
|
|
this.messageSourceCleanState = WebSocketHelper.OperationFinished;
|
|
this.closeTimeout = closeTimeout;
|
|
}
|
|
|
|
public override bool CanRead
|
|
{
|
|
get { return this.isForRead; }
|
|
}
|
|
|
|
public override bool CanSeek
|
|
{
|
|
get { return false; }
|
|
}
|
|
|
|
public override bool CanTimeout
|
|
{
|
|
get
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public override bool CanWrite
|
|
{
|
|
get { return !this.isForRead; }
|
|
}
|
|
|
|
public override long Length
|
|
{
|
|
get { throw FxTrace.Exception.AsError(new NotSupportedException(SR.GetString(SR.SeekNotSupported))); }
|
|
}
|
|
|
|
public override long Position
|
|
{
|
|
get
|
|
{
|
|
throw FxTrace.Exception.AsError(new NotSupportedException(SR.GetString(SR.SeekNotSupported)));
|
|
}
|
|
|
|
set
|
|
{
|
|
throw FxTrace.Exception.AsError(new NotSupportedException(SR.GetString(SR.SeekNotSupported)));
|
|
}
|
|
}
|
|
|
|
public override int ReadTimeout
|
|
{
|
|
get
|
|
{
|
|
return this.readTimeout;
|
|
}
|
|
|
|
set
|
|
{
|
|
Fx.Assert(value >= 0, "ReadTimeout should not be negative.");
|
|
this.readTimeout = value;
|
|
}
|
|
}
|
|
|
|
public override int WriteTimeout
|
|
{
|
|
get
|
|
{
|
|
return this.writeTimeout;
|
|
}
|
|
|
|
set
|
|
{
|
|
Fx.Assert(value >= 0, "WriteTimeout should not be negative.");
|
|
this.writeTimeout = value;
|
|
}
|
|
}
|
|
|
|
public override void Close()
|
|
{
|
|
TimeoutHelper helper = new TimeoutHelper(this.closeTimeout);
|
|
base.Close();
|
|
this.Cleanup(helper.RemainingTime());
|
|
}
|
|
|
|
public override void Flush()
|
|
{
|
|
}
|
|
|
|
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
|
{
|
|
Fx.Assert(this.messageSource != null, "messageSource should not be null in read case.");
|
|
|
|
if (this.ReadTimeout <= 0)
|
|
{
|
|
throw FxTrace.Exception.AsError(WebSocketHelper.GetTimeoutException(null, TimeoutHelper.FromMilliseconds(this.ReadTimeout), WebSocketHelper.ReceiveOperation));
|
|
}
|
|
|
|
TimeoutHelper helper = new TimeoutHelper(TimeoutHelper.FromMilliseconds(this.ReadTimeout));
|
|
|
|
if (this.endOfMessageReached)
|
|
{
|
|
return new CompletedAsyncResult<int>(0, callback, state);
|
|
}
|
|
|
|
if (this.initialReadBuffer.Count != 0)
|
|
{
|
|
int bytesRead = this.GetBytesFromInitialReadBuffer(buffer, offset, count);
|
|
return new CompletedAsyncResult<int>(bytesRead, callback, state);
|
|
}
|
|
|
|
if (this.endofMessageReceived)
|
|
{
|
|
this.endOfMessageReached = true;
|
|
return new CompletedAsyncResult<int>(0, callback, state);
|
|
}
|
|
|
|
if (TD.WebSocketAsyncReadStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncReadStart(this.webSocket.GetHashCode());
|
|
}
|
|
|
|
IOThreadCancellationTokenSource cancellationTokenSource = new IOThreadCancellationTokenSource(helper.RemainingTime());
|
|
Task<int> task = this.webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, offset, count), cancellationTokenSource.Token).ContinueWith(t =>
|
|
{
|
|
cancellationTokenSource.Dispose();
|
|
WebSocketHelper.ThrowExceptionOnTaskFailure(t, TimeoutHelper.FromMilliseconds(this.ReadTimeout), WebSocketHelper.ReceiveOperation);
|
|
this.endOfMessageReached = t.Result.EndOfMessage;
|
|
|
|
int receivedBytes = t.Result.Count;
|
|
CheckResultAndEnsureNotCloseMessage(this.messageSource, t.Result);
|
|
|
|
if (this.endOfMessageReached)
|
|
{
|
|
this.Cleanup(helper.RemainingTime());
|
|
}
|
|
|
|
if (TD.WebSocketAsyncReadStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncReadStop(
|
|
this.webSocket.GetHashCode(),
|
|
receivedBytes,
|
|
this.messageSource != null ? TraceUtility.GetRemoteEndpointAddressPort(this.messageSource.RemoteEndpointMessageProperty) : string.Empty);
|
|
}
|
|
|
|
return receivedBytes;
|
|
}, TaskContinuationOptions.None);
|
|
|
|
return task.AsAsyncResult<int>(callback, state);
|
|
}
|
|
|
|
public override int EndRead(IAsyncResult asyncResult)
|
|
{
|
|
Task<int> task = (Task<int>)asyncResult;
|
|
WebSocketHelper.ThrowExceptionOnTaskFailure((Task)task, TimeoutHelper.FromMilliseconds(this.ReadTimeout), WebSocketHelper.ReceiveOperation);
|
|
return task.Result;
|
|
}
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage(FxCop.Category.ReliabilityBasic, "Reliability104",
|
|
Justification = "The exceptions will be traced and thrown by the handling method.")]
|
|
public override int Read(byte[] buffer, int offset, int count)
|
|
{
|
|
Fx.Assert(this.messageSource != null, "messageSource should not be null in read case.");
|
|
|
|
if (this.ReadTimeout <= 0)
|
|
{
|
|
throw FxTrace.Exception.AsError(WebSocketHelper.GetTimeoutException(null, TimeoutHelper.FromMilliseconds(this.ReadTimeout), WebSocketHelper.ReceiveOperation));
|
|
}
|
|
|
|
TimeoutHelper helper = new TimeoutHelper(TimeoutHelper.FromMilliseconds(this.ReadTimeout));
|
|
|
|
if (this.endOfMessageReached)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (this.initialReadBuffer.Count != 0)
|
|
{
|
|
return this.GetBytesFromInitialReadBuffer(buffer, offset, count);
|
|
}
|
|
|
|
int receivedBytes = 0;
|
|
if (this.endofMessageReceived)
|
|
{
|
|
this.endOfMessageReached = true;
|
|
}
|
|
else
|
|
{
|
|
if (TD.WebSocketAsyncReadStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncReadStart(this.webSocket.GetHashCode());
|
|
}
|
|
|
|
Task<WebSocketReceiveResult> task = this.webSocket.ReceiveAsync(new ArraySegment<byte>(buffer, offset, count), CancellationToken.None);
|
|
task.Wait(helper.RemainingTime(), WebSocketHelper.ThrowCorrectException, WebSocketHelper.ReceiveOperation);
|
|
if (task.Result.EndOfMessage)
|
|
{
|
|
this.endofMessageReceived = true;
|
|
this.endOfMessageReached = true;
|
|
}
|
|
|
|
receivedBytes = task.Result.Count;
|
|
CheckResultAndEnsureNotCloseMessage(this.messageSource, task.Result);
|
|
|
|
if (TD.WebSocketAsyncReadStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncReadStop(
|
|
this.webSocket.GetHashCode(),
|
|
receivedBytes,
|
|
this.messageSource != null ? TraceUtility.GetRemoteEndpointAddressPort(this.messageSource.RemoteEndpointMessageProperty) : string.Empty);
|
|
}
|
|
}
|
|
|
|
if (this.endOfMessageReached)
|
|
{
|
|
this.Cleanup(helper.RemainingTime());
|
|
}
|
|
|
|
return receivedBytes;
|
|
}
|
|
|
|
public override long Seek(long offset, SeekOrigin origin)
|
|
{
|
|
throw FxTrace.Exception.AsError(new NotSupportedException());
|
|
}
|
|
|
|
public override void SetLength(long value)
|
|
{
|
|
throw FxTrace.Exception.AsError(new NotSupportedException());
|
|
}
|
|
|
|
public override void Write(byte[] buffer, int offset, int count)
|
|
{
|
|
if (this.endOfMessageWritten == WebSocketHelper.OperationFinished)
|
|
{
|
|
throw FxTrace.Exception.AsError(new InvalidOperationException(SR.GetString(SR.WebSocketStreamWriteCalledAfterEOMSent)));
|
|
}
|
|
|
|
if (this.WriteTimeout <= 0)
|
|
{
|
|
throw FxTrace.Exception.AsError(WebSocketHelper.GetTimeoutException(null, TimeoutHelper.FromMilliseconds(this.WriteTimeout), WebSocketHelper.SendOperation));
|
|
}
|
|
|
|
if (TD.WebSocketAsyncWriteStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStart(
|
|
this.webSocket.GetHashCode(),
|
|
count,
|
|
this.messageSource != null ? TraceUtility.GetRemoteEndpointAddressPort(this.messageSource.RemoteEndpointMessageProperty) : string.Empty);
|
|
}
|
|
|
|
Task task = this.webSocket.SendAsync(new ArraySegment<byte>(buffer, offset, count), this.outgoingMessageType, false, CancellationToken.None);
|
|
task.Wait(TimeoutHelper.FromMilliseconds(this.WriteTimeout), WebSocketHelper.ThrowCorrectException, WebSocketHelper.SendOperation);
|
|
|
|
if (TD.WebSocketAsyncWriteStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStop(this.webSocket.GetHashCode());
|
|
}
|
|
}
|
|
|
|
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
|
{
|
|
if (this.endOfMessageWritten == WebSocketHelper.OperationFinished)
|
|
{
|
|
throw FxTrace.Exception.AsError(new InvalidOperationException(SR.GetString(SR.WebSocketStreamWriteCalledAfterEOMSent)));
|
|
}
|
|
|
|
if (this.WriteTimeout <= 0)
|
|
{
|
|
throw FxTrace.Exception.AsError(WebSocketHelper.GetTimeoutException(null, TimeoutHelper.FromMilliseconds(this.WriteTimeout), WebSocketHelper.SendOperation));
|
|
}
|
|
|
|
if (TD.WebSocketAsyncWriteStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStart(
|
|
this.webSocket.GetHashCode(),
|
|
count,
|
|
this.messageSource != null ? TraceUtility.GetRemoteEndpointAddressPort(this.messageSource.RemoteEndpointMessageProperty) : string.Empty);
|
|
}
|
|
|
|
IOThreadCancellationTokenSource cancellationTokenSource = new IOThreadCancellationTokenSource(this.WriteTimeout);
|
|
Task task = this.webSocket.SendAsync(new ArraySegment<byte>(buffer, offset, count), this.outgoingMessageType, false, cancellationTokenSource.Token).ContinueWith(t =>
|
|
{
|
|
if (TD.WebSocketAsyncWriteStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStop(this.webSocket.GetHashCode());
|
|
}
|
|
|
|
cancellationTokenSource.Dispose();
|
|
WebSocketHelper.ThrowExceptionOnTaskFailure(t, TimeoutHelper.FromMilliseconds(this.WriteTimeout), WebSocketHelper.SendOperation);
|
|
});
|
|
|
|
return task.AsAsyncResult(callback, state);
|
|
}
|
|
|
|
public override void EndWrite(IAsyncResult asyncResult)
|
|
{
|
|
Task task = (Task)asyncResult;
|
|
WebSocketHelper.ThrowExceptionOnTaskFailure(task, TimeoutHelper.FromMilliseconds(this.WriteTimeout), WebSocketHelper.SendOperation);
|
|
}
|
|
|
|
public void WriteEndOfMessage(TimeSpan timeout)
|
|
{
|
|
if (TD.WebSocketAsyncWriteStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStart(
|
|
this.webSocket.GetHashCode(),
|
|
0,
|
|
this.messageSource != null ? TraceUtility.GetRemoteEndpointAddressPort(this.messageSource.RemoteEndpointMessageProperty) : string.Empty);
|
|
}
|
|
|
|
if (Interlocked.CompareExchange(ref this.endOfMessageWritten, WebSocketHelper.OperationFinished, WebSocketHelper.OperationNotStarted) == WebSocketHelper.OperationNotStarted)
|
|
{
|
|
Task task = this.webSocket.SendAsync(new ArraySegment<byte>(EmptyArray<byte>.Instance, 0, 0), this.outgoingMessageType, true, CancellationToken.None);
|
|
task.Wait(timeout, WebSocketHelper.ThrowCorrectException, WebSocketHelper.SendOperation);
|
|
}
|
|
|
|
if (TD.WebSocketAsyncWriteStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStop(this.webSocket.GetHashCode());
|
|
}
|
|
}
|
|
|
|
public async void WriteEndOfMessageAsync(TimeSpan timeout, WaitCallback callback, object state)
|
|
{
|
|
if (TD.WebSocketAsyncWriteStartIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStart(
|
|
this.webSocket.GetHashCode(),
|
|
0,
|
|
this.messageSource != null ? TraceUtility.GetRemoteEndpointAddressPort(this.messageSource.RemoteEndpointMessageProperty) : string.Empty);
|
|
}
|
|
|
|
using (IOThreadCancellationTokenSource cancellationTokenSource = new IOThreadCancellationTokenSource(timeout))
|
|
{
|
|
try
|
|
{
|
|
Task task = this.webSocket.SendAsync(new ArraySegment<byte>(EmptyArray<byte>.Instance, 0, 0), this.outgoingMessageType, true, cancellationTokenSource.Token);
|
|
|
|
// The callback here will only be TransportDuplexSessionChannel.OnWriteComplete. It's safe to call this callback without flowing
|
|
// security context here since there's no user code involved.
|
|
await task.SuppressContextFlow();
|
|
|
|
if (TD.WebSocketAsyncWriteStopIsEnabled())
|
|
{
|
|
TD.WebSocketAsyncWriteStop(this.webSocket.GetHashCode());
|
|
}
|
|
}
|
|
catch(AggregateException ex)
|
|
{
|
|
WebSocketHelper.ThrowCorrectException(ex, TimeoutHelper.FromMilliseconds(this.WriteTimeout), WebSocketHelper.SendOperation);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (Fx.IsFatal(ex))
|
|
{
|
|
throw;
|
|
}
|
|
|
|
WebSocketHelper.ThrowCorrectException(ex);
|
|
}
|
|
finally
|
|
{
|
|
callback.Invoke(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CheckResultAndEnsureNotCloseMessage(WebSocketMessageSource messageSource, WebSocketReceiveResult result)
|
|
{
|
|
messageSource.CheckCloseStatus(result);
|
|
if (result.MessageType == WebSocketMessageType.Close)
|
|
{
|
|
throw FxTrace.Exception.AsError(new ProtocolException(SR.GetString(SR.WebSocketUnexpectedCloseMessageError)));
|
|
}
|
|
}
|
|
|
|
int GetBytesFromInitialReadBuffer(byte[] buffer, int offset, int count)
|
|
{
|
|
int bytesToCopy = this.initialReadBuffer.Count > count ? count : this.initialReadBuffer.Count;
|
|
Buffer.BlockCopy(this.initialReadBuffer.Array, this.initialReadBuffer.Offset, buffer, offset, bytesToCopy);
|
|
this.initialReadBuffer = new ArraySegment<byte>(this.initialReadBuffer.Array, this.initialReadBuffer.Offset + bytesToCopy, this.initialReadBuffer.Count - bytesToCopy);
|
|
return bytesToCopy;
|
|
}
|
|
|
|
void Cleanup(TimeSpan timeout)
|
|
{
|
|
if (this.isForRead)
|
|
{
|
|
if (Interlocked.CompareExchange(ref this.messageSourceCleanState, WebSocketHelper.OperationFinished, WebSocketHelper.OperationNotStarted) == WebSocketHelper.OperationNotStarted)
|
|
{
|
|
Exception pendingException = null;
|
|
try
|
|
{
|
|
if (!this.endofMessageReceived && (this.webSocket.State == WebSocketState.Open || this.webSocket.State == WebSocketState.CloseSent))
|
|
{
|
|
// Drain the reading stream
|
|
TimeoutHelper helper = new TimeoutHelper(timeout);
|
|
do
|
|
{
|
|
Task<WebSocketReceiveResult> receiveTask = this.webSocket.ReceiveAsync(new ArraySegment<byte>(this.initialReadBuffer.Array), CancellationToken.None);
|
|
receiveTask.Wait(helper.RemainingTime(), WebSocketHelper.ThrowCorrectException, WebSocketHelper.ReceiveOperation);
|
|
this.endofMessageReceived = receiveTask.Result.EndOfMessage;
|
|
}
|
|
while (!this.endofMessageReceived && (this.webSocket.State == WebSocketState.Open || this.webSocket.State == WebSocketState.CloseSent));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (Fx.IsFatal(ex))
|
|
{
|
|
throw;
|
|
}
|
|
|
|
// Not throwing out this exception during stream cleanup. The exception
|
|
// will be thrown out when we are trying to receive the next message using the same
|
|
// WebSocket object.
|
|
pendingException = WebSocketHelper.ConvertAndTraceException(ex, timeout, WebSocketHelper.CloseOperation);
|
|
}
|
|
|
|
this.bufferManager.ReturnBuffer(this.initialReadBuffer.Array);
|
|
Fx.Assert(this.messageSource != null, "messageSource should not be null.");
|
|
this.messageSource.FinishUsingMessageStream(pendingException);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Interlocked.CompareExchange(ref this.endOfMessageWritten, WebSocketHelper.OperationFinished, WebSocketHelper.OperationNotStarted) == WebSocketHelper.OperationNotStarted)
|
|
{
|
|
this.WriteEndOfMessage(timeout);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class WebSocketCloseDetails : IWebSocketCloseDetails
|
|
{
|
|
WebSocketCloseStatus outputCloseStatus = WebSocketCloseStatus.NormalClosure;
|
|
string outputCloseStatusDescription;
|
|
WebSocketCloseStatus? inputCloseStatus;
|
|
string inputCloseStatusDescription;
|
|
|
|
public WebSocketCloseStatus? InputCloseStatus
|
|
{
|
|
get
|
|
{
|
|
return this.inputCloseStatus;
|
|
}
|
|
|
|
internal set
|
|
{
|
|
this.inputCloseStatus = value;
|
|
}
|
|
}
|
|
|
|
public string InputCloseStatusDescription
|
|
{
|
|
get
|
|
{
|
|
return this.inputCloseStatusDescription;
|
|
}
|
|
|
|
internal set
|
|
{
|
|
this.inputCloseStatusDescription = value;
|
|
}
|
|
}
|
|
|
|
internal WebSocketCloseStatus OutputCloseStatus
|
|
{
|
|
get
|
|
{
|
|
return this.outputCloseStatus;
|
|
}
|
|
}
|
|
|
|
internal string OutputCloseStatusDescription
|
|
{
|
|
get
|
|
{
|
|
return this.outputCloseStatusDescription;
|
|
}
|
|
}
|
|
|
|
public void SetOutputCloseStatus(WebSocketCloseStatus closeStatus, string closeStatusDescription)
|
|
{
|
|
this.outputCloseStatus = closeStatus;
|
|
this.outputCloseStatusDescription = closeStatusDescription;
|
|
}
|
|
}
|
|
}
|
|
}
|