e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
625 lines
28 KiB
C#
625 lines
28 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="AspNetWebSocket.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//------------------------------------------------------------------------------
|
|
|
|
namespace System.Web.WebSockets {
|
|
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Net.WebSockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Web.Util;
|
|
|
|
// Used to send and receive messages over a WebSocket connection
|
|
//
|
|
// SYNCHRONIZATION AND THREAD SAFETY:
|
|
//
|
|
// This class is not generally thread-safe, but the following exceptions are made:
|
|
//
|
|
// - We'll detect multiple calls to SendAsync (e.g. a second call takes place while the first
|
|
// call isn't yet complete) and throw an appropriate exception. CloseOutputAsync is
|
|
// an equivalent of SendAsync for this purpose.
|
|
//
|
|
// - There can be a pending call to SendAsync (or CloseOutputAsync) at the same time as a
|
|
// call to ReceiveAsync, and we'll handle synchronization properly.
|
|
//
|
|
// - CloseAsync is the equivalent of starting parallel sends and receives. If there's already
|
|
// a send (SendAsync / CloseOutputAsync) or receive (ReceiveAsync) in progress, we'll
|
|
// detect this and throw an appropriate exception.
|
|
//
|
|
// - Any thread can call Abort at any time. Any pending sends / receives will result in
|
|
// failure. Continuations hanging off the failed tasks will execute, but they may do
|
|
// so before or after the call to Abort returns.
|
|
//
|
|
// - Dispose is *not* thread-safe and should not be called while the socket has a
|
|
// pending operation.
|
|
//
|
|
// TAP asks that if exceptions are thrown due to precondition violations (e.g. bad parameters
|
|
// or the object is in an invalid state), these be thrown synchronously from the Async method
|
|
// rather than shoved into the resulting Task. Hence in the design below, the AsyncImpl methods
|
|
// themselves are synchronous and perform any necessary initialization, then they return
|
|
// delegates that can be used to kick off the asynchronous operations. This allows the
|
|
// AsyncImpl callers to provide coarser locking around multiple methods if necessary. In all
|
|
// other cases, the locks are held for the absolute minimum time necessary to guarantee thread-
|
|
// safety. Importantly, locks must never be held while awaiting an asynchronous method.
|
|
//
|
|
// VISIBILITY:
|
|
//
|
|
// Aside from the ctor, internal members of this type are not meant to be called from outside
|
|
// the type itself. Any internal members are only internal to ease unit testing. We have the
|
|
// goal of allowing third-party developers to create the same higher-level abstractions over
|
|
// this type that we can, and having ASP.NET call internal members runs counter to that goal.
|
|
//
|
|
// TERMINOLOGY:
|
|
//
|
|
// The WebSocket protocol is defined at:
|
|
// http://tools.ietf.org/html/rfc6455
|
|
//
|
|
// "WSPC" is the WebSocket Protocol Component (websocket.dll), which provides WebSocket-
|
|
// parsing services to WCF, IIS, IE, and others. ASP.NET doesn't call into WSPC directly;
|
|
// rather, IIS calls into WSPC on our behalf.
|
|
|
|
public sealed class AspNetWebSocket : WebSocket, IAsyncAbortableWebSocket {
|
|
|
|
// RFC 6455, Sec. 5.5:
|
|
// All control frames MUST be 125 bytes or less in length and MUST NOT be fragmented.
|
|
//
|
|
// When a CLOSE frame is sent, it has an optional payload that consists of a 2-byte status code followed by a
|
|
// UTF8-encoded string. This string must therefore be no greater than 123 bytes (after UTF8-encoding) in length.
|
|
private const int _maxCloseMessageByteCount = 123;
|
|
|
|
// represents no-op tasks or asynchronous operations
|
|
private static readonly Task _completedTask = Task.FromResult<object>(null);
|
|
private static readonly Func<Task> _completedTaskFunc = () => _completedTask;
|
|
|
|
// machine-readable / human-readable reasons this WebSocket was closed
|
|
private const WebSocketCloseStatus CLOSE_STATUS_NOT_SET = (WebSocketCloseStatus)(-1); // used as a 'null' placeholder for _closeStatus since Nullable<T> access not guaranteed atomic
|
|
private WebSocketCloseStatus _closeStatus = CLOSE_STATUS_NOT_SET;
|
|
private string _closeStatusDescription;
|
|
|
|
// for determining whether the socket has been disposed of
|
|
private bool _disposed;
|
|
|
|
// the pipe used for low-level communication with the remote endpoint
|
|
private readonly IWebSocketPipe _pipe;
|
|
private readonly string _subProtocol;
|
|
|
|
// the current state of the RECEIVE (remote to local) and SEND (local to remote) channels
|
|
private int _abortAsyncCalled;
|
|
private CountdownTask _pendingOperationCounter = new CountdownTask(1);
|
|
internal ChannelState _receiveState;
|
|
internal ChannelState _sendState;
|
|
|
|
// the current state (observable externally) of this socket
|
|
internal WebSocketState _state = WebSocketState.Open;
|
|
|
|
// for synchronization around controlling state transitions
|
|
private readonly object _stateLockObj = new object();
|
|
|
|
internal AspNetWebSocket(IWebSocketPipe pipe, string subProtocol) {
|
|
_pipe = pipe;
|
|
_subProtocol = subProtocol;
|
|
|
|
// It is possible that a pipe couldn't be established, e.g. if the client disconnects or there is
|
|
// a server error before the handshake is complete. If this is the case, we just immediately
|
|
// mark this AspNetWebSocket instance as aborted.
|
|
if (_pipe == null) {
|
|
Abort();
|
|
}
|
|
}
|
|
|
|
public override WebSocketCloseStatus? CloseStatus {
|
|
get {
|
|
ThrowIfDisposed();
|
|
|
|
WebSocketCloseStatus closeStatus = _closeStatus;
|
|
if (closeStatus == CLOSE_STATUS_NOT_SET) {
|
|
// no close status (not even Unspecified) to report to the caller; most likely reason is connection hasn't been closed
|
|
return null;
|
|
}
|
|
else {
|
|
return closeStatus;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override string CloseStatusDescription {
|
|
get {
|
|
ThrowIfDisposed();
|
|
return _closeStatusDescription;
|
|
}
|
|
}
|
|
|
|
public override WebSocketState State {
|
|
get {
|
|
ThrowIfDisposed();
|
|
return _state;
|
|
}
|
|
}
|
|
|
|
public override string SubProtocol {
|
|
get {
|
|
ThrowIfDisposed();
|
|
return _subProtocol;
|
|
}
|
|
}
|
|
|
|
// for unit testing
|
|
internal CountdownTask PendingOperationCounter {
|
|
get {
|
|
return _pendingOperationCounter;
|
|
}
|
|
}
|
|
|
|
// Rudely closes a connection and cancels all pending I/O.
|
|
// This is a fire-and-forget method which never blocks.
|
|
// Both this and AbortAsync() are thread-safe.
|
|
public override void Abort() {
|
|
Abort(throwIfDisposed: true); // throws if disposed, per spec
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "CloseTcpConnection() is not a dangerous method.")]
|
|
private void Abort(bool throwIfDisposed, bool isDisposing = false) {
|
|
lock (_stateLockObj) {
|
|
// State validation
|
|
if (throwIfDisposed) {
|
|
ThrowIfDisposed();
|
|
}
|
|
|
|
try {
|
|
if (IsStateTerminal(_state)) {
|
|
// If we're already in a terminal state, do nothing.
|
|
return;
|
|
}
|
|
else {
|
|
// Allowed transitions:
|
|
// - Open -> Aborted
|
|
// - CloseSent -> Aborted
|
|
// - CloseReceived -> Aborted
|
|
_state = WebSocketState.Aborted;
|
|
}
|
|
}
|
|
finally {
|
|
// Is this being called from Dispose()?
|
|
if (isDisposing) {
|
|
_disposed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- CORE LOGIC --
|
|
// Everything after this point will be executed only once.
|
|
|
|
if (_pipe != null) {
|
|
_pipe.CloseTcpConnection();
|
|
}
|
|
}
|
|
|
|
// Similar to Abort(), but returns a Task which allows a caller to know when
|
|
// the Abort operation is complete. When the returned Task is complete, this
|
|
// means that there will be no more calls to or from the _pipe object.
|
|
// This method is always safe to call, even if the socket has been disposed.
|
|
// This method must be called at the end of WebSocket processing in order
|
|
// to release resources associated with this socket.
|
|
internal Task AbortAsync() {
|
|
Abort(throwIfDisposed: false);
|
|
|
|
if (Interlocked.Exchange(ref _abortAsyncCalled, 1) == 0) {
|
|
// The CountdownTask was seeded with an initial value of 1 so that the
|
|
// associated Task wouldn't be marked as complete until this call to
|
|
// MarkOperationCompleted. If there is pending IO, the current value
|
|
// might still be > 1.
|
|
_pendingOperationCounter.MarkOperationCompleted();
|
|
}
|
|
return _pendingOperationCounter.Task;
|
|
}
|
|
|
|
// Asynchronously sends a CLOSE frame and waits for the remote endpoint to send a CLOSE/ACK.
|
|
public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) {
|
|
return CloseAsyncImpl(closeStatus, statusDescription, cancellationToken)();
|
|
}
|
|
|
|
private Func<Task> CloseAsyncImpl(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken, bool performValidation = true) {
|
|
Func<Task> sendCloseTaskFunc = null;
|
|
Func<Task<WebSocketReceiveResult>> receiveCloseTaskFunc = null;
|
|
|
|
// -- PARAMETER VALIDATION --
|
|
// Performed outside lock since doesn't affect state
|
|
|
|
if (performValidation) {
|
|
ValidateCloseStatusCodeAndDescription(closeStatus, ref statusDescription);
|
|
}
|
|
|
|
lock (_stateLockObj) {
|
|
// -- STATE VALIDATION --
|
|
// Performed within lock
|
|
|
|
if (performValidation) {
|
|
ThrowIfDisposed();
|
|
ThrowIfAborted();
|
|
ThrowIfSendUnavailable(allowClosed: true);
|
|
ThrowIfReceiveUnavailable(allowClosed: true);
|
|
}
|
|
|
|
// -- STATE INITIALIZATION --
|
|
// Performed within lock
|
|
|
|
// State transitions are handled automatically by the CloseOutputAsyncImpl / ReceiveAsyncImpl methods
|
|
// These methods don't need to perform validation since this method has already performed it (if necessary)
|
|
if (_sendState != ChannelState.Closed) {
|
|
sendCloseTaskFunc = CloseOutputAsyncImpl(closeStatus, statusDescription, cancellationToken, performValidation: false);
|
|
}
|
|
if (_receiveState != ChannelState.Closed) {
|
|
ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[_maxCloseMessageByteCount]);
|
|
receiveCloseTaskFunc = ReceiveAsyncImpl(buffer, cancellationToken, performValidation: false);
|
|
}
|
|
|
|
// special-case no outstanding work to perform
|
|
if (sendCloseTaskFunc == null && receiveCloseTaskFunc == null) {
|
|
return _completedTaskFunc;
|
|
}
|
|
}
|
|
|
|
// once initialization is complete, the implementation can run truly asynchronously
|
|
return async () => {
|
|
// By kicking off both tasks in parallel before awaiting either one, we have full-duplex communication
|
|
Task sendCloseTask = (sendCloseTaskFunc != null) ? sendCloseTaskFunc() : null;
|
|
Task<WebSocketReceiveResult> receiveCloseTask = (receiveCloseTaskFunc != null) ? receiveCloseTaskFunc() : null;
|
|
|
|
if (sendCloseTask != null) {
|
|
await sendCloseTask.ConfigureAwait(continueOnCapturedContext: false);
|
|
|
|
// -- ASYNC POINT --
|
|
// Any code after this which requires synchronization must reacquire the lock
|
|
}
|
|
|
|
if (receiveCloseTask != null) {
|
|
WebSocketReceiveResult result = await receiveCloseTask.ConfigureAwait(continueOnCapturedContext: false);
|
|
|
|
// -- ASYNC POINT --
|
|
// Any code after this which requires synchronization must reacquire the lock
|
|
|
|
// Throw if this wasn't a CLOSE frame
|
|
if (result.MessageType != WebSocketMessageType.Close) {
|
|
Abort(throwIfDisposed: false);
|
|
throw new WebSocketException(WebSocketError.InvalidMessageType);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Sends a CLOSE frame asynchronously.
|
|
public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) {
|
|
return CloseOutputAsyncImpl(closeStatus, statusDescription, cancellationToken)();
|
|
}
|
|
|
|
private Func<Task> CloseOutputAsyncImpl(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken, bool performValidation = true) {
|
|
// -- PARAMETER VALIDATION --
|
|
// Performed outside lock since doesn't affect state
|
|
|
|
if (performValidation) {
|
|
ValidateCloseStatusCodeAndDescription(closeStatus, ref statusDescription);
|
|
}
|
|
|
|
lock (_stateLockObj) {
|
|
// -- STATE VALIDATION --
|
|
// Performed within lock
|
|
|
|
if (performValidation) {
|
|
ThrowIfDisposed();
|
|
ThrowIfAborted();
|
|
ThrowIfSendUnavailable(allowClosed: true);
|
|
}
|
|
|
|
// -- STATE INITIALIZATION --
|
|
// Performed within lock
|
|
|
|
if (_sendState == ChannelState.Closed) {
|
|
// already closed; nothing to do
|
|
return _completedTaskFunc;
|
|
}
|
|
|
|
// Mark channel as in use
|
|
_sendState = ChannelState.Busy;
|
|
_pendingOperationCounter.MarkOperationPending();
|
|
}
|
|
|
|
// once initialization is complete, the implementation can run truly asynchronously
|
|
return async () => {
|
|
// Send the fragment
|
|
await DoWork(() => _pipe.WriteCloseFragmentAsync(closeStatus, statusDescription), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
|
|
|
// -- ASYNC POINT --
|
|
// Any code after this which requires synchronization must reacquire the lock
|
|
|
|
lock (_stateLockObj) {
|
|
_sendState = ChannelState.Closed;
|
|
|
|
// State transition:
|
|
// - Open -> CloseSent
|
|
// - CloseReceived -> Closed
|
|
switch (_state) {
|
|
case WebSocketState.Open:
|
|
_state = WebSocketState.CloseSent;
|
|
break;
|
|
|
|
case WebSocketState.CloseReceived:
|
|
_state = WebSocketState.Closed;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Releases resources associated with this object; similar to calling Abort
|
|
public override void Dispose() {
|
|
throw new NotSupportedException(SR.GetString(SR.AspNetWebSocket_DisposeNotSupported));
|
|
}
|
|
|
|
internal void DisposeInternal() {
|
|
Abort(throwIfDisposed: false, isDisposing: true);
|
|
}
|
|
|
|
|
|
// Receives a single fragment from the remote endpoint
|
|
public override Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken) {
|
|
return ReceiveAsyncImpl(buffer, cancellationToken)();
|
|
}
|
|
|
|
private Func<Task<WebSocketReceiveResult>> ReceiveAsyncImpl(ArraySegment<byte> buffer, CancellationToken cancellationToken, bool performValidation = true) {
|
|
lock (_stateLockObj) {
|
|
// -- STATE VALIDATION --
|
|
// Performed within lock
|
|
|
|
if (performValidation) {
|
|
ThrowIfDisposed();
|
|
ThrowIfAborted();
|
|
ThrowIfReceiveUnavailable();
|
|
}
|
|
|
|
// -- STATE INITIALIZATION --
|
|
// Performed within lock
|
|
|
|
// Mark channel as in use
|
|
_receiveState = ChannelState.Busy;
|
|
_pendingOperationCounter.MarkOperationPending();
|
|
}
|
|
|
|
// once initialization is complete, the implementation can run truly asynchronously
|
|
return async () => {
|
|
// Receive a single fragment
|
|
WebSocketReceiveResult result = await DoWork(() => _pipe.ReadFragmentAsync(buffer), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
|
|
|
// -- ASYNC POINT --
|
|
// Any code after this which requires synchronization must reacquire the lock
|
|
|
|
lock (_stateLockObj) {
|
|
if (result.MessageType == WebSocketMessageType.Close) {
|
|
// received a CLOSE frame
|
|
_receiveState = ChannelState.Closed;
|
|
Debug.Assert(result.CloseStatus.HasValue, "The CloseStatus property should be non-null when a CLOSE frame is received.");
|
|
_closeStatus = result.CloseStatus.Value;
|
|
_closeStatusDescription = result.CloseStatusDescription;
|
|
|
|
// State transition:
|
|
// - Open -> CloseReceived
|
|
// - CloseSent -> Closed
|
|
switch (_state) {
|
|
case WebSocketState.Open:
|
|
_state = WebSocketState.CloseReceived;
|
|
break;
|
|
|
|
case WebSocketState.CloseSent:
|
|
_state = WebSocketState.Closed;
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
_receiveState = ChannelState.Ready;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
}
|
|
|
|
// Sends a single fragment to the remote endpoint
|
|
public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) {
|
|
return SendAsyncImpl(buffer, messageType, endOfMessage, cancellationToken)();
|
|
}
|
|
|
|
private Func<Task> SendAsyncImpl(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken, bool performValidation = true) {
|
|
// -- PARAMETER VALIDATION --
|
|
// Performed outside lock since doesn't affect state
|
|
|
|
if (performValidation) {
|
|
ValidateSendMessageType(messageType);
|
|
}
|
|
|
|
lock (_stateLockObj) {
|
|
// -- STATE VALIDATION --
|
|
// Performed within lock
|
|
|
|
if (performValidation) {
|
|
ThrowIfDisposed();
|
|
ThrowIfAborted();
|
|
ThrowIfSendUnavailable();
|
|
}
|
|
|
|
// -- STATE INITIALIZATION --
|
|
// Performed within lock
|
|
|
|
// Mark channel as in use
|
|
_sendState = ChannelState.Busy;
|
|
_pendingOperationCounter.MarkOperationPending();
|
|
}
|
|
|
|
// once initialization is complete, the implementation can run truly asynchronously
|
|
return async () => {
|
|
// Send the fragment
|
|
await DoWork(() => _pipe.WriteFragmentAsync(buffer, (messageType == WebSocketMessageType.Text), endOfMessage), cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
|
|
|
// -- ASYNC POINT --
|
|
// Any code after this which requires synchronization must reacquire the lock
|
|
|
|
lock (_stateLockObj) {
|
|
_sendState = ChannelState.Ready;
|
|
}
|
|
};
|
|
}
|
|
|
|
#region Validation and helper methods
|
|
// Validates that the provided 'closeStatus' and 'statusDescription' parameters are sane.
|
|
// The 'statusDescription' parameter is taken by reference since it might be normalized.
|
|
internal static void ValidateCloseStatusCodeAndDescription(WebSocketCloseStatus closeStatus, ref string statusDescription) {
|
|
// We do three checks here:
|
|
// - RFC 6455, Sec. 5.5.1: Close status is 16-bit unsigned integer, so we need to make sure it is within the range 0x0000 - 0xffff.
|
|
// - RFC 6455, Sec. 7.4.2: 0 - 999 is an invalid value for the close status.
|
|
// - RFC 6455, Sec. 7.4.1: 1004, 1006, 1010, 1015 are invalid status codes for the server to send to the client.
|
|
if (closeStatus < (WebSocketCloseStatus)1000
|
|
|| closeStatus > (WebSocketCloseStatus)UInt16.MaxValue
|
|
|| closeStatus == (WebSocketCloseStatus)1004
|
|
|| closeStatus == (WebSocketCloseStatus)1006
|
|
|| closeStatus == (WebSocketCloseStatus)1010
|
|
|| closeStatus == (WebSocketCloseStatus)1015) {
|
|
throw new ArgumentOutOfRangeException("closeStatus");
|
|
}
|
|
|
|
if (closeStatus == WebSocketCloseStatus.Empty) {
|
|
// Fix Bug : 312472, we would like to allow empty strings to be passed to our APIs when status code is 1005.
|
|
// Since WSPC requires the statusDescription to be null, we convert.
|
|
if (statusDescription == String.Empty) {
|
|
statusDescription = null;
|
|
}
|
|
// If the status code is 1005 (Empty), the statusDescription string MUST be null.
|
|
// This behavior is required by WSPC and matches WCF.
|
|
if (statusDescription != null) {
|
|
throw new ArgumentException(SR.GetString(SR.AspNetWebSocket_CloseStatusEmptyButCloseDescriptionNonNull), "statusDescription");
|
|
}
|
|
}
|
|
else if (statusDescription != null) {
|
|
// Need to make sure the provided status description fits within a single WebSocket control frame.
|
|
int byteCount = Encoding.UTF8.GetByteCount(statusDescription);
|
|
if (byteCount > _maxCloseMessageByteCount) {
|
|
throw new ArgumentException(SR.GetString(SR.AspNetWebSocket_CloseDescriptionTooLong, _maxCloseMessageByteCount), "statusDescription");
|
|
}
|
|
}
|
|
else {
|
|
// WSPC requires that null status descriptions be normalized to empty strings.
|
|
statusDescription = String.Empty;
|
|
}
|
|
}
|
|
|
|
private static void ValidateSendMessageType(WebSocketMessageType messageType) {
|
|
switch (messageType) {
|
|
case WebSocketMessageType.Text:
|
|
case WebSocketMessageType.Binary:
|
|
return; // these are OK
|
|
|
|
default:
|
|
throw new ArgumentException(SR.GetString(SR.AspNetWebSocket_SendMessageTypeInvalid), "messageType");
|
|
}
|
|
}
|
|
|
|
private void ThrowIfDisposed() {
|
|
if (_disposed) {
|
|
throw new ObjectDisposedException(objectName: GetType().FullName);
|
|
}
|
|
}
|
|
|
|
private void ThrowIfAborted() {
|
|
if (_state == WebSocketState.Aborted) {
|
|
throw new WebSocketException(WebSocketError.InvalidState);
|
|
}
|
|
}
|
|
|
|
private void ThrowIfSendUnavailable(bool allowClosed = false) {
|
|
switch (_sendState) {
|
|
case ChannelState.Busy:
|
|
throw new InvalidOperationException(SR.GetString(SR.AspNetWebSocket_SendInProgress));
|
|
|
|
case ChannelState.Closed:
|
|
if (allowClosed) { break; }
|
|
throw new InvalidOperationException(SR.GetString(SR.AspNetWebSocket_CloseAlreadySent));
|
|
}
|
|
}
|
|
|
|
private void ThrowIfReceiveUnavailable(bool allowClosed = false) {
|
|
switch (_receiveState) {
|
|
case ChannelState.Busy:
|
|
throw new InvalidOperationException(SR.GetString(SR.AspNetWebSocket_ReceiveInProgress));
|
|
|
|
case ChannelState.Closed:
|
|
if (allowClosed) { break; }
|
|
throw new InvalidOperationException(SR.GetString(SR.AspNetWebSocket_CloseAlreadyReceived));
|
|
}
|
|
}
|
|
|
|
// Helper method that wraps performing some work and aborting on exceptions
|
|
private async Task<T> DoWork<T>(Func<Task<T>> taskDelegate, CancellationToken cancellationToken) {
|
|
// Used for timing out operations
|
|
IDisposable cancellationTokenRegistration = null;
|
|
|
|
try {
|
|
try {
|
|
// If cancellation is requested, honor it immediately. This is the same kind of optimization done throughout
|
|
// the rest of the framework, e.g. as in FileStream.ReadAsync. Our finally block will be responsible for
|
|
// throwing the actual exception which results in the Task being canceled.
|
|
if (cancellationToken.IsCancellationRequested) {
|
|
return default(T);
|
|
}
|
|
|
|
Task<T> task = taskDelegate();
|
|
|
|
if (!task.IsCompleted && cancellationToken.CanBeCanceled) {
|
|
// If the task didn't complete synchronously, let the CancellationToken abort the operation
|
|
// The callback may complete inline, so we need to make sure it doesn't throw
|
|
cancellationTokenRegistration = cancellationToken.Register(() => Abort(throwIfDisposed: false));
|
|
}
|
|
|
|
// The 'await' keyword may cause an exception to be observed (rethrown)
|
|
// We call only thread-safe methods so don't need to spend the time to come back to the SynchronizationContext
|
|
return await task.ConfigureAwait(continueOnCapturedContext: false);
|
|
}
|
|
finally {
|
|
// called for both normal and exceptional completion
|
|
_pendingOperationCounter.MarkOperationCompleted();
|
|
|
|
// failed due to cancellation?
|
|
if (cancellationTokenRegistration != null) {
|
|
cancellationTokenRegistration.Dispose();
|
|
}
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
}
|
|
}
|
|
catch {
|
|
// Something went wrong while communicating on the pipe - mark faulted and observe exception.
|
|
// Benign ---- - Abort might be called both by the CancellationTokenRegistration and by the line below.
|
|
Abort(throwIfDisposed: false);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Helper method that wraps performing some work and aborting on exceptions
|
|
internal Task DoWork(Func<Task> taskDelegate, CancellationToken cancellationToken) {
|
|
return DoWork<object>(async () => { await taskDelegate().ConfigureAwait(continueOnCapturedContext: false); return null; }, cancellationToken);
|
|
}
|
|
#endregion
|
|
|
|
Task IAsyncAbortableWebSocket.AbortAsync() {
|
|
return AbortAsync();
|
|
}
|
|
|
|
// Represents the state of a single channel; send and receive have their own states
|
|
internal enum ChannelState {
|
|
Ready, // this channel is available for transmitting new frames
|
|
Busy, // the channel is already busy transmitting frames
|
|
Closed // this channel has been closed
|
|
}
|
|
|
|
}
|
|
}
|