Merge branch 'release/2.1' into dev
This commit is contained in:
commit
4c6c0aa8a4
|
|
@ -5,6 +5,18 @@
|
|||
<TypeScriptCompileBlocked>True</TypeScriptCompileBlocked>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="selenium\run-ci-tests.ts" />
|
||||
<None Remove="selenium\run-tests.ts" />
|
||||
<None Remove="ts\Common.ts" />
|
||||
<None Remove="ts\ConnectionTests.ts" />
|
||||
<None Remove="ts\HubConnectionTests.ts" />
|
||||
<None Remove="ts\index.ts" />
|
||||
<None Remove="ts\Utils.ts" />
|
||||
<None Remove="ts\WebDriverReporter.ts" />
|
||||
<None Remove="ts\WebSocketTests.ts" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.SignalR.MsgPack\Microsoft.AspNetCore.SignalR.MsgPack.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.csproj" />
|
||||
|
|
@ -25,6 +37,18 @@
|
|||
<Folder Include="wwwroot\js\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<TypeScriptCompile Include="selenium\run-ci-tests.ts" />
|
||||
<TypeScriptCompile Include="selenium\run-tests.ts" />
|
||||
<TypeScriptCompile Include="ts\Common.ts" />
|
||||
<TypeScriptCompile Include="ts\ConnectionTests.ts" />
|
||||
<TypeScriptCompile Include="ts\HubConnectionTests.ts" />
|
||||
<TypeScriptCompile Include="ts\index.ts" />
|
||||
<TypeScriptCompile Include="ts\Utils.ts" />
|
||||
<TypeScriptCompile Include="ts\WebDriverReporter.ts" />
|
||||
<TypeScriptCompile Include="ts\WebSocketTests.ts" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="ClientBuild" BeforeTargets="AfterBuild">
|
||||
<ItemGroup>
|
||||
<MsgPack5Files Include="$(MSBuildThisFileDirectory)../signalr-protocol-msgpack/node_modules/msgpack5/dist/*.js" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"noEmitOnError": true,
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -35,36 +36,25 @@ namespace ClientSample
|
|||
baseUrl = string.IsNullOrEmpty(baseUrl) ? "http://localhost:5000/chat" : baseUrl;
|
||||
|
||||
var loggerFactory = new LoggerFactory();
|
||||
var logger = loggerFactory.CreateLogger<Program>();
|
||||
|
||||
Console.WriteLine($"Connecting to {baseUrl}...");
|
||||
var connection = new HttpConnection(new Uri(baseUrl), loggerFactory);
|
||||
try
|
||||
{
|
||||
var closeTcs = new TaskCompletionSource<object>();
|
||||
connection.Closed += e => closeTcs.SetResult(null);
|
||||
connection.OnReceived(data => Console.Out.WriteLineAsync($"{Encoding.UTF8.GetString(data)}"));
|
||||
await connection.StartAsync(TransferFormat.Text);
|
||||
|
||||
Console.WriteLine($"Connected to {baseUrl}");
|
||||
var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += async (sender, a) =>
|
||||
var shutdown = new TaskCompletionSource<object>();
|
||||
Console.CancelKeyPress += (sender, a) =>
|
||||
{
|
||||
a.Cancel = true;
|
||||
await connection.DisposeAsync();
|
||||
shutdown.TrySetResult(null);
|
||||
};
|
||||
|
||||
while (!closeTcs.Task.IsCompleted)
|
||||
{
|
||||
var line = await Task.Run(() => Console.ReadLine(), cts.Token);
|
||||
_ = ReceiveLoop(Console.Out, connection.Transport.Input);
|
||||
_ = SendLoop(Console.In, connection.Transport.Output);
|
||||
|
||||
if (line == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await connection.SendAsync(Encoding.UTF8.GetBytes(line), cts.Token);
|
||||
}
|
||||
await shutdown.Task;
|
||||
}
|
||||
catch (AggregateException aex) when (aex.InnerExceptions.All(e => e is OperationCanceledException))
|
||||
{
|
||||
|
|
@ -78,5 +68,40 @@ namespace ClientSample
|
|||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static async Task ReceiveLoop(TextWriter output, PipeReader input)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var result = await input.ReadAsync();
|
||||
var buffer = result.Buffer;
|
||||
|
||||
try
|
||||
{
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
await output.WriteLineAsync(Encoding.UTF8.GetString(buffer.ToArray()));
|
||||
}
|
||||
else if (result.IsCompleted)
|
||||
{
|
||||
// No more data, and the pipe is complete
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
input.AdvanceTo(buffer.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SendLoop(TextReader input, PipeWriter output)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var result = await input.ReadLineAsync();
|
||||
await output.WriteAsync(Encoding.UTF8.GetBytes(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
<handlers>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
|
||||
</handlers>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" />
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00">
|
||||
<environmentVariables />
|
||||
</aspNetCore>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
event.preventDefault();
|
||||
});
|
||||
|
||||
connection.start().then(function () {
|
||||
connection.start(signalR.TransferFormat.Text).then(function () {
|
||||
console.log("Opened");
|
||||
}, function () {
|
||||
console.log("Error opening connection");
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client
|
||||
|
|
@ -17,29 +18,29 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
private static readonly Action<ILogger, string, string, string, int, Exception> _preparingBlockingInvocation =
|
||||
LoggerMessage.Define<string, string, string, int>(LogLevel.Trace, new EventId(2, "PreparingBlockingInvocation"), "Preparing blocking invocation '{InvocationId}' of '{Target}', with return type '{ReturnType}' and {ArgumentCount} argument(s).");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _registerInvocation =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(3, "RegisterInvocation"), "Registering Invocation ID '{InvocationId}' for tracking.");
|
||||
private static readonly Action<ILogger, string, Exception> _registeringInvocation =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(3, "RegisteringInvocation"), "Registering Invocation ID '{InvocationId}' for tracking.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, string, Exception> _issueInvocation =
|
||||
LoggerMessage.Define<string, string, string, string>(LogLevel.Trace, new EventId(4, "IssueInvocation"), "Issuing Invocation '{InvocationId}': {ReturnType} {MethodName}({Args}).");
|
||||
private static readonly Action<ILogger, string, string, string, string, Exception> _issuingInvocation =
|
||||
LoggerMessage.Define<string, string, string, string>(LogLevel.Trace, new EventId(4, "IssuingInvocation"), "Issuing Invocation '{InvocationId}': {ReturnType} {MethodName}({Args}).");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _sendInvocation =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(5, "SendInvocation"), "Sending Invocation '{InvocationId}'.");
|
||||
private static readonly Action<ILogger, string, string, Exception> _sendingMessage =
|
||||
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(5, "SendingMessage"), "Sending {MessageType} message '{InvocationId}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _sendInvocationCompleted =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(6, "SendInvocationCompleted"), "Sending Invocation '{InvocationId}' completed.");
|
||||
private static readonly Action<ILogger, string, string, Exception> _messageSent =
|
||||
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(6, "MessageSent"), "Sending {MessageType} message '{InvocationId}' completed.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _sendInvocationFailed =
|
||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(7, "SendInvocationFailed"), "Sending Invocation '{InvocationId}' failed.");
|
||||
private static readonly Action<ILogger, string, Exception> _failedToSendInvocation =
|
||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(7, "FailedToSendInvocation"), "Sending Invocation '{InvocationId}' failed.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, Exception> _receivedInvocation =
|
||||
LoggerMessage.Define<string, string, string>(LogLevel.Trace, new EventId(8, "ReceivedInvocation"), "Received Invocation '{InvocationId}': {MethodName}({Args}).");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _dropCompletionMessage =
|
||||
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(9, "DropCompletionMessage"), "Dropped unsolicited Completion message for invocation '{InvocationId}'.");
|
||||
private static readonly Action<ILogger, string, Exception> _droppedCompletionMessage =
|
||||
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(9, "DroppedCompletionMessage"), "Dropped unsolicited Completion message for invocation '{InvocationId}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _dropStreamMessage =
|
||||
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(10, "DropStreamMessage"), "Dropped unsolicited StreamItem message for invocation '{InvocationId}'.");
|
||||
private static readonly Action<ILogger, string, Exception> _droppedStreamMessage =
|
||||
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(10, "DroppedStreamMessage"), "Dropped unsolicited StreamItem message for invocation '{InvocationId}'.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _shutdownConnection =
|
||||
LoggerMessage.Define(LogLevel.Trace, new EventId(11, "ShutdownConnection"), "Shutting down connection.");
|
||||
|
|
@ -47,8 +48,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
private static readonly Action<ILogger, Exception> _shutdownWithError =
|
||||
LoggerMessage.Define(LogLevel.Error, new EventId(12, "ShutdownWithError"), "Connection is shutting down due to an error.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _removeInvocation =
|
||||
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(13, "RemoveInvocation"), "Removing pending invocation {InvocationId}.");
|
||||
private static readonly Action<ILogger, string, Exception> _removingInvocation =
|
||||
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(13, "RemovingInvocation"), "Removing pending invocation {InvocationId}.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _missingHandler =
|
||||
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(14, "MissingHandler"), "Failed to find handler for '{Target}' method.");
|
||||
|
|
@ -68,11 +69,11 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
private static readonly Action<ILogger, string, Exception> _cancelingInvocationCompletion =
|
||||
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(19, "CancelingInvocationCompletion"), "Canceling dispatch of Completion message for Invocation {InvocationId}. The invocation was canceled.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _cancelingCompletion =
|
||||
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(20, "CancelingCompletion"), "Canceling dispatch of Completion message for Invocation {InvocationId}. The invocation was canceled.");
|
||||
private static readonly Action<ILogger, string, string, int, Exception> _releasingConnectionLock =
|
||||
LoggerMessage.Define<string, string, int>(LogLevel.Trace, new EventId(20, "ReleasingConnectionLock"), "Releasing Connection Lock in {MethodName} ({FilePath}:{LineNumber}).");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _invokeAfterTermination =
|
||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(21, "InvokeAfterTermination"), "Invoke for Invocation '{InvocationId}' was called after the connection was terminated.");
|
||||
private static readonly Action<ILogger, Exception> _stopped =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(21, "Stopped"), "HubConnection stopped.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _invocationAlreadyInUse =
|
||||
LoggerMessage.Define<string>(LogLevel.Critical, new EventId(22, "InvocationAlreadyInUse"), "Invocation ID '{InvocationId}' is already in use.");
|
||||
|
|
@ -125,6 +126,60 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
private static readonly Action<ILogger, string, Exception> _receivedCloseWithError =
|
||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(38, "ReceivedCloseWithError"), "Received close message with an error: {Error}");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _handshakeComplete =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(39, "HandshakeComplete"), "Handshake with server complete.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _registeringHandler =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(40, "RegisteringHandler"), "Registering handler for client method '{MethodName}'.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _starting =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(41, "Starting"), "Starting HubConnection.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, int, Exception> _waitingOnConnectionLock =
|
||||
LoggerMessage.Define<string, string, int>(LogLevel.Trace, new EventId(42, "WaitingOnConnectionLock"), "Waiting on Connection Lock in {MethodName} ({FilePath}:{LineNumber}).");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _errorStartingConnection =
|
||||
LoggerMessage.Define(LogLevel.Error, new EventId(43, "ErrorStartingConnection"), "Error starting connection.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _started =
|
||||
LoggerMessage.Define(LogLevel.Information, new EventId(44, "Started"), "HubConnection started.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _sendingCancellation =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(45, "SendingCancellation"), "Sending Cancellation for Invocation '{InvocationId}'.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _cancelingOutstandingInvocations =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(46, "CancelingOutstandingInvocations"), "Canceling all outstanding invocations.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _receiveLoopStarting =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(47, "ReceiveLoopStarting"), "Receive loop starting.");
|
||||
|
||||
private static readonly Action<ILogger, double, Exception> _startingServerTimeoutTimer =
|
||||
LoggerMessage.Define<double>(LogLevel.Debug, new EventId(48, "StartingServerTimeoutTimer"), "Starting server timeout timer. Duration: {ServerTimeout:0.00}ms");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _notUsingServerTimeout =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(49, "NotUsingServerTimeout"), "Not using server timeout because the transport inherently tracks server availability.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _serverDisconnectedWithError =
|
||||
LoggerMessage.Define(LogLevel.Error, new EventId(50, "ServerDisconnectedWithError"), "The server connection was terminated with an error.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _invokingClosedEventHandler =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(51, "InvokingClosedEventHandler"), "Invoking the Closed event handler.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _stopping =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(52, "Stopping"), "Stopping HubConnection.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _terminatingReceiveLoop =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(53, "TerminatingReceiveLoop"), "Terminating receive loop.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _waitingForReceiveLoopToTerminate =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(54, "WaitingForReceiveLoopToTerminate"), "Waiting for the receive loop to terminate.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _unableToSendCancellation =
|
||||
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(55, "UnableToSendCancellation"), "Unable to send cancellation for invocation '{InvocationId}'. The connection is inactive.");
|
||||
|
||||
private static readonly Action<ILogger, long, Exception> _processingMessage =
|
||||
LoggerMessage.Define<long>(LogLevel.Debug, new EventId(56, "ProcessingMessage"), "Processing {MessageLength} byte message from server.");
|
||||
|
||||
public static void PreparingNonBlockingInvocation(ILogger logger, string target, int count)
|
||||
{
|
||||
_preparingNonBlockingInvocation(logger, target, count, null);
|
||||
|
|
@ -140,33 +195,39 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
_preparingStreamingInvocation(logger, invocationId, target, returnType, count, null);
|
||||
}
|
||||
|
||||
public static void RegisterInvocation(ILogger logger, string invocationId)
|
||||
public static void RegisteringInvocation(ILogger logger, string invocationId)
|
||||
{
|
||||
_registerInvocation(logger, invocationId, null);
|
||||
_registeringInvocation(logger, invocationId, null);
|
||||
}
|
||||
|
||||
public static void IssueInvocation(ILogger logger, string invocationId, string returnType, string methodName, object[] args)
|
||||
public static void IssuingInvocation(ILogger logger, string invocationId, string returnType, string methodName, object[] args)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
var argsList = args == null ? string.Empty : string.Join(", ", args.Select(a => a?.GetType().FullName ?? "(null)"));
|
||||
_issueInvocation(logger, invocationId, returnType, methodName, argsList, null);
|
||||
_issuingInvocation(logger, invocationId, returnType, methodName, argsList, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendInvocation(ILogger logger, string invocationId)
|
||||
public static void SendingMessage(ILogger logger, HubInvocationMessage message)
|
||||
{
|
||||
_sendInvocation(logger, invocationId, null);
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_sendingMessage(logger, message.GetType().Name, message.InvocationId, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendInvocationCompleted(ILogger logger, string invocationId)
|
||||
public static void MessageSent(ILogger logger, HubInvocationMessage message)
|
||||
{
|
||||
_sendInvocationCompleted(logger, invocationId, null);
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_messageSent(logger, message.GetType().Name, message.InvocationId, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendInvocationFailed(ILogger logger, string invocationId, Exception exception)
|
||||
public static void FailedToSendInvocation(ILogger logger, string invocationId, Exception exception)
|
||||
{
|
||||
_sendInvocationFailed(logger, invocationId, exception);
|
||||
_failedToSendInvocation(logger, invocationId, exception);
|
||||
}
|
||||
|
||||
public static void ReceivedInvocation(ILogger logger, string invocationId, string methodName, object[] args)
|
||||
|
|
@ -178,14 +239,14 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
}
|
||||
}
|
||||
|
||||
public static void DropCompletionMessage(ILogger logger, string invocationId)
|
||||
public static void DroppedCompletionMessage(ILogger logger, string invocationId)
|
||||
{
|
||||
_dropCompletionMessage(logger, invocationId, null);
|
||||
_droppedCompletionMessage(logger, invocationId, null);
|
||||
}
|
||||
|
||||
public static void DropStreamMessage(ILogger logger, string invocationId)
|
||||
public static void DroppedStreamMessage(ILogger logger, string invocationId)
|
||||
{
|
||||
_dropStreamMessage(logger, invocationId, null);
|
||||
_droppedStreamMessage(logger, invocationId, null);
|
||||
}
|
||||
|
||||
public static void ShutdownConnection(ILogger logger)
|
||||
|
|
@ -198,9 +259,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
_shutdownWithError(logger, exception);
|
||||
}
|
||||
|
||||
public static void RemoveInvocation(ILogger logger, string invocationId)
|
||||
public static void RemovingInvocation(ILogger logger, string invocationId)
|
||||
{
|
||||
_removeInvocation(logger, invocationId, null);
|
||||
_removingInvocation(logger, invocationId, null);
|
||||
}
|
||||
|
||||
public static void MissingHandler(ILogger logger, string target)
|
||||
|
|
@ -233,14 +294,9 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
_cancelingInvocationCompletion(logger, invocationId, null);
|
||||
}
|
||||
|
||||
public static void CancelingCompletion(ILogger logger, string invocationId)
|
||||
public static void Stopped(ILogger logger)
|
||||
{
|
||||
_cancelingCompletion(logger, invocationId, null);
|
||||
}
|
||||
|
||||
public static void InvokeAfterTermination(ILogger logger, string invocationId)
|
||||
{
|
||||
_invokeAfterTermination(logger, invocationId, null);
|
||||
_stopped(logger, null);
|
||||
}
|
||||
|
||||
public static void InvocationAlreadyInUse(ILogger logger, string invocationId)
|
||||
|
|
@ -322,6 +378,105 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
{
|
||||
_receivedCloseWithError(logger, error, null);
|
||||
}
|
||||
|
||||
public static void HandshakeComplete(ILogger logger)
|
||||
{
|
||||
_handshakeComplete(logger, null);
|
||||
}
|
||||
|
||||
public static void RegisteringHandler(ILogger logger, string methodName)
|
||||
{
|
||||
_registeringHandler(logger, methodName, null);
|
||||
}
|
||||
|
||||
public static void Starting(ILogger logger)
|
||||
{
|
||||
_starting(logger, null);
|
||||
}
|
||||
|
||||
public static void ErrorStartingConnection(ILogger logger, Exception ex)
|
||||
{
|
||||
_errorStartingConnection(logger, ex);
|
||||
}
|
||||
|
||||
public static void Started(ILogger logger)
|
||||
{
|
||||
_started(logger, null);
|
||||
}
|
||||
|
||||
public static void SendingCancellation(ILogger logger, string invocationId)
|
||||
{
|
||||
_sendingCancellation(logger, invocationId, null);
|
||||
}
|
||||
|
||||
public static void CancelingOutstandingInvocations(ILogger logger)
|
||||
{
|
||||
_cancelingOutstandingInvocations(logger, null);
|
||||
}
|
||||
|
||||
public static void ReceiveLoopStarting(ILogger logger)
|
||||
{
|
||||
_receiveLoopStarting(logger, null);
|
||||
}
|
||||
|
||||
public static void StartingServerTimeoutTimer(ILogger logger, TimeSpan serverTimeout)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_startingServerTimeoutTimer(logger, serverTimeout.TotalMilliseconds, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotUsingServerTimeout(ILogger logger)
|
||||
{
|
||||
_notUsingServerTimeout(logger, null);
|
||||
}
|
||||
|
||||
public static void ServerDisconnectedWithError(ILogger logger, Exception ex)
|
||||
{
|
||||
_serverDisconnectedWithError(logger, ex);
|
||||
}
|
||||
|
||||
public static void InvokingClosedEventHandler(ILogger logger)
|
||||
{
|
||||
_invokingClosedEventHandler(logger, null);
|
||||
}
|
||||
|
||||
public static void Stopping(ILogger logger)
|
||||
{
|
||||
_stopping(logger, null);
|
||||
}
|
||||
|
||||
public static void TerminatingReceiveLoop(ILogger logger)
|
||||
{
|
||||
_terminatingReceiveLoop(logger, null);
|
||||
}
|
||||
|
||||
public static void WaitingForReceiveLoopToTerminate(ILogger logger)
|
||||
{
|
||||
_waitingForReceiveLoopToTerminate(logger, null);
|
||||
}
|
||||
|
||||
public static void ProcessingMessage(ILogger logger, long length)
|
||||
{
|
||||
_processingMessage(logger, length, null);
|
||||
}
|
||||
|
||||
public static void WaitingOnConnectionLock(ILogger logger, string memberName, string filePath, int lineNumber)
|
||||
{
|
||||
_waitingOnConnectionLock(logger, memberName, filePath, lineNumber, null);
|
||||
}
|
||||
|
||||
public static void ReleasingConnectionLock(ILogger logger, string memberName, string filePath, int lineNumber)
|
||||
{
|
||||
_releasingConnectionLock(logger, memberName, filePath, lineNumber, null);
|
||||
}
|
||||
|
||||
public static void UnableToSendCancellation(ILogger logger, string invocationId)
|
||||
{
|
||||
_unableToSendCancellation(logger, invocationId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -43,12 +43,11 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
}
|
||||
|
||||
IHubConnectionBuilder builder = this;
|
||||
var connection = _connectionFactoryDelegate();
|
||||
|
||||
var loggerFactory = builder.GetLoggerFactory();
|
||||
var hubProtocol = builder.GetHubProtocol();
|
||||
|
||||
return new HubConnection(connection, hubProtocol ?? new JsonHubProtocol(), loggerFactory);
|
||||
return new HubConnection(_connectionFactoryDelegate, hubProtocol ?? new JsonHubProtocol(), loggerFactory);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
|||
|
||||
protected override void Cancel()
|
||||
{
|
||||
_channel.Writer.TryComplete(new OperationCanceledException("Invocation terminated"));
|
||||
_channel.Writer.TryComplete(new OperationCanceledException());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.Client.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -1,12 +1,30 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Internal.Formatters
|
||||
{
|
||||
public static class TextMessageParser
|
||||
{
|
||||
public static bool TryParseMessage(ref ReadOnlySequence<byte> buffer, out ReadOnlySequence<byte> payload)
|
||||
{
|
||||
var position = buffer.PositionOf(TextMessageFormatter.RecordSeparator);
|
||||
if (position == null)
|
||||
{
|
||||
payload = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
payload = buffer.Slice(0, position.Value);
|
||||
|
||||
// Skip record separator
|
||||
buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseMessage(ref ReadOnlyMemory<byte> buffer, out ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
var index = buffer.Span.IndexOf(TextMessageFormatter.RecordSeparator);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
|
@ -11,16 +12,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
public interface IConnection
|
||||
{
|
||||
Task StartAsync(TransferFormat transferFormat);
|
||||
Task SendAsync(byte[] data, CancellationToken cancellationToken);
|
||||
Task StopAsync();
|
||||
Task DisposeAsync();
|
||||
Task AbortAsync(Exception ex);
|
||||
|
||||
IDisposable OnReceived(Func<byte[], object, Task> callback, object state);
|
||||
|
||||
event Action<Exception> Closed;
|
||||
|
||||
IDuplexPipe Transport { get; }
|
||||
IFeatureCollection Features { get; }
|
||||
|
||||
Task StartAsync();
|
||||
Task StartAsync(TransferFormat transferFormat);
|
||||
Task DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Net.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
|
|
|
|||
|
|
@ -5,110 +5,91 @@ using System;
|
|||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Http
|
||||
{
|
||||
public partial class HttpConnection
|
||||
{
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, Exception> _httpConnectionStarting =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(1, "HttpConnectionStarting"), "Starting connection.");
|
||||
private static readonly Action<ILogger, Exception> _starting =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(1, "Starting"), "Starting HttpConnection.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _httpConnectionClosed =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "HttpConnectionClosed"), "Connection was closed from a different thread.");
|
||||
private static readonly Action<ILogger, Exception> _skippingStart =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(2, "SkippingStart"), "Skipping start, connection is already started.");
|
||||
|
||||
private static readonly Action<ILogger, string, Uri, Exception> _startingTransport =
|
||||
LoggerMessage.Define<string, Uri>(LogLevel.Debug, new EventId(3, "StartingTransport"), "Starting transport '{Transport}' with Url: {Url}.");
|
||||
private static readonly Action<ILogger, Exception> _started =
|
||||
LoggerMessage.Define(LogLevel.Information, new EventId(3, "Started"), "HttpConnection Started.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _processRemainingMessages =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(4, "ProcessRemainingMessages"), "Ensuring all outstanding messages are processed.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _drainEvents =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(5, "DrainEvents"), "Draining event queue.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _completeClosed =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(6, "CompleteClosed"), "Completing Closed task.");
|
||||
|
||||
private static readonly Action<ILogger, Uri, Exception> _establishingConnection =
|
||||
LoggerMessage.Define<Uri>(LogLevel.Debug, new EventId(7, "EstablishingConnection"), "Establishing Connection at: {Url}.");
|
||||
|
||||
private static readonly Action<ILogger, Uri, Exception> _errorWithNegotiation =
|
||||
LoggerMessage.Define<Uri>(LogLevel.Error, new EventId(8, "ErrorWithNegotiation"), "Failed to start connection. Error getting negotiation response from '{Url}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _errorStartingTransport =
|
||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(9, "ErrorStartingTransport"), "Failed to start connection. Error starting transport '{Transport}'.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _httpReceiveStarted =
|
||||
LoggerMessage.Define(LogLevel.Trace, new EventId(10, "HttpReceiveStarted"), "Beginning receive loop.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _skipRaisingReceiveEvent =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(11, "SkipRaisingReceiveEvent"), "Message received but connection is not connected. Skipping raising Received event.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _scheduleReceiveEvent =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(12, "ScheduleReceiveEvent"), "Scheduling raising Received event.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _raiseReceiveEvent =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(13, "RaiseReceiveEvent"), "Raising Received event.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _failedReadingMessage =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(14, "FailedReadingMessage"), "Could not read message.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _errorReceiving =
|
||||
LoggerMessage.Define(LogLevel.Error, new EventId(15, "ErrorReceiving"), "Error receiving message.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _endReceive =
|
||||
LoggerMessage.Define(LogLevel.Trace, new EventId(16, "EndReceive"), "Ending receive loop.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _sendingMessage =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(17, "SendingMessage"), "Sending message.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _stoppingClient =
|
||||
LoggerMessage.Define(LogLevel.Information, new EventId(18, "StoppingClient"), "Stopping client.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _exceptionThrownFromCallback =
|
||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(19, "ExceptionThrownFromCallback"), "An exception was thrown from the '{Callback}' callback.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _disposingClient =
|
||||
LoggerMessage.Define(LogLevel.Information, new EventId(20, "DisposingClient"), "Disposing client.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _abortingClient =
|
||||
LoggerMessage.Define(LogLevel.Error, new EventId(21, "AbortingClient"), "Aborting client.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _errorDuringClosedEvent =
|
||||
LoggerMessage.Define(LogLevel.Error, new EventId(22, "ErrorDuringClosedEvent"), "An exception was thrown in the handler for the Closed event.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _skippingStop =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(23, "SkippingStop"), "Skipping stop, connection is already stopped.");
|
||||
private static readonly Action<ILogger, Exception> _disposingHttpConnection =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(4, "DisposingHttpConnection"), "Disposing HttpConnection.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _skippingDispose =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(24, "SkippingDispose"), "Skipping dispose, connection is already disposed.");
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(5, "SkippingDispose"), "Skipping dispose, connection is already disposed.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, Exception> _connectionStateChanged =
|
||||
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(25, "ConnectionStateChanged"), "Connection state changed from {PreviousState} to {NewState}.");
|
||||
private static readonly Action<ILogger, Exception> _disposed =
|
||||
LoggerMessage.Define(LogLevel.Information, new EventId(6, "Disposed"), "HttpConnection Disposed.");
|
||||
|
||||
private static readonly Action<ILogger, string, Uri, Exception> _startingTransport =
|
||||
LoggerMessage.Define<string, Uri>(LogLevel.Debug, new EventId(7, "StartingTransport"), "Starting transport '{Transport}' with Url: {Url}.");
|
||||
|
||||
private static readonly Action<ILogger, Uri, Exception> _establishingConnection =
|
||||
LoggerMessage.Define<Uri>(LogLevel.Debug, new EventId(8, "EstablishingConnection"), "Establishing connection with server at '{Url}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _connectionEstablished =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(9, "Established"), "Established connection '{ConnectionId}' with the server.");
|
||||
|
||||
private static readonly Action<ILogger, Uri, Exception> _errorWithNegotiation =
|
||||
LoggerMessage.Define<Uri>(LogLevel.Error, new EventId(10, "ErrorWithNegotiation"), "Failed to start connection. Error getting negotiation response from '{Url}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _errorStartingTransport =
|
||||
LoggerMessage.Define<string>(LogLevel.Error, new EventId(11, "ErrorStartingTransport"), "Failed to start connection. Error starting transport '{Transport}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _transportNotSupported =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(26, "TransportNotSupported"), "Skipping transport {TransportName} because it is not supported by this client.");
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(12, "TransportNotSupported"), "Skipping transport {TransportName} because it is not supported by this client.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, Exception> _transportDoesNotSupportTransferFormat =
|
||||
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(27, "TransportDoesNotSupportTransferFormat"), "Skipping transport {TransportName} because it does not support the requested transfer format '{TransferFormat}'.");
|
||||
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(13, "TransportDoesNotSupportTransferFormat"), "Skipping transport {TransportName} because it does not support the requested transfer format '{TransferFormat}'.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _transportDisabledByClient =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(28, "TransportDisabledByClient"), "Skipping transport {TransportName} because it was disabled by the client.");
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(14, "TransportDisabledByClient"), "Skipping transport {TransportName} because it was disabled by the client.");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _transportFailed =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(29, "TransportFailed"), "Skipping transport {TransportName} because it failed to initialize.");
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(15, "TransportFailed"), "Skipping transport {TransportName} because it failed to initialize.");
|
||||
|
||||
private static readonly Action<ILogger, Exception> _webSocketsNotSupportedByOperatingSystem =
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(30, "WebSocketsNotSupportedByOperatingSystem"), "Skipping WebSockets because they are not supported by the operating system.");
|
||||
LoggerMessage.Define(LogLevel.Debug, new EventId(16, "WebSocketsNotSupportedByOperatingSystem"), "Skipping WebSockets because they are not supported by the operating system.");
|
||||
|
||||
public static void HttpConnectionStarting(ILogger logger)
|
||||
private static readonly Action<ILogger, Exception> _transportThrewExceptionOnStop =
|
||||
LoggerMessage.Define(LogLevel.Error, new EventId(17, "TransportThrewExceptionOnStop"), "The transport threw an exception while stopping.");
|
||||
|
||||
public static void Starting(ILogger logger)
|
||||
{
|
||||
_httpConnectionStarting(logger, null);
|
||||
_starting(logger, null);
|
||||
}
|
||||
|
||||
public static void HttpConnectionClosed(ILogger logger)
|
||||
public static void SkippingStart(ILogger logger)
|
||||
{
|
||||
_httpConnectionClosed(logger, null);
|
||||
_skippingStart(logger, null);
|
||||
}
|
||||
|
||||
public static void Started(ILogger logger)
|
||||
{
|
||||
_started(logger, null);
|
||||
}
|
||||
|
||||
public static void DisposingHttpConnection(ILogger logger)
|
||||
{
|
||||
_disposingHttpConnection(logger, null);
|
||||
}
|
||||
|
||||
public static void SkippingDispose(ILogger logger)
|
||||
{
|
||||
_skippingDispose(logger, null);
|
||||
}
|
||||
|
||||
public static void Disposed(ILogger logger)
|
||||
{
|
||||
_disposed(logger, null);
|
||||
}
|
||||
|
||||
public static void StartingTransport(ILogger logger, TransportType transportType, Uri url)
|
||||
|
|
@ -119,26 +100,16 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
}
|
||||
}
|
||||
|
||||
public static void ProcessRemainingMessages(ILogger logger)
|
||||
{
|
||||
_processRemainingMessages(logger, null);
|
||||
}
|
||||
|
||||
public static void DrainEvents(ILogger logger)
|
||||
{
|
||||
_drainEvents(logger, null);
|
||||
}
|
||||
|
||||
public static void CompleteClosed(ILogger logger)
|
||||
{
|
||||
_completeClosed(logger, null);
|
||||
}
|
||||
|
||||
public static void EstablishingConnection(ILogger logger, Uri url)
|
||||
{
|
||||
_establishingConnection(logger, url, null);
|
||||
}
|
||||
|
||||
public static void ConnectionEstablished(ILogger logger, string connectionId)
|
||||
{
|
||||
_connectionEstablished(logger, connectionId, null);
|
||||
}
|
||||
|
||||
public static void ErrorWithNegotiation(ILogger logger, Uri url, Exception exception)
|
||||
{
|
||||
_errorWithNegotiation(logger, url, exception);
|
||||
|
|
@ -152,89 +123,6 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
}
|
||||
}
|
||||
|
||||
public static void HttpReceiveStarted(ILogger logger)
|
||||
{
|
||||
_httpReceiveStarted(logger, null);
|
||||
}
|
||||
|
||||
public static void SkipRaisingReceiveEvent(ILogger logger)
|
||||
{
|
||||
_skipRaisingReceiveEvent(logger, null);
|
||||
}
|
||||
|
||||
public static void ScheduleReceiveEvent(ILogger logger)
|
||||
{
|
||||
_scheduleReceiveEvent(logger, null);
|
||||
}
|
||||
|
||||
public static void RaiseReceiveEvent(ILogger logger)
|
||||
{
|
||||
_raiseReceiveEvent(logger, null);
|
||||
}
|
||||
|
||||
public static void FailedReadingMessage(ILogger logger)
|
||||
{
|
||||
_failedReadingMessage(logger, null);
|
||||
}
|
||||
|
||||
public static void ErrorReceiving(ILogger logger, Exception exception)
|
||||
{
|
||||
_errorReceiving(logger, exception);
|
||||
}
|
||||
|
||||
public static void EndReceive(ILogger logger)
|
||||
{
|
||||
_endReceive(logger, null);
|
||||
}
|
||||
|
||||
public static void SendingMessage(ILogger logger)
|
||||
{
|
||||
_sendingMessage(logger, null);
|
||||
}
|
||||
|
||||
public static void AbortingClient(ILogger logger, Exception ex)
|
||||
{
|
||||
_abortingClient(logger, ex);
|
||||
}
|
||||
|
||||
public static void StoppingClient(ILogger logger)
|
||||
{
|
||||
_stoppingClient(logger, null);
|
||||
}
|
||||
|
||||
public static void DisposingClient(ILogger logger)
|
||||
{
|
||||
_disposingClient(logger, null);
|
||||
}
|
||||
|
||||
public static void SkippingDispose(ILogger logger)
|
||||
{
|
||||
_skippingDispose(logger, null);
|
||||
}
|
||||
|
||||
public static void ConnectionStateChanged(ILogger logger, HttpConnection.ConnectionState previousState, HttpConnection.ConnectionState newState)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_connectionStateChanged(logger, previousState.ToString(), newState.ToString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SkippingStop(ILogger logger)
|
||||
{
|
||||
_skippingStop(logger, null);
|
||||
}
|
||||
|
||||
public static void ExceptionThrownFromCallback(ILogger logger, string callbackName, Exception exception)
|
||||
{
|
||||
_exceptionThrownFromCallback(logger, callbackName, exception);
|
||||
}
|
||||
|
||||
public static void ErrorDuringClosedEvent(ILogger logger, Exception exception)
|
||||
{
|
||||
_errorDuringClosedEvent(logger, exception);
|
||||
}
|
||||
|
||||
public static void TransportNotSupported(ILogger logger, string transport)
|
||||
{
|
||||
_transportNotSupported(logger, transport, null);
|
||||
|
|
@ -268,6 +156,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
_webSocketsNotSupportedByOperatingSystem(logger, null);
|
||||
}
|
||||
|
||||
public static void TransportThrewExceptionOnStop(ILogger logger, Exception ex)
|
||||
{
|
||||
_transportThrewExceptionOnStop(logger, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
|
|
@ -11,18 +9,16 @@ using System.Net.Http;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Http.Internal;
|
||||
using Microsoft.AspNetCore.Sockets.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Http
|
||||
{
|
||||
public partial class HttpConnection : IConnection
|
||||
{
|
||||
|
|
@ -31,36 +27,40 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private static readonly Version Windows8Version = new Version(6, 2);
|
||||
#endif
|
||||
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private volatile ConnectionState _connectionState = ConnectionState.Disconnected;
|
||||
private readonly object _stateChangeLock = new object();
|
||||
private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(1, 1);
|
||||
private bool _started;
|
||||
private bool _disposed;
|
||||
|
||||
private IDuplexPipe _transportPipe;
|
||||
|
||||
private volatile IDuplexPipe _transportChannel;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly HttpOptions _httpOptions;
|
||||
private volatile ITransport _transport;
|
||||
private volatile Task _receiveLoopTask;
|
||||
private TaskCompletionSource<object> _startTcs;
|
||||
private TaskCompletionSource<object> _closeTcs;
|
||||
private TaskQueue _eventQueue;
|
||||
private ITransport _transport;
|
||||
private readonly ITransportFactory _transportFactory;
|
||||
private string _connectionId;
|
||||
private Exception _abortException;
|
||||
private readonly TimeSpan _eventQueueDrainTimeout = TimeSpan.FromSeconds(5);
|
||||
private PipeReader Input => _transportChannel.Input;
|
||||
private PipeWriter Output => _transportChannel.Output;
|
||||
private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>();
|
||||
private readonly TransportType _requestedTransportType = TransportType.All;
|
||||
private readonly ConnectionLogScope _logScope;
|
||||
private readonly IDisposable _scopeDisposable;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
public Uri Url { get; }
|
||||
|
||||
public IFeatureCollection Features { get; } = new FeatureCollection();
|
||||
public IDuplexPipe Transport
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckDisposed();
|
||||
if (_transportPipe == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot access the {nameof(Transport)} pipe before the connection has started.");
|
||||
}
|
||||
return _transportPipe;
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<Exception> Closed;
|
||||
public IFeatureCollection Features { get; } = new FeatureCollection();
|
||||
|
||||
public HttpConnection(Uri url)
|
||||
: this(url, TransportType.All)
|
||||
|
|
@ -84,8 +84,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
public HttpConnection(Uri url, TransportType transportType, ILoggerFactory loggerFactory, HttpOptions httpOptions)
|
||||
{
|
||||
Url = url ?? throw new ArgumentNullException(nameof(url));
|
||||
|
||||
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
||||
|
||||
_logger = _loggerFactory.CreateLogger<HttpConnection>();
|
||||
_httpOptions = httpOptions;
|
||||
|
||||
|
|
@ -100,6 +100,277 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
_scopeDisposable = _logger.BeginScope(_logScope);
|
||||
}
|
||||
|
||||
public HttpConnection(Uri url, ITransportFactory transportFactory, ILoggerFactory loggerFactory, HttpOptions httpOptions)
|
||||
{
|
||||
Url = url ?? throw new ArgumentNullException(nameof(url));
|
||||
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
||||
_logger = _loggerFactory.CreateLogger<HttpConnection>();
|
||||
_httpOptions = httpOptions;
|
||||
_httpClient = CreateHttpClient();
|
||||
_transportFactory = transportFactory ?? throw new ArgumentNullException(nameof(transportFactory));
|
||||
_logScope = new ConnectionLogScope();
|
||||
_scopeDisposable = _logger.BeginScope(_logScope);
|
||||
}
|
||||
|
||||
public Task StartAsync() => StartAsync(TransferFormat.Binary);
|
||||
|
||||
public async Task StartAsync(TransferFormat transferFormat)
|
||||
{
|
||||
await StartAsyncCore(transferFormat).ForceAsync();
|
||||
}
|
||||
|
||||
private async Task StartAsyncCore(TransferFormat transferFormat)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (_started)
|
||||
{
|
||||
Log.SkippingStart(_logger);
|
||||
return;
|
||||
}
|
||||
|
||||
await _connectionLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (_started)
|
||||
{
|
||||
Log.SkippingStart(_logger);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Starting(_logger);
|
||||
|
||||
await SelectAndStartTransport(transferFormat);
|
||||
|
||||
_started = true;
|
||||
Log.Started(_logger);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisposeAsync() => await DisposeAsyncCore().ForceAsync();
|
||||
|
||||
private async Task DisposeAsyncCore(Exception exception = null)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _connectionLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!_disposed && _started)
|
||||
{
|
||||
Log.DisposingHttpConnection(_logger);
|
||||
|
||||
// Complete our ends of the pipes.
|
||||
_transportPipe.Input.Complete(exception);
|
||||
_transportPipe.Output.Complete(exception);
|
||||
|
||||
// Stop the transport, but we don't care if it throws.
|
||||
// The transport should also have completed the pipe with this exception.
|
||||
try
|
||||
{
|
||||
await _transport.StopAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.TransportThrewExceptionOnStop(_logger, ex);
|
||||
}
|
||||
|
||||
Log.Disposed(_logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.SkippingDispose(_logger);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// We want to do these things even if the WaitForWriterToComplete/WaitForReaderToComplete fails
|
||||
if (!_disposed)
|
||||
{
|
||||
_scopeDisposable.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
_connectionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectAndStartTransport(TransferFormat transferFormat)
|
||||
{
|
||||
if (_requestedTransportType == TransportType.WebSockets)
|
||||
{
|
||||
Log.StartingTransport(_logger, _requestedTransportType, Url);
|
||||
await StartTransport(Url, _requestedTransportType, transferFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
var negotiationResponse = await GetNegotiationResponse();
|
||||
|
||||
// This should only need to happen once
|
||||
var connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
|
||||
|
||||
// We're going to search for the transfer format as a string because we don't want to parse
|
||||
// all the transfer formats in the negotiation response, and we want to allow transfer formats
|
||||
// we don't understand in the negotiate response.
|
||||
var transferFormatString = transferFormat.ToString();
|
||||
|
||||
foreach (var transport in negotiationResponse.AvailableTransports)
|
||||
{
|
||||
if (!Enum.TryParse<TransportType>(transport.Transport, out var transportType))
|
||||
{
|
||||
Log.TransportNotSupported(_logger, transport.Transport);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (transportType == TransportType.WebSockets && !IsWebSocketsSupported())
|
||||
{
|
||||
Log.WebSocketsNotSupportedByOperatingSystem(_logger);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if ((transportType & _requestedTransportType) == 0)
|
||||
{
|
||||
Log.TransportDisabledByClient(_logger, transportType);
|
||||
}
|
||||
else if (!transport.TransferFormats.Contains(transferFormatString, StringComparer.Ordinal))
|
||||
{
|
||||
Log.TransportDoesNotSupportTransferFormat(_logger, transportType, transferFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The negotiation response gets cleared in the fallback scenario.
|
||||
if (negotiationResponse == null)
|
||||
{
|
||||
negotiationResponse = await GetNegotiationResponse();
|
||||
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
|
||||
}
|
||||
|
||||
Log.StartingTransport(_logger, transportType, connectUrl);
|
||||
await StartTransport(connectUrl, transportType, transferFormat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.TransportFailed(_logger, transportType, ex);
|
||||
// Try the next transport
|
||||
// Clear the negotiation response so we know to re-negotiate.
|
||||
negotiationResponse = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_transport == null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to connect to the server with any of the available transports.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NegotiationResponse> Negotiate(Uri url, HttpClient httpClient, ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get a connection ID from the server
|
||||
Log.EstablishingConnection(logger, url);
|
||||
var urlBuilder = new UriBuilder(url);
|
||||
if (!urlBuilder.Path.EndsWith("/"))
|
||||
{
|
||||
urlBuilder.Path += "/";
|
||||
}
|
||||
urlBuilder.Path += "negotiate";
|
||||
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, urlBuilder.Uri))
|
||||
{
|
||||
// Corefx changed the default version and High Sierra curlhandler tries to upgrade request
|
||||
request.Version = new Version(1, 1);
|
||||
SendUtils.PrepareHttpRequest(request, _httpOptions);
|
||||
|
||||
using (var response = await httpClient.SendAsync(request))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
var negotiateResponse = await ParseNegotiateResponse(response);
|
||||
Log.ConnectionEstablished(_logger, negotiateResponse.ConnectionId);
|
||||
return negotiateResponse;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ErrorWithNegotiation(logger, url, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<NegotiationResponse> ParseNegotiateResponse(HttpResponseMessage response)
|
||||
{
|
||||
NegotiationResponse negotiationResponse;
|
||||
using (var reader = new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync())))
|
||||
{
|
||||
try
|
||||
{
|
||||
negotiationResponse = new JsonSerializer().Deserialize<NegotiationResponse>(reader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException("Invalid negotiation response received.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (negotiationResponse == null)
|
||||
{
|
||||
throw new FormatException("Invalid negotiation response received.");
|
||||
}
|
||||
|
||||
return negotiationResponse;
|
||||
}
|
||||
|
||||
private static Uri CreateConnectUrl(Uri url, string connectionId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionId))
|
||||
{
|
||||
throw new FormatException("Invalid connection id.");
|
||||
}
|
||||
|
||||
return Utils.AppendQueryString(url, "id=" + connectionId);
|
||||
}
|
||||
|
||||
private async Task StartTransport(Uri connectUrl, TransportType transportType, TransferFormat transferFormat)
|
||||
{
|
||||
// Create the pipe pair (Application's writer is connected to Transport's reader, and vice versa)
|
||||
var options = new PipeOptions(writerScheduler: PipeScheduler.ThreadPool, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
|
||||
var pair = DuplexPipe.CreateConnectionPair(options, options);
|
||||
|
||||
// Construct the transport
|
||||
var transport = _transportFactory.CreateTransport(transportType);
|
||||
|
||||
// Start the transport, giving it one end of the pipe
|
||||
try
|
||||
{
|
||||
await transport.StartAsync(connectUrl, pair.Application, transferFormat, this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ErrorStartingTransport(_logger, _transport, ex);
|
||||
_transport = null;
|
||||
throw;
|
||||
}
|
||||
|
||||
// We successfully started, set the transport properties (we don't want to set these until the transport is definitely running).
|
||||
_transport = transport;
|
||||
_transportPipe = pair.Transport;
|
||||
}
|
||||
|
||||
private HttpClient CreateHttpClient()
|
||||
{
|
||||
var httpClientHandler = new HttpClientHandler();
|
||||
|
|
@ -148,582 +419,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
return httpClient;
|
||||
}
|
||||
|
||||
public HttpConnection(Uri url, ITransportFactory transportFactory, ILoggerFactory loggerFactory, HttpOptions httpOptions)
|
||||
private void CheckDisposed()
|
||||
{
|
||||
Url = url ?? throw new ArgumentNullException(nameof(url));
|
||||
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
||||
_logger = _loggerFactory.CreateLogger<HttpConnection>();
|
||||
_httpOptions = httpOptions;
|
||||
_httpClient = CreateHttpClient();
|
||||
_transportFactory = transportFactory ?? throw new ArgumentNullException(nameof(transportFactory));
|
||||
_logScope = new ConnectionLogScope();
|
||||
_scopeDisposable = _logger.BeginScope(_logScope);
|
||||
}
|
||||
|
||||
public Task StartAsync() => StartAsync(TransferFormat.Binary);
|
||||
public async Task StartAsync(TransferFormat transferFormat) => await StartAsyncCore(transferFormat).ForceAsync();
|
||||
|
||||
private Task StartAsyncCore(TransferFormat transferFormat)
|
||||
{
|
||||
if (ChangeState(from: ConnectionState.Disconnected, to: ConnectionState.Connecting) != ConnectionState.Disconnected)
|
||||
if (_disposed)
|
||||
{
|
||||
return Task.FromException(
|
||||
new InvalidOperationException($"Cannot start a connection that is not in the {nameof(ConnectionState.Disconnected)} state."));
|
||||
}
|
||||
|
||||
_startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_eventQueue = new TaskQueue();
|
||||
|
||||
StartAsyncInternal(transferFormat)
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
var abortException = _abortException;
|
||||
if (t.IsFaulted || abortException != null)
|
||||
{
|
||||
_startTcs.SetException(_abortException ?? t.Exception.InnerException);
|
||||
}
|
||||
else if (t.IsCanceled)
|
||||
{
|
||||
_startTcs.SetCanceled();
|
||||
}
|
||||
else
|
||||
{
|
||||
_startTcs.SetResult(null);
|
||||
}
|
||||
});
|
||||
|
||||
return _startTcs.Task;
|
||||
}
|
||||
|
||||
private async Task<NegotiationResponse> GetNegotiationResponse()
|
||||
{
|
||||
var negotiationResponse = await Negotiate(Url, _httpClient, _logger);
|
||||
_connectionId = negotiationResponse.ConnectionId;
|
||||
_logScope.ConnectionId = _connectionId;
|
||||
return negotiationResponse;
|
||||
}
|
||||
|
||||
private async Task StartAsyncInternal(TransferFormat transferFormat)
|
||||
{
|
||||
Log.HttpConnectionStarting(_logger);
|
||||
|
||||
try
|
||||
{
|
||||
var connectUrl = Url;
|
||||
if (_requestedTransportType == TransportType.WebSockets)
|
||||
{
|
||||
// if we're running on Windows 7 this could throw because the OS does not support web sockets
|
||||
Log.StartingTransport(_logger, _requestedTransportType, connectUrl);
|
||||
await StartTransport(connectUrl, _requestedTransportType, transferFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
var negotiationResponse = await GetNegotiationResponse();
|
||||
|
||||
// Connection is being disposed while start was in progress
|
||||
if (_connectionState == ConnectionState.Disposed)
|
||||
{
|
||||
Log.HttpConnectionClosed(_logger);
|
||||
return;
|
||||
}
|
||||
|
||||
// This should only need to happen once
|
||||
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
|
||||
|
||||
// We're going to search for the transfer format as a string because we don't want to parse
|
||||
// all the transfer formats in the negotiation response, and we want to allow transfer formats
|
||||
// we don't understand in the negotiate response.
|
||||
var transferFormatString = transferFormat.ToString();
|
||||
|
||||
foreach (var transport in negotiationResponse.AvailableTransports)
|
||||
{
|
||||
if (!Enum.TryParse<TransportType>(transport.Transport, out var transportType))
|
||||
{
|
||||
Log.TransportNotSupported(_logger, transport.Transport);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (transportType == TransportType.WebSockets && !IsWebSocketsSupported())
|
||||
{
|
||||
Log.WebSocketsNotSupportedByOperatingSystem(_logger);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if ((transportType & _requestedTransportType) == 0)
|
||||
{
|
||||
Log.TransportDisabledByClient(_logger, transportType);
|
||||
}
|
||||
else if (!transport.TransferFormats.Contains(transferFormatString, StringComparer.Ordinal))
|
||||
{
|
||||
Log.TransportDoesNotSupportTransferFormat(_logger, transportType, transferFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The negotiation response gets cleared in the fallback scenario.
|
||||
if (negotiationResponse == null)
|
||||
{
|
||||
negotiationResponse = await GetNegotiationResponse();
|
||||
connectUrl = CreateConnectUrl(Url, negotiationResponse.ConnectionId);
|
||||
}
|
||||
|
||||
Log.StartingTransport(_logger, transportType, connectUrl);
|
||||
await StartTransport(connectUrl, transportType, transferFormat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.TransportFailed(_logger, transportType, ex);
|
||||
// Try the next transport
|
||||
// Clear the negotiation response so we know to re-negotiate.
|
||||
negotiationResponse = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_transport == null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to connect to the server with any of the available transports.");
|
||||
}
|
||||
}
|
||||
|
||||
catch
|
||||
{
|
||||
// The connection can now be either in the Connecting or Disposed state - only change the state to
|
||||
// Disconnected if the connection was in the Connecting state to not resurrect a Disposed connection
|
||||
ChangeState(from: ConnectionState.Connecting, to: ConnectionState.Disconnected);
|
||||
throw;
|
||||
}
|
||||
|
||||
// if the connection is not in the Connecting state here it means the user called DisposeAsync while
|
||||
// the connection was starting
|
||||
if (ChangeState(from: ConnectionState.Connecting, to: ConnectionState.Connected) == ConnectionState.Connecting)
|
||||
{
|
||||
_closeTcs = new TaskCompletionSource<object>();
|
||||
|
||||
Input.OnWriterCompleted(async (exception, state) =>
|
||||
{
|
||||
// Grab the exception and then clear it.
|
||||
// See comment at AbortAsync for more discussion on the thread-safety
|
||||
// StartAsync can't be called until the ChangeState below, so we're OK.
|
||||
var abortException = _abortException;
|
||||
_abortException = null;
|
||||
|
||||
// There is an inherent race between receive and close. Removing the last message from the channel
|
||||
// makes Input.Completion task completed and runs this continuation. We need to await _receiveLoopTask
|
||||
// to make sure that the message removed from the channel is processed before we drain the queue.
|
||||
// There is a short window between we start the channel and assign the _receiveLoopTask a value.
|
||||
// To make sure that _receiveLoopTask can be awaited (i.e. is not null) we need to await _startTask.
|
||||
Log.ProcessRemainingMessages(_logger);
|
||||
|
||||
await _startTcs.Task;
|
||||
await _receiveLoopTask;
|
||||
|
||||
Log.DrainEvents(_logger);
|
||||
|
||||
await Task.WhenAny(_eventQueue.Drain().NoThrow(), Task.Delay(_eventQueueDrainTimeout));
|
||||
|
||||
Log.CompleteClosed(_logger);
|
||||
_logScope.ConnectionId = null;
|
||||
|
||||
// At this point the connection can be either in the Connected or Disposed state. The state should be changed
|
||||
// to the Disconnected state only if it was in the Connected state.
|
||||
// From this point on, StartAsync can be called at any time.
|
||||
ChangeState(from: ConnectionState.Connected, to: ConnectionState.Disconnected);
|
||||
|
||||
_closeTcs.SetResult(null);
|
||||
|
||||
try
|
||||
{
|
||||
if (exception != null)
|
||||
{
|
||||
Closed?.Invoke(exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Call the closed event. If there was an abort exception, it will be flowed forward
|
||||
// However, if there wasn't, this will just be null and we're good
|
||||
Closed?.Invoke(abortException);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Suppress (but log) the exception, this is user code
|
||||
Log.ErrorDuringClosedEvent(_logger, ex);
|
||||
}
|
||||
|
||||
}, null);
|
||||
|
||||
_receiveLoopTask = ReceiveAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<NegotiationResponse> Negotiate(Uri url, HttpClient httpClient, ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get a connection ID from the server
|
||||
Log.EstablishingConnection(logger, url);
|
||||
var urlBuilder = new UriBuilder(url);
|
||||
if (!urlBuilder.Path.EndsWith("/"))
|
||||
{
|
||||
urlBuilder.Path += "/";
|
||||
}
|
||||
urlBuilder.Path += "negotiate";
|
||||
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, urlBuilder.Uri))
|
||||
{
|
||||
// Corefx changed the default version and High Sierra curlhandler tries to upgrade request
|
||||
request.Version = new Version(1, 1);
|
||||
SendUtils.PrepareHttpRequest(request, _httpOptions);
|
||||
|
||||
using (var response = await httpClient.SendAsync(request))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await ParseNegotiateResponse(response, logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ErrorWithNegotiation(logger, url, ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<NegotiationResponse> ParseNegotiateResponse(HttpResponseMessage response, ILogger logger)
|
||||
{
|
||||
NegotiationResponse negotiationResponse;
|
||||
using (var reader = new JsonTextReader(new StreamReader(await response.Content.ReadAsStreamAsync())))
|
||||
{
|
||||
try
|
||||
{
|
||||
negotiationResponse = new JsonSerializer().Deserialize<NegotiationResponse>(reader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new FormatException("Invalid negotiation response received.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (negotiationResponse == null)
|
||||
{
|
||||
throw new FormatException("Invalid negotiation response received.");
|
||||
}
|
||||
|
||||
return negotiationResponse;
|
||||
}
|
||||
|
||||
private static Uri CreateConnectUrl(Uri url, string connectionId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionId))
|
||||
{
|
||||
throw new FormatException("Invalid connection id.");
|
||||
}
|
||||
|
||||
return Utils.AppendQueryString(url, "id=" + connectionId);
|
||||
}
|
||||
|
||||
private async Task StartTransport(Uri connectUrl, TransportType transportType, TransferFormat transferFormat)
|
||||
{
|
||||
var options = new PipeOptions(writerScheduler: PipeScheduler.Inline, readerScheduler: PipeScheduler.ThreadPool, useSynchronizationContext: false);
|
||||
var pair = DuplexPipe.CreateConnectionPair(options, options);
|
||||
_transportChannel = pair.Transport;
|
||||
_transport = _transportFactory.CreateTransport(transportType);
|
||||
|
||||
// Start the transport, giving it one end of the pipeline
|
||||
try
|
||||
{
|
||||
await _transport.StartAsync(connectUrl, pair.Application, transferFormat, this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ErrorStartingTransport(_logger, _transport, ex);
|
||||
_transport = null;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.HttpReceiveStarted(_logger);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (_connectionState != ConnectionState.Connected)
|
||||
{
|
||||
Log.SkipRaisingReceiveEvent(_logger);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var result = await Input.ReadAsync();
|
||||
var buffer = result.Buffer;
|
||||
|
||||
try
|
||||
{
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
Log.ScheduleReceiveEvent(_logger);
|
||||
var data = buffer.ToArray();
|
||||
|
||||
_ = _eventQueue.Enqueue(async () =>
|
||||
{
|
||||
Log.RaiseReceiveEvent(_logger);
|
||||
|
||||
// Copying the callbacks to avoid concurrency issues
|
||||
ReceiveCallback[] callbackCopies;
|
||||
lock (_callbacks)
|
||||
{
|
||||
callbackCopies = new ReceiveCallback[_callbacks.Count];
|
||||
_callbacks.CopyTo(callbackCopies);
|
||||
}
|
||||
|
||||
foreach (var callbackObject in callbackCopies)
|
||||
{
|
||||
try
|
||||
{
|
||||
await callbackObject.InvokeAsync(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ExceptionThrownFromCallback(_logger, nameof(OnReceived), ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
else if (result.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Input.AdvanceTo(buffer.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Input.Complete(ex);
|
||||
|
||||
Log.ErrorReceiving(_logger, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Input.Complete();
|
||||
}
|
||||
|
||||
Log.EndReceive(_logger);
|
||||
}
|
||||
|
||||
public async Task SendAsync(byte[] data, CancellationToken cancellationToken = default) =>
|
||||
await SendAsyncCore(data, cancellationToken).ForceAsync();
|
||||
|
||||
private async Task SendAsyncCore(byte[] data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
if (_connectionState != ConnectionState.Connected)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot send messages when the connection is not in the Connected state.");
|
||||
}
|
||||
|
||||
Log.SendingMessage(_logger);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Output.WriteAsync(data);
|
||||
}
|
||||
|
||||
// AbortAsync creates a few thread-safety races that we are OK with.
|
||||
// 1. If the transport shuts down gracefully after AbortAsync is called but BEFORE _abortException is called, then the
|
||||
// Closed event will not receive the Abort exception. This is OK because technically the transport was shut down gracefully
|
||||
// before it was aborted
|
||||
// 2. If the transport is closed gracefully and then AbortAsync is called before it captures the _abortException value
|
||||
// the graceful shutdown could be turned into an abort. However, again, this is an inherent race between two different conditions:
|
||||
// The transport shutting down because the server went away, and the user requesting the Abort
|
||||
// 3. Finally, because this is an instance field, there is a possible race around accidentally re-using _abortException in the restarted
|
||||
// connection. The scenario here is: AbortAsync(someException); StartAsync(); CloseAsync(); Where the _abortException value from the
|
||||
// first AbortAsync call is still set at the time CloseAsync gets to calling the Closed event. However, this can't happen because the
|
||||
// StartAsync method can't be called until the connection state is changed to Disconnected, which happens AFTER the close code
|
||||
// captures and resets _abortException.
|
||||
public async Task AbortAsync(Exception exception) => await StopAsyncCore(exception ?? throw new ArgumentNullException(nameof(exception))).ForceAsync();
|
||||
|
||||
public async Task StopAsync() => await StopAsyncCore(exception: null).ForceAsync();
|
||||
|
||||
private async Task StopAsyncCore(Exception exception)
|
||||
{
|
||||
lock (_stateChangeLock)
|
||||
{
|
||||
if (!(_connectionState == ConnectionState.Connecting || _connectionState == ConnectionState.Connected))
|
||||
{
|
||||
Log.SkippingStop(_logger);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Note that this method can be called at the same time when the connection is being closed from the server
|
||||
// side due to an error. We are resilient to this since we merely try to close the channel here and the
|
||||
// channel can be closed only once. As a result the continuation that does actual job and raises the Closed
|
||||
// event runs always only once.
|
||||
|
||||
// See comment at AbortAsync for more discussion on the thread-safety of this.
|
||||
_abortException = exception;
|
||||
|
||||
Log.StoppingClient(_logger);
|
||||
|
||||
try
|
||||
{
|
||||
await _startTcs.Task;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We only await the start task to make sure that StartAsync completed. The
|
||||
// _startTask is returned to the user and they should handle exceptions.
|
||||
}
|
||||
|
||||
TaskCompletionSource<object> closeTcs = null;
|
||||
Task receiveLoopTask = null;
|
||||
ITransport transport = null;
|
||||
|
||||
lock (_stateChangeLock)
|
||||
{
|
||||
// Copy locals in lock to prevent a race when the server closes the connection and StopAsync is called
|
||||
// at the same time
|
||||
if (_connectionState != ConnectionState.Connected)
|
||||
{
|
||||
// If not Connected then someone else disconnected while StopAsync was in progress, we can now NO-OP
|
||||
return;
|
||||
}
|
||||
|
||||
// Create locals of relevant member variables to prevent a race when Closed event triggers a connect
|
||||
// while StopAsync is still running
|
||||
closeTcs = _closeTcs;
|
||||
receiveLoopTask = _receiveLoopTask;
|
||||
transport = _transport;
|
||||
}
|
||||
|
||||
if (_transportChannel != null)
|
||||
{
|
||||
Output.Complete();
|
||||
}
|
||||
|
||||
if (transport != null)
|
||||
{
|
||||
await transport.StopAsync();
|
||||
}
|
||||
|
||||
if (receiveLoopTask != null)
|
||||
{
|
||||
await receiveLoopTask;
|
||||
}
|
||||
|
||||
if (closeTcs != null)
|
||||
{
|
||||
await closeTcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisposeAsync() => await DisposeAsyncCore().ForceAsync();
|
||||
|
||||
private async Task DisposeAsyncCore()
|
||||
{
|
||||
// This will no-op if we're already stopped
|
||||
await StopAsyncCore(exception: null);
|
||||
|
||||
if (ChangeState(to: ConnectionState.Disposed) == ConnectionState.Disposed)
|
||||
{
|
||||
Log.SkippingDispose(_logger);
|
||||
|
||||
// the connection was already disposed
|
||||
return;
|
||||
}
|
||||
|
||||
Log.DisposingClient(_logger);
|
||||
|
||||
_httpClient?.Dispose();
|
||||
_scopeDisposable.Dispose();
|
||||
}
|
||||
|
||||
public IDisposable OnReceived(Func<byte[], object, Task> callback, object state)
|
||||
{
|
||||
var receiveCallback = new ReceiveCallback(callback, state);
|
||||
lock (_callbacks)
|
||||
{
|
||||
_callbacks.Add(receiveCallback);
|
||||
}
|
||||
return new Subscription(receiveCallback, _callbacks);
|
||||
}
|
||||
|
||||
private class ReceiveCallback
|
||||
{
|
||||
private readonly Func<byte[], object, Task> _callback;
|
||||
private readonly object _state;
|
||||
|
||||
public ReceiveCallback(Func<byte[], object, Task> callback, object state)
|
||||
{
|
||||
_callback = callback;
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public Task InvokeAsync(byte[] data)
|
||||
{
|
||||
return _callback(data, _state);
|
||||
}
|
||||
}
|
||||
|
||||
private class Subscription : IDisposable
|
||||
{
|
||||
private readonly ReceiveCallback _receiveCallback;
|
||||
private readonly List<ReceiveCallback> _callbacks;
|
||||
public Subscription(ReceiveCallback callback, List<ReceiveCallback> callbacks)
|
||||
{
|
||||
_receiveCallback = callback;
|
||||
_callbacks = callbacks;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_callbacks)
|
||||
{
|
||||
_callbacks.Remove(_receiveCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionState ChangeState(ConnectionState from, ConnectionState to)
|
||||
{
|
||||
lock (_stateChangeLock)
|
||||
{
|
||||
var state = _connectionState;
|
||||
if (_connectionState == from)
|
||||
{
|
||||
_connectionState = to;
|
||||
}
|
||||
|
||||
Log.ConnectionStateChanged(_logger, state, to);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionState ChangeState(ConnectionState to)
|
||||
{
|
||||
lock (_stateChangeLock)
|
||||
{
|
||||
var state = _connectionState;
|
||||
_connectionState = to;
|
||||
Log.ConnectionStateChanged(_logger, state, to);
|
||||
return state;
|
||||
throw new ObjectDisposedException(nameof(HttpConnection));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -747,13 +447,12 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
#endif
|
||||
}
|
||||
|
||||
// Internal because it's used by logging to avoid ToStringing prematurely.
|
||||
internal enum ConnectionState
|
||||
private async Task<NegotiationResponse> GetNegotiationResponse()
|
||||
{
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disposed
|
||||
var negotiationResponse = await Negotiate(Url, _httpClient, _logger);
|
||||
_connectionId = negotiationResponse.ConnectionId;
|
||||
_logScope.ConnectionId = _connectionId;
|
||||
return negotiationResponse;
|
||||
}
|
||||
|
||||
private class NegotiationResponse
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
public static partial class HttpConnectionExtensions
|
||||
{
|
||||
public static IDisposable OnReceived(this HttpConnection connection, Func<byte[], Task> callback)
|
||||
{
|
||||
return connection.OnReceived((data, state) =>
|
||||
{
|
||||
var currentCallback = (Func<byte[], Task>)state;
|
||||
return currentCallback(data);
|
||||
}, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Http
|
|||
|
||||
/// <summary>
|
||||
/// Gets or sets a delegate that will be invoked with the <see cref="ClientWebSocketOptions"/> object used
|
||||
/// by the <see cref="WebSocketsTransport"/> to configure the WebSocket.
|
||||
/// to configure the WebSocket when using the WebSockets transport.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This delegate is invoked after headers from <see cref="Headers"/> and the access token from <see cref="AccessTokenFactory"/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||
{
|
||||
public partial class LongPollingTransport
|
||||
{
|
||||
|
|
@ -38,6 +39,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
private static readonly Action<ILogger, Uri, Exception> _errorPolling =
|
||||
LoggerMessage.Define<Uri>(LogLevel.Error, new EventId(9, "ErrorPolling"), "Error while polling '{PollUrl}'.");
|
||||
|
||||
// long? does properly format as "(null)" when null.
|
||||
private static readonly Action<ILogger, int, long?, Exception> _pollResponseReceived =
|
||||
LoggerMessage.Define<int, long?>(LogLevel.Trace, new EventId(10, "PollResponseReceived"),
|
||||
"Poll response with status code {StatusCode} received from server. Content length: {ContentLength}.");
|
||||
|
||||
// EventIds 100 - 106 used in SendUtils
|
||||
|
||||
public static void StartTransport(ILogger logger, TransferFormat transferFormat)
|
||||
|
|
@ -84,6 +90,15 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
_errorPolling(logger, pollUrl, exception);
|
||||
}
|
||||
|
||||
public static void PollResponseReceived(ILogger logger, HttpResponseMessage response)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
_pollResponseReceived(logger, (int)response.StatusCode,
|
||||
response.Content.Headers.ContentLength ?? -1, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
|
|||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||
{
|
||||
public partial class LongPollingTransport : ITransport
|
||||
{
|
||||
|
|
@ -107,6 +107,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
continue;
|
||||
}
|
||||
|
||||
Log.PollResponseReceived(_logger, response);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NoContent || cancellationToken.IsCancellationRequested)
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||
{
|
||||
public partial class ServerSentEventsTransport
|
||||
{
|
||||
|
|
@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Sockets.Internal.Formatters;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||
{
|
||||
public partial class ServerSentEventsTransport : ITransport
|
||||
{
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||
{
|
||||
// Allows serial queuing of Task instances
|
||||
// The tasks are not called on the current synchronization context
|
||||
|
||||
public sealed class TaskQueue
|
||||
{
|
||||
private readonly object _lockObj = new object();
|
||||
private Task _lastQueuedTask;
|
||||
private volatile bool _drained;
|
||||
|
||||
public TaskQueue()
|
||||
: this(Task.CompletedTask)
|
||||
{ }
|
||||
|
||||
public TaskQueue(Task initialTask)
|
||||
{
|
||||
_lastQueuedTask = initialTask;
|
||||
}
|
||||
|
||||
public bool IsDrained
|
||||
{
|
||||
get { return _drained; }
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<Task> taskFunc)
|
||||
{
|
||||
return Enqueue(s => taskFunc(), null);
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<object, Task> taskFunc, object state)
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
if (_drained)
|
||||
{
|
||||
return _lastQueuedTask;
|
||||
}
|
||||
|
||||
var newTask = _lastQueuedTask.ContinueWith((t, s1) =>
|
||||
{
|
||||
if (t.IsFaulted || t.IsCanceled)
|
||||
{
|
||||
return t;
|
||||
}
|
||||
|
||||
return taskFunc(s1) ?? Task.CompletedTask;
|
||||
},
|
||||
state).Unwrap();
|
||||
_lastQueuedTask = newTask;
|
||||
return newTask;
|
||||
}
|
||||
}
|
||||
|
||||
public Task Drain()
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
_drained = true;
|
||||
|
||||
return _lastQueuedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ using System.Net.WebSockets;
|
|||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||
{
|
||||
public partial class WebSocketsTransport
|
||||
{
|
||||
|
|
@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Sockets.Client.Http;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||
{
|
||||
public partial class WebSocketsTransport : ITransport
|
||||
{
|
||||
|
|
@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
|||
{
|
||||
using (socket)
|
||||
{
|
||||
// Begin sending and receiving. Receiving must be started first because ExecuteAsync enables SendAsync.
|
||||
// Begin sending and receiving.
|
||||
var receiving = StartReceiving(socket);
|
||||
var sending = StartSending(socket);
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
|||
using Microsoft.AspNetCore.SignalR.Tests;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
|
|
@ -43,8 +44,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task CheckFixedMessage(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task CheckFixedMessage(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(CheckFixedMessage)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
var connection = new HubConnectionBuilder()
|
||||
|
|
@ -64,7 +66,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -76,13 +78,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task CanSendAndReceiveMessage(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task CanSendAndReceiveMessage(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(CanSendAndReceiveMessage)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
const string originalMessage = "SignalR";
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, path, transportType), protocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -93,7 +95,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -105,13 +107,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task CanStopAndStartConnection(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task CanStopAndStartConnection(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, LogLevel.Trace, $"{nameof(CanStopAndStartConnection)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
const string originalMessage = "SignalR";
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, path, transportType), protocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -124,7 +126,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -134,15 +136,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanStartConnectionFromClosedEvent()
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task CanStartConnectionFromClosedEvent(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, LogLevel.Trace, $"{nameof(CanStartConnectionFromClosedEvent)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger<HubConnectionTests>();
|
||||
const string originalMessage = "SignalR";
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + "/default"), loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, new JsonHubProtocol(), loggerFactory);
|
||||
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, "/default", transportType), new JsonHubProtocol(), loggerFactory);
|
||||
var restartTcs = new TaskCompletionSource<object>();
|
||||
connection.Closed += async e =>
|
||||
{
|
||||
|
|
@ -175,7 +179,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -185,16 +189,21 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
private Func<IConnection> GetHttpConnectionFactory(ILoggerFactory loggerFactory, string path, TransportType transportType)
|
||||
{
|
||||
return () => new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task MethodsAreCaseInsensitive(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task MethodsAreCaseInsensitive(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(MethodsAreCaseInsensitive)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
const string originalMessage = "SignalR";
|
||||
var uriString = "http://test/" + path;
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, path, transportType), protocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -205,7 +214,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -217,14 +226,14 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task CanInvokeClientMethodFromServer(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task CanInvokeClientMethodFromServer(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, LogLevel.Trace, $"{nameof(CanInvokeClientMethodFromServer)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
const string originalMessage = "SignalR";
|
||||
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, path, transportType), protocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -238,7 +247,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -250,12 +259,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task InvokeNonExistantClientMethodFromServer(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task InvokeNonExistantClientMethodFromServer(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, LogLevel.Trace, $"{nameof(InvokeNonExistantClientMethodFromServer)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, path, transportType), protocol, loggerFactory);
|
||||
var closeTcs = new TaskCompletionSource<object>();
|
||||
connection.Closed += e =>
|
||||
{
|
||||
|
|
@ -290,12 +299,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task CanStreamClientMethodFromServer(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task CanStreamClientMethodFromServer(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, LogLevel.Trace, $"{nameof(CanStreamClientMethodFromServer)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, path, transportType), protocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -307,7 +316,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -319,12 +328,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task CanCloseStreamMethodEarly(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task CanCloseStreamMethodEarly(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(CanCloseStreamMethodEarly)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, path, transportType), protocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -333,16 +342,21 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
var channel = await connection.StreamAsChannelAsync<int>("Stream", 1000, cts.Token).OrTimeout();
|
||||
|
||||
// Wait for the server to start streaming items
|
||||
await channel.WaitToReadAsync().AsTask().OrTimeout();
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
var results = await channel.ReadAllAsync().OrTimeout();
|
||||
var results = await channel.ReadAllAsync(suppressExceptions: true).OrTimeout();
|
||||
|
||||
Assert.True(results.Count > 0 && results.Count < 1000);
|
||||
|
||||
// We should have been canceled.
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => channel.Completion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -354,12 +368,15 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task StreamDoesNotStartIfTokenAlreadyCanceled(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task StreamDoesNotStartIfTokenAlreadyCanceled(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
using (StartLog(out var loggerFactory, $"{nameof(StreamDoesNotStartIfTokenAlreadyCanceled)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, LogLevel.Trace, $"{nameof(StreamDoesNotStartIfTokenAlreadyCanceled)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection =
|
||||
new HubConnection(
|
||||
GetHttpConnectionFactory(loggerFactory, path, transportType), protocol,
|
||||
loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -369,11 +386,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
var channel = await connection.StreamAsChannelAsync<int>("Stream", 5, cts.Token).OrTimeout();
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => channel.WaitToReadAsync().AsTask().OrTimeout());
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
||||
channel.WaitToReadAsync().AsTask().OrTimeout());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -385,12 +403,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ExceptionFromStreamingSentToClient(IHubProtocol protocol, TransportType transportType, string path)
|
||||
public async Task ExceptionFromStreamingSentToClient(string protocolName, TransportType transportType, string path)
|
||||
{
|
||||
var protocol = HubProtocols[protocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ExceptionFromStreamingSentToClient)}_{protocol.Name}_{transportType}_{path.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, path, transportType), protocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -401,7 +419,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -413,12 +431,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionIfHubMethodCannotBeResolved(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionIfHubMethodCannotBeResolved(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionIfHubMethodCannotBeResolved)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -428,7 +446,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -440,12 +458,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionOnHubMethodArgumentCountMismatch(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionOnHubMethodArgumentCountMismatch(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionOnHubMethodArgumentCountMismatch)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -455,7 +473,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -467,12 +485,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionOnHubMethodArgumentTypeMismatch(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionOnHubMethodArgumentTypeMismatch(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionOnHubMethodArgumentTypeMismatch)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -482,7 +500,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -494,12 +512,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionIfStreamingHubMethodCannotBeResolved(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionIfStreamingHubMethodCannotBeResolved(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionIfStreamingHubMethodCannotBeResolved)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -510,7 +528,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -522,13 +540,13 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionOnStreamingHubMethodArgumentCountMismatch(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionOnStreamingHubMethodArgumentCountMismatch(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionOnStreamingHubMethodArgumentCountMismatch)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
loggerFactory.AddConsole(LogLevel.Trace);
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -539,7 +557,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -551,12 +569,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionOnStreamingHubMethodArgumentTypeMismatch(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionOnStreamingHubMethodArgumentTypeMismatch(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionOnStreamingHubMethodArgumentTypeMismatch)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -567,7 +585,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -579,12 +597,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionIfNonStreamMethodInvokedWithStreamAsync(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionIfNonStreamMethodInvokedWithStreamAsync(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionIfNonStreamMethodInvokedWithStreamAsync)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -594,7 +612,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -606,12 +624,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionIfStreamMethodInvokedWithInvoke(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionIfStreamMethodInvokedWithInvoke(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionIfStreamMethodInvokedWithInvoke)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -621,7 +639,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -633,12 +651,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||
public async Task ServerThrowsHubExceptionIfBuildingAsyncEnumeratorIsNotPossible(IHubProtocol hubProtocol, TransportType transportType, string hubPath)
|
||||
public async Task ServerThrowsHubExceptionIfBuildingAsyncEnumeratorIsNotPossible(string hubProtocolName, TransportType transportType, string hubPath)
|
||||
{
|
||||
var hubProtocol = HubProtocols[hubProtocolName];
|
||||
using (StartLog(out var loggerFactory, $"{nameof(ServerThrowsHubExceptionIfBuildingAsyncEnumeratorIsNotPossible)}_{hubProtocol.Name}_{transportType}_{hubPath.TrimStart('/')}"))
|
||||
{
|
||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + hubPath), transportType, loggerFactory);
|
||||
var connection = new HubConnection(httpConnection, hubProtocol, loggerFactory);
|
||||
var connection = new HubConnection(GetHttpConnectionFactory(loggerFactory, hubPath, transportType), hubProtocol, loggerFactory);
|
||||
try
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
|
@ -648,7 +666,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -682,7 +700,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -713,7 +731,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -747,7 +765,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -785,7 +803,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -814,7 +832,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
|
@ -834,9 +852,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
{
|
||||
foreach (var hubPath in HubPaths)
|
||||
{
|
||||
if (!(protocol is MessagePackHubProtocol) || transport != TransportType.ServerSentEvents)
|
||||
if (!(protocol.Value is MessagePackHubProtocol) || transport != TransportType.ServerSentEvents)
|
||||
{
|
||||
yield return new object[] { protocol, transport, hubPath };
|
||||
yield return new object[] { protocol.Key, transport, hubPath };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -847,11 +865,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
// This list excludes "special" hub paths like "default-nowebsockets" which exist for specific tests.
|
||||
public static string[] HubPaths = new[] { "/default", "/dynamic", "/hubT" };
|
||||
|
||||
public static IEnumerable<IHubProtocol> HubProtocols =>
|
||||
new IHubProtocol[]
|
||||
public static Dictionary<string, IHubProtocol> HubProtocols =>
|
||||
new Dictionary<string, IHubProtocol>
|
||||
{
|
||||
new JsonHubProtocol(),
|
||||
new MessagePackHubProtocol(),
|
||||
{ "json", new JsonHubProtocol() },
|
||||
{ "messagepack", new MessagePackHubProtocol() },
|
||||
};
|
||||
|
||||
public static IEnumerable<object[]> TransportTypes()
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public partial class HttpConnectionTests
|
||||
{
|
||||
// Nested class for grouping
|
||||
public class AbortAsync
|
||||
{
|
||||
[Fact]
|
||||
public Task AbortAsyncTriggersClosedEventWithException()
|
||||
{
|
||||
return WithConnectionAsync(CreateConnection(), async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Abort with an error
|
||||
var expected = new Exception("Ruh roh!");
|
||||
await connection.AbortAsync(expected).OrTimeout();
|
||||
|
||||
// Verify that it is thrown
|
||||
var actual = await Assert.ThrowsAsync<Exception>(async () => await closed.OrTimeout());
|
||||
Assert.Same(expected, actual);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task AbortAsyncWhileStoppingTriggersClosedEventWithException()
|
||||
{
|
||||
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(2, out var syncPoints))), async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Stop normally
|
||||
var stopTask = connection.StopAsync().OrTimeout();
|
||||
|
||||
// Wait to reach the first sync point
|
||||
await syncPoints[0].WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// Abort with an error
|
||||
var expected = new Exception("Ruh roh!");
|
||||
var abortTask = connection.AbortAsync(expected).OrTimeout();
|
||||
|
||||
// Wait for the sync point to hit again
|
||||
await syncPoints[1].WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// Release sync point 0
|
||||
syncPoints[0].Continue();
|
||||
|
||||
// We should close with the error from Abort (because it was set by the call to Abort even though Stop triggered the close)
|
||||
var actual = await Assert.ThrowsAsync<Exception>(async () => await closed.OrTimeout());
|
||||
Assert.Same(expected, actual);
|
||||
|
||||
// Clean-up
|
||||
syncPoints[1].Continue();
|
||||
await Task.WhenAll(stopTask, abortTask).OrTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task StopAsyncWhileAbortingTriggersClosedEventWithoutException()
|
||||
{
|
||||
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(2, out var syncPoints))), async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Abort with an error
|
||||
var expected = new Exception("Ruh roh!");
|
||||
var abortTask = connection.AbortAsync(expected).OrTimeout();
|
||||
|
||||
// Wait to reach the first sync point
|
||||
await syncPoints[0].WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// Stop normally, without a sync point.
|
||||
// This should clear the exception, meaning Closed will not "throw"
|
||||
syncPoints[1].Continue();
|
||||
await connection.StopAsync();
|
||||
await closed.OrTimeout();
|
||||
|
||||
// Clean-up
|
||||
syncPoints[0].Continue();
|
||||
await abortTask.OrTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task StartAsyncCannotBeCalledWhileAbortAsyncInProgress()
|
||||
{
|
||||
return WithConnectionAsync(CreateConnection(transport: new TestTransport(onTransportStop: SyncPoint.Create(out var syncPoint))), async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Abort with an error
|
||||
var expected = new Exception("Ruh roh!");
|
||||
var abortTask = connection.AbortAsync(expected).OrTimeout();
|
||||
|
||||
// Wait to reach the first sync point
|
||||
await syncPoint.WaitForSyncPoint().OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", ex.Message);
|
||||
|
||||
// Release the sync point and wait for close to complete
|
||||
// (it will throw the abort exception)
|
||||
syncPoint.Continue();
|
||||
await abortTask.OrTimeout();
|
||||
var actual = await Assert.ThrowsAsync<Exception>(() => closed.OrTimeout());
|
||||
Assert.Same(expected, actual);
|
||||
|
||||
// We can start now
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// And we can stop without getting the abort exception.
|
||||
await connection.StopAsync().OrTimeout();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,13 +2,14 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
|
@ -24,92 +25,58 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotStartRunningConnection()
|
||||
public async Task CanStartStartedConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(CreateConnection(loggerFactory: loggerFactory), async (connection, closed) =>
|
||||
await WithConnectionAsync(CreateConnection(loggerFactory: loggerFactory), async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanStartStartingConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(loggerFactory: loggerFactory, transport: new TestTransport(onTransportStart: SyncPoint.Create(out var syncPoint))),
|
||||
async (connection) =>
|
||||
{
|
||||
var firstStart = connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await syncPoint.WaitForSyncPoint();
|
||||
var secondStart = connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
syncPoint.Continue();
|
||||
|
||||
await firstStart;
|
||||
await secondStart;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotStartConnectionDisposedAfterStarting()
|
||||
public async Task CannotStartConnectionOnceDisposed()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync();
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
await Assert.ThrowsAsync<ObjectDisposedException>(
|
||||
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotStartDisposedConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.DisposeAsync();
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
async () => await connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDisposeStartingConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(
|
||||
loggerFactory: loggerFactory,
|
||||
transport: new TestTransport(
|
||||
onTransportStart: SyncPoint.Create(out var transportStart),
|
||||
onTransportStop: SyncPoint.Create(out var transportStop))),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
// Start the connection and wait for the transport to start up.
|
||||
var startTask = connection.StartAsync(TransferFormat.Text);
|
||||
await transportStart.WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// While the transport is starting, dispose the connection
|
||||
var disposeTask = connection.DisposeAsync();
|
||||
transportStart.Continue(); // We need to release StartAsync, because Dispose waits for it.
|
||||
|
||||
// Wait for start to finish, as that has to finish before the transport will be stopped.
|
||||
await startTask.OrTimeout();
|
||||
|
||||
// Then release DisposeAsync (via the transport StopAsync call)
|
||||
await transportStop.WaitForSyncPoint().OrTimeout();
|
||||
transportStop.Continue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
|
|
@ -138,7 +105,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(
|
||||
loggerFactory: loggerFactory,
|
||||
transport: new TestTransport(onTransportStart: OnTransportStart)),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
Assert.Equal(0, startCounter);
|
||||
await connection.StartAsync(TransferFormat.Text);
|
||||
|
|
@ -164,7 +131,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
CreateConnection(
|
||||
loggerFactory: loggerFactory,
|
||||
transport: new TestTransport(onTransportStart: OnTransportStart)),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text));
|
||||
Assert.Equal("Unable to connect to the server with any of the available transports.", ex.Message);
|
||||
|
|
@ -174,66 +141,115 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanStartStoppedConnection()
|
||||
public async Task CanDisposeUnstartedConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.StopAsync().OrTimeout();
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.DisposeAsync();
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanStopStartingConnection()
|
||||
public async Task CanDisposeStartingConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(
|
||||
loggerFactory: loggerFactory,
|
||||
transport: new TestTransport(onTransportStart: SyncPoint.Create(out var transportStart))),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
// Start and wait for the transport to start up.
|
||||
var startTask = connection.StartAsync(TransferFormat.Text);
|
||||
await transportStart.WaitForSyncPoint().OrTimeout();
|
||||
transport: new TestTransport(
|
||||
onTransportStart: SyncPoint.Create(out var transportStart),
|
||||
onTransportStop: SyncPoint.Create(out var transportStop))),
|
||||
async (connection) =>
|
||||
{
|
||||
// Start the connection and wait for the transport to start up.
|
||||
var startTask = connection.StartAsync(TransferFormat.Text);
|
||||
await transportStart.WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// Stop the connection while it's starting
|
||||
var stopTask = connection.StopAsync();
|
||||
transportStart.Continue(); // We need to release Start in order for Stop to begin working.
|
||||
// While the transport is starting, dispose the connection
|
||||
var disposeTask = connection.DisposeAsync().OrTimeout();
|
||||
transportStart.Continue(); // We need to release StartAsync, because Dispose waits for it.
|
||||
|
||||
// Wait for start to finish, which will allow stop to finish and the connection to close.
|
||||
await startTask.OrTimeout();
|
||||
await stopTask.OrTimeout();
|
||||
await closed.OrTimeout();
|
||||
});
|
||||
// Wait for start to finish, as that has to finish before the transport will be stopped.
|
||||
await startTask.OrTimeout();
|
||||
|
||||
// Then release DisposeAsync (via the transport StopAsync call)
|
||||
await transportStop.WaitForSyncPoint().OrTimeout();
|
||||
transportStop.Continue();
|
||||
|
||||
// Dispose should finish
|
||||
await disposeTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StoppingStoppingConnectionNoOps()
|
||||
public async Task CanDisposeDisposingConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
CreateConnection(
|
||||
loggerFactory: loggerFactory,
|
||||
transport: new TestTransport(onTransportStop: SyncPoint.Create(out var transportStop))),
|
||||
async (connection) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await Task.WhenAll(connection.StopAsync(), connection.StopAsync()).OrTimeout();
|
||||
await closed.OrTimeout();
|
||||
|
||||
// Dispose the connection
|
||||
var stopTask = connection.DisposeAsync().OrTimeout();
|
||||
|
||||
// Once the transport starts shutting down
|
||||
await transportStop.WaitForSyncPoint();
|
||||
Assert.False(stopTask.IsCompleted);
|
||||
|
||||
// Start disposing again, and then let the first dispose continue
|
||||
var disposeTask = connection.DisposeAsync().OrTimeout();
|
||||
transportStop.Continue();
|
||||
|
||||
// Wait for the tasks to complete
|
||||
await stopTask.OrTimeout();
|
||||
await disposeTask.OrTimeout();
|
||||
|
||||
// We should be disposed and thus unable to restart.
|
||||
await AssertDisposedAsync(connection);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanStartConnectionAfterConnectionStoppedWithError()
|
||||
public async Task TransportIsStoppedWhenConnectionIsDisposed()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler();
|
||||
|
||||
using (var httpClient = new HttpClient(testHttpHandler))
|
||||
{
|
||||
var testTransport = new TestTransport();
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(transport: testTransport),
|
||||
async (connection) =>
|
||||
{
|
||||
// Start the transport
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.NotNull(testTransport.Receiving);
|
||||
Assert.False(testTransport.Receiving.IsCompleted);
|
||||
|
||||
// Stop the connection, and we should stop the transport
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
await testTransport.Receiving.OrTimeout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TransportPipeIsCompletedWhenErrorOccursInTransport()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
|
|
@ -257,119 +273,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(httpHandler, loggerFactory),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.SendAsync(new byte[] { 0x42 }).OrTimeout();
|
||||
|
||||
// Wait for the connection to close, because the send failed.
|
||||
await Assert.ThrowsAsync<HttpRequestException>(() => closed.OrTimeout());
|
||||
|
||||
// Start it up again
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposedStoppingConnectionDisposesConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(
|
||||
loggerFactory: loggerFactory,
|
||||
transport: new TestTransport(onTransportStop: SyncPoint.Create(out var transportStop))),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
// Start the connection
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// Stop the connection
|
||||
var stopTask = connection.StopAsync().OrTimeout();
|
||||
|
||||
// Once the transport starts shutting down
|
||||
await transportStop.WaitForSyncPoint();
|
||||
|
||||
// Start disposing and allow it to finish shutting down
|
||||
var disposeTask = connection.DisposeAsync().OrTimeout();
|
||||
transportStop.Continue();
|
||||
|
||||
// Wait for the tasks to complete
|
||||
await stopTask.OrTimeout();
|
||||
await closed.OrTimeout();
|
||||
await disposeTask.OrTimeout();
|
||||
|
||||
// We should be disposed and thus unable to restart.
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanDisposeStoppedConnection()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(loggerFactory: loggerFactory),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.StopAsync().OrTimeout();
|
||||
await closed.OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
await connection.Transport.Output.WriteAsync(new byte[] { 0x42 }).OrTimeout();
|
||||
|
||||
// We should get the exception in the transport input completion.
|
||||
await Assert.ThrowsAsync<HttpRequestException>(() => connection.Transport.Input.WaitForWriterToComplete());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task ClosedEventRaisedWhenTheClientIsDisposed()
|
||||
{
|
||||
return WithConnectionAsync(
|
||||
CreateConnection(),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
await closed.OrTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionClosedWhenTransportFails()
|
||||
{
|
||||
var testTransport = new TestTransport();
|
||||
|
||||
var expected = new Exception("Whoops!");
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(transport: testTransport),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
testTransport.Application.Output.Complete(expected);
|
||||
var actual = await Assert.ThrowsAsync<Exception>(() => closed.OrTimeout());
|
||||
Assert.Same(expected, actual);
|
||||
|
||||
var sendException = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync(new byte[0]).OrTimeout());
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", sendException.Message);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task ClosedEventNotRaisedWhenTheClientIsStoppedButWasNeverStarted()
|
||||
{
|
||||
return WithConnectionAsync(
|
||||
CreateConnection(),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
Assert.False(closed.IsCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SSEWontStartIfSuccessfulConnectionIsNotEstablished()
|
||||
{
|
||||
|
|
@ -386,7 +300,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(httpHandler, loggerFactory: loggerFactory, url: null, transport: sse),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
|
|
@ -412,7 +326,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(httpHandler, loggerFactory: loggerFactory, url: null, transport: sse),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
var startTask = connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.False(connectResponseTcs.Task.IsCompleted);
|
||||
|
|
@ -423,30 +337,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TransportIsStoppedWhenConnectionIsStopped()
|
||||
private static async Task AssertDisposedAsync(HttpConnection connection)
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler();
|
||||
|
||||
// Just keep returning data when polled
|
||||
testHttpHandler.OnLongPoll(_ => ResponseUtils.CreateResponse(HttpStatusCode.OK));
|
||||
|
||||
using (var httpClient = new HttpClient(testHttpHandler))
|
||||
{
|
||||
var longPollingTransport = new LongPollingTransport(httpClient);
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(transport: longPollingTransport),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
// Start the transport
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.False(longPollingTransport.Running.IsCompleted, "Expected that the transport would still be running");
|
||||
|
||||
// Stop the connection, and we should stop the transport
|
||||
await connection.StopAsync().OrTimeout();
|
||||
await longPollingTransport.Running.OrTimeout();
|
||||
});
|
||||
}
|
||||
var exception =
|
||||
await Assert.ThrowsAsync<ObjectDisposedException>(() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,102 +43,18 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
}
|
||||
|
||||
private static async Task WithConnectionAsync(HttpConnection connection, Func<HttpConnection, Task, Task> body)
|
||||
private static async Task WithConnectionAsync(HttpConnection connection, Func<HttpConnection, Task> body)
|
||||
{
|
||||
try
|
||||
{
|
||||
var closedTcs = new TaskCompletionSource<object>();
|
||||
connection.Closed += ex =>
|
||||
{
|
||||
if (ex != null)
|
||||
{
|
||||
closedTcs.SetException(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
closedTcs.SetResult(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Using OrTimeout here will hide any timeout issues in the test :(.
|
||||
await body(connection, closedTcs.Task);
|
||||
await body(connection);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
// Possibly useful as a general-purpose async testing helper?
|
||||
private class SyncPoint
|
||||
{
|
||||
private TaskCompletionSource<object> _atSyncPoint = new TaskCompletionSource<object>();
|
||||
private TaskCompletionSource<object> _continueFromSyncPoint = new TaskCompletionSource<object>();
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the code-under-test to reach <see cref="WaitToContinue"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task WaitForSyncPoint() => _atSyncPoint.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the code-under-test to continue past where it waited for <see cref="WaitToContinue"/>.
|
||||
/// </summary>
|
||||
public void Continue() => _continueFromSyncPoint.TrySetResult(null);
|
||||
|
||||
/// <summary>
|
||||
/// Used by the code-under-test to wait for the test code to sync up.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This code will unblock <see cref="WaitForSyncPoint"/> and then block waiting for <see cref="Continue"/> to be called.
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
public Task WaitToContinue()
|
||||
{
|
||||
_atSyncPoint.TrySetResult(null);
|
||||
return _continueFromSyncPoint.Task;
|
||||
}
|
||||
|
||||
public static Func<Task> Create(out SyncPoint syncPoint)
|
||||
{
|
||||
var handler = Create(1, out var syncPoints);
|
||||
syncPoint = syncPoints[0];
|
||||
return handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a re-entrant function that waits for sync points in sequence.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of sync points to expect</param>
|
||||
/// <param name="syncPoints">The <see cref="SyncPoint"/> objects that can be used to coordinate the sync point</param>
|
||||
/// <returns></returns>
|
||||
public static Func<Task> Create(int count, out SyncPoint[] syncPoints)
|
||||
{
|
||||
// Need to use a local so the closure can capture it. You can't use out vars in a closure.
|
||||
var localSyncPoints = new SyncPoint[count];
|
||||
for (var i = 0; i < count; i += 1)
|
||||
{
|
||||
localSyncPoints[i] = new SyncPoint();
|
||||
}
|
||||
|
||||
syncPoints = localSyncPoints;
|
||||
|
||||
var counter = 0;
|
||||
return () =>
|
||||
{
|
||||
if (counter >= localSyncPoints.Length)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
var syncPoint = localSyncPoints[counter];
|
||||
|
||||
counter += 1;
|
||||
return syncPoint.WaitToContinue();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler, url: requestedUrl),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
|
|
@ -95,17 +95,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
new
|
||||
{
|
||||
transport = "QuantumEntanglement",
|
||||
transferFormats = new string[] { "Qbits" },
|
||||
transferFormats = new[] { "Qbits" },
|
||||
},
|
||||
new
|
||||
{
|
||||
transport = "CarrierPigeon",
|
||||
transferFormats = new string[] { "Text" },
|
||||
transferFormats = new[] { "Text" },
|
||||
},
|
||||
new
|
||||
{
|
||||
transport = "LongPolling",
|
||||
transferFormats = new string[] { "Text", "Binary" }
|
||||
transferFormats = new[] { "Text", "Binary" }
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
|
@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler, transportFactory: transportFactory.Object),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
});
|
||||
|
|
@ -141,17 +141,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
new
|
||||
{
|
||||
transport = "WebSockets",
|
||||
transferFormats = new string[] { "Qbits" },
|
||||
transferFormats = new[] { "Qbits" },
|
||||
},
|
||||
new
|
||||
{
|
||||
transport = "ServerSentEvents",
|
||||
transferFormats = new string[] { "Text" },
|
||||
transferFormats = new[] { "Text" },
|
||||
},
|
||||
new
|
||||
{
|
||||
transport = "LongPolling",
|
||||
transferFormats = new string[] { "Text", "Binary" }
|
||||
transferFormats = new[] { "Text", "Binary" }
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
|
@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler, transportFactory: transportFactory.Object),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
});
|
||||
|
|
@ -178,7 +178,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<TException>(
|
||||
() => connection.StartAsync(TransferFormat.Text).OrTimeout());
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public partial class HttpConnectionTests
|
||||
{
|
||||
public class OnReceived
|
||||
{
|
||||
[Fact]
|
||||
public async Task CanReceiveData()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler();
|
||||
|
||||
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.OK, "42"));
|
||||
testHttpHandler.OnSocketSend((_, __) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
var receiveTcs = new TaskCompletionSource<string>();
|
||||
connection.OnReceived((data, state) =>
|
||||
{
|
||||
var tcs = ((TaskCompletionSource<string>)state);
|
||||
tcs.TrySetResult(Encoding.UTF8.GetString(data));
|
||||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanReceiveDataEvenIfExceptionThrownFromPreviousReceivedEvent()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler();
|
||||
|
||||
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.OK, "42"));
|
||||
testHttpHandler.OnSocketSend((_, __) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
var receiveTcs = new TaskCompletionSource<string>();
|
||||
var receivedRaised = false;
|
||||
connection.OnReceived((data, state) =>
|
||||
{
|
||||
if (!receivedRaised)
|
||||
{
|
||||
receivedRaised = true;
|
||||
return Task.FromException(new InvalidOperationException());
|
||||
}
|
||||
|
||||
receiveTcs.TrySetResult(Encoding.UTF8.GetString(data));
|
||||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
|
||||
Assert.True(receivedRaised);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanReceiveDataEvenIfExceptionThrownSynchronouslyFromPreviousReceivedEvent()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler();
|
||||
|
||||
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.OK, "42"));
|
||||
testHttpHandler.OnSocketSend((_, __) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
var receiveTcs = new TaskCompletionSource<string>();
|
||||
var receivedRaised = false;
|
||||
connection.OnReceived((data, state) =>
|
||||
{
|
||||
if (!receivedRaised)
|
||||
{
|
||||
receivedRaised = true;
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
receiveTcs.TrySetResult(Encoding.UTF8.GetString(data));
|
||||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.Contains("42", await receiveTcs.Task.OrTimeout());
|
||||
Assert.True(receivedRaised);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,20 +2,53 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public partial class HttpConnectionTests
|
||||
{
|
||||
public class SendAsync
|
||||
public class Transport
|
||||
{
|
||||
[Fact]
|
||||
public async Task CanReceiveData()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler();
|
||||
|
||||
// Set the long poll up to return a single message over a few polls.
|
||||
var requestCount = 0;
|
||||
var messageFragments = new[] {"This ", "is ", "a ", "test"};
|
||||
testHttpHandler.OnLongPoll(cancellationToken =>
|
||||
{
|
||||
if (requestCount >= messageFragments.Length)
|
||||
{
|
||||
return ResponseUtils.CreateResponse(HttpStatusCode.NoContent);
|
||||
}
|
||||
|
||||
var resp = ResponseUtils.CreateResponse(HttpStatusCode.OK, messageFragments[requestCount]);
|
||||
requestCount += 1;
|
||||
return resp;
|
||||
});
|
||||
testHttpHandler.OnSocketSend((_, __) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler),
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
Assert.Contains("This is a test", Encoding.UTF8.GetString(await connection.Transport.Input.ReadAllAsync()));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanSendData()
|
||||
{
|
||||
|
|
@ -36,11 +69,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
await connection.SendAsync(data).OrTimeout();
|
||||
await connection.Transport.Output.WriteAsync(data).OrTimeout();
|
||||
|
||||
Assert.Equal(data, await sendTcs.Task.OrTimeout());
|
||||
|
||||
|
|
@ -53,74 +86,44 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
{
|
||||
return WithConnectionAsync(
|
||||
CreateConnection(),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => connection.SendAsync(new byte[0]).OrTimeout());
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||
() => connection.Transport.Output.WriteAsync(new byte[0]).OrTimeout());
|
||||
Assert.Equal($"Cannot access the {nameof(Transport)} pipe before the connection has started.", exception.Message);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task SendThrowsIfConnectionIsStopped()
|
||||
public Task TransportPipeCannotBeAccessedAfterConnectionIsDisposed()
|
||||
{
|
||||
return WithConnectionAsync(
|
||||
CreateConnection(),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.StopAsync().OrTimeout();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => connection.SendAsync(new byte[0]).OrTimeout());
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task SendThrowsIfConnectionIsDisposed()
|
||||
{
|
||||
return WithConnectionAsync(
|
||||
CreateConnection(),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => connection.SendAsync(new byte[0]).OrTimeout());
|
||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||
var exception = await Assert.ThrowsAsync<ObjectDisposedException>(
|
||||
() => connection.Transport.Output.WriteAsync(new byte[0]).OrTimeout());
|
||||
Assert.Equal(nameof(HttpConnection), exception.ObjectName);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExceptionOnSendAsyncClosesWithError()
|
||||
public Task TransportIsShutDownAfterDispose()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler();
|
||||
|
||||
var longPollTcs = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
testHttpHandler.OnLongPoll(cancellationToken =>
|
||||
{
|
||||
cancellationToken.Register(() => longPollTcs.TrySetResult(null));
|
||||
|
||||
return longPollTcs.Task;
|
||||
});
|
||||
|
||||
testHttpHandler.OnSocketSend((buf, cancellationToken) =>
|
||||
{
|
||||
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError));
|
||||
});
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(testHttpHandler),
|
||||
async (connection, closed) =>
|
||||
var transport = new TestTransport();
|
||||
return WithConnectionAsync(
|
||||
CreateConnection(transport: transport),
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
|
||||
await connection.SendAsync(new byte[] { 0 }).OrTimeout();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<HttpRequestException>(() => closed.OrTimeout());
|
||||
// This will throw OperationCancelledException if it's forcibly terminated
|
||||
// which we don't want
|
||||
await transport.Receiving.OrTimeout();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -2,17 +2,13 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
|
|
@ -54,87 +50,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
() => new HttpConnection(new Uri("http://fakeuri.org/"), requestedTransportType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EventsAreNotRunningOnMainLoop()
|
||||
{
|
||||
var testTransport = new TestTransport();
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(transport: testTransport),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
// Block up the OnReceived callback until we finish the test.
|
||||
var onReceived = new SyncPoint();
|
||||
connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout());
|
||||
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
|
||||
// This will trigger the received callback
|
||||
await testTransport.Application.Output.WriteAsync(new byte[] { 1 });
|
||||
|
||||
// Wait to hit the sync point. We are now blocking up the TaskQueue
|
||||
await onReceived.WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// Now we write something else and we want to test that the HttpConnection receive loop is still
|
||||
// removing items from the channel even though OnReceived is blocked up.
|
||||
await testTransport.Application.Output.WriteAsync(new byte[] { 1 });
|
||||
|
||||
// Now that we've written, we wait for WaitToReadAsync to return an INCOMPLETE task. It will do so
|
||||
// once HttpConnection reads the message. We also use a CTS to timeout in case the loop is indeed blocked
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
while (testTransport.Application.Input.WaitToReadAsync().IsCompleted && !cts.IsCancellationRequested)
|
||||
{
|
||||
// Yield to allow the HttpConnection to dequeue the message
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
// If we exited because we were cancelled, throw.
|
||||
cts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
// We're free! Unblock onreceived
|
||||
onReceived.Continue();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EventQueueTimeout()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger<HttpConnectionTests>();
|
||||
|
||||
var testTransport = new TestTransport();
|
||||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(transport: testTransport),
|
||||
async (connection, closed) =>
|
||||
{
|
||||
var onReceived = new SyncPoint();
|
||||
connection.OnReceived(_ => onReceived.WaitToContinue().OrTimeout());
|
||||
|
||||
logger.LogInformation("Starting connection");
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
logger.LogInformation("Started connection");
|
||||
|
||||
await testTransport.Application.Output.WriteAsync(new byte[] { 1 });
|
||||
await onReceived.WaitForSyncPoint().OrTimeout();
|
||||
|
||||
// Dispose should complete, even though the receive callbacks are completely blocked up.
|
||||
logger.LogInformation("Disposing connection");
|
||||
await connection.DisposeAsync().OrTimeout(TimeSpan.FromSeconds(10));
|
||||
logger.LogInformation("Disposed connection");
|
||||
|
||||
// Clear up blocked tasks.
|
||||
onReceived.Continue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpOptionsSetOntoHttpClientHandler()
|
||||
{
|
||||
var testHttpHandler = new TestHttpMessageHandler();
|
||||
var testHttpHandler = TestHttpMessageHandler.CreateDefault();
|
||||
|
||||
var negotiateUrlTcs = new TaskCompletionSource<string>();
|
||||
testHttpHandler.OnNegotiate((request, cancellationToken) =>
|
||||
|
|
@ -146,7 +65,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
HttpClientHandler httpClientHandler = null;
|
||||
|
||||
HttpOptions httpOptions = new HttpOptions();
|
||||
var httpOptions = new HttpOptions();
|
||||
httpOptions.HttpMessageHandler = inner =>
|
||||
{
|
||||
httpClientHandler = (HttpClientHandler)inner;
|
||||
|
|
@ -161,7 +80,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(httpOptions, url: "http://fakeuri.org/"),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
|
|
@ -198,7 +117,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
{
|
||||
await WithConnectionAsync(
|
||||
CreateConnection(httpOptions, loggerFactory: mockLoggerFactory.Object, url: "http://fakeuri.org/"),
|
||||
async (connection, closed) =>
|
||||
async (connection) =>
|
||||
{
|
||||
await connection.StartAsync(TransferFormat.Text).OrTimeout();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,204 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public class HubConnectionExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task On()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On("Foo",
|
||||
() => tcs.SetResult(new object[0])),
|
||||
new object[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT1()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int>("Foo",
|
||||
r => tcs.SetResult(new object[] { r })),
|
||||
new object[] { 42 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT2()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string>("Foo",
|
||||
(r1, r2) => tcs.SetResult(new object[] { r1, r2 })),
|
||||
new object[] { 42, "abc" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT3()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float>("Foo",
|
||||
(r1, r2, r3) => tcs.SetResult(new object[] { r1, r2, r3 })),
|
||||
new object[] { 42, "abc", 24.0f });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT4()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double>("Foo",
|
||||
(r1, r2, r3, r4) => tcs.SetResult(new object[] { r1, r2, r3, r4 })),
|
||||
new object[] { 42, "abc", 24.0f, 10d });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT5()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double, string>("Foo",
|
||||
(r1, r2, r3, r4, r5) => tcs.SetResult(new object[] { r1, r2, r3, r4, r5 })),
|
||||
new object[] { 42, "abc", 24.0f, 10d, "123" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT6()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double, string, byte>("Foo",
|
||||
(r1, r2, r3, r4, r5, r6) => tcs.SetResult(new object[] { r1, r2, r3, r4, r5, r6 })),
|
||||
new object[] { 42, "abc", 24.0f, 10d, "123", 24 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT7()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double, string, byte, char>("Foo",
|
||||
(r1, r2, r3, r4, r5, r6, r7) => tcs.SetResult(new object[] { r1, r2, r3, r4, r5, r6, r7 })),
|
||||
new object[] { 42, "abc", 24.0f, 10d, "123", 24, 'c' });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT8()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double, string, byte, char, string>("Foo",
|
||||
(r1, r2, r3, r4, r5, r6, r7, r8) => tcs.SetResult(new object[] { r1, r2, r3, r4, r5, r6, r7, r8 })),
|
||||
new object[] { 42, "abc", 24.0f, 10d, "123", 24, 'c', "XYZ" });
|
||||
}
|
||||
|
||||
private async Task InvokeOn(Action<HubConnection, TaskCompletionSource<object[]>> onAction, object[] args)
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
var handlerTcs = new TaskCompletionSource<object[]>();
|
||||
try
|
||||
{
|
||||
onAction(hubConnection, handlerTcs);
|
||||
await hubConnection.StartAsync();
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "1",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = args
|
||||
}).OrTimeout();
|
||||
|
||||
var result = await handlerTcs.Task.OrTimeout();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionNotClosedOnCallbackArgumentCountMismatch()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
var receiveTcs = new TaskCompletionSource<int>();
|
||||
|
||||
try
|
||||
{
|
||||
hubConnection.On<int>("Foo", r => { receiveTcs.SetResult(r); });
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "1",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = new object[] { 42, "42" }
|
||||
}).OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "2",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = new object[] { 42 }
|
||||
}).OrTimeout();
|
||||
|
||||
Assert.Equal(42, await receiveTcs.Task.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionNotClosedOnCallbackArgumentTypeMismatch()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
var receiveTcs = new TaskCompletionSource<int>();
|
||||
|
||||
try
|
||||
{
|
||||
hubConnection.On<int>("Foo", r => { receiveTcs.SetResult(r); });
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "1",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = new object[] { "xxx" }
|
||||
}).OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "2",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = new object[] { 42 }
|
||||
}).OrTimeout();
|
||||
|
||||
Assert.Equal(42, await receiveTcs.Task.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,429 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
// This includes tests that verify HubConnection conforms to the Hub Protocol, without setting up a full server (even TestServer).
|
||||
// We can also have more control over the messages we send to HubConnection in order to ensure that protocol errors and other quirks
|
||||
// don't cause problems.
|
||||
public class HubConnectionProtocolTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendAsyncSendsANonBlockingInvocationMessage()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.SendAsync("Foo");
|
||||
|
||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
Assert.Equal("{\"type\":1,\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientSendsHandshakeMessageWhenStartingConnection()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
var handshakeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
Assert.Equal("{\"protocol\":\"json\",\"version\":1}\u001e", handshakeMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeSendsAnInvocationMessage()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync("Foo");
|
||||
|
||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
Assert.Equal("{\"type\":1,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReceiveCloseMessageWithoutErrorWillCloseHubConnection()
|
||||
{
|
||||
TaskCompletionSource<Exception> closedTcs = new TaskCompletionSource<Exception>();
|
||||
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
hubConnection.Closed += e => closedTcs.SetResult(e);
|
||||
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new { type = 7 }).OrTimeout();
|
||||
|
||||
Exception closeException = await closedTcs.Task.OrTimeout();
|
||||
Assert.Null(closeException);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReceiveCloseMessageWithErrorWillCloseHubConnection()
|
||||
{
|
||||
TaskCompletionSource<Exception> closedTcs = new TaskCompletionSource<Exception>();
|
||||
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
hubConnection.Closed += e => closedTcs.SetResult(e);
|
||||
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new { type = 7, error = "Error!" }).OrTimeout();
|
||||
|
||||
Exception closeException = await closedTcs.Task.OrTimeout();
|
||||
Assert.NotNull(closeException);
|
||||
Assert.Equal("Error!", closeException.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamSendsAnInvocationMessage()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<object>("Foo");
|
||||
|
||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
Assert.Equal("{\"type\":4,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}\u001e", invokeMessage);
|
||||
|
||||
// Complete the channel
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
||||
await channel.Completion;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeCompletedWhenCompletionMessageReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync("Foo");
|
||||
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
||||
|
||||
await invokeTask.OrTimeout();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamCompletesWhenCompletionMessageIsReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<int>("Foo");
|
||||
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
||||
|
||||
Assert.Empty(await channel.ReadAllAsync());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeYieldsResultWhenCompletionMessageReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo");
|
||||
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, result = 42 }).OrTimeout();
|
||||
|
||||
Assert.Equal(42, await invokeTask.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeFailsWithExceptionWhenCompletionWithErrorReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo");
|
||||
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, error = "An error occurred" }).OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<HubException>(() => invokeTask).OrTimeout();
|
||||
Assert.Equal("An error occurred", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamFailsIfCompletionMessageHasPayload()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<string>("Foo");
|
||||
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, result = "Oops" }).OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await channel.ReadAllAsync().OrTimeout());
|
||||
Assert.Equal("Server provided a result in a completion response to a streamed invocation.", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamFailsWithExceptionWhenCompletionWithErrorReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<int>("Foo");
|
||||
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, error = "An error occurred" }).OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<HubException>(async () => await channel.ReadAllAsync().OrTimeout());
|
||||
Assert.Equal("An error occurred", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeFailsWithErrorWhenStreamingItemReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo");
|
||||
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = 42 }).OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => invokeTask).OrTimeout();
|
||||
Assert.Equal("Streaming hub methods must be invoked with the 'HubConnection.StreamAsChannelAsync' method.", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamYieldsItemsAsTheyArrive()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<string>("Foo");
|
||||
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = "1" }).OrTimeout();
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = "2" }).OrTimeout();
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = "3" }).OrTimeout();
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
||||
|
||||
var notifications = await channel.ReadAllAsync().OrTimeout();
|
||||
|
||||
Assert.Equal(new[] { "1", "2", "3", }, notifications.ToArray());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlerRegisteredWithOnIsFiredWhenInvocationReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
var handlerCalled = new TaskCompletionSource<object[]>();
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
hubConnection.On<int, string, float>("Foo", (r1, r2, r3) => handlerCalled.TrySetResult(new object[] { r1, r2, r3 }));
|
||||
|
||||
var args = new object[] { 1, "Foo", 2.0f };
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 1, target = "Foo", arguments = args }).OrTimeout();
|
||||
|
||||
Assert.Equal(args, await handlerCalled.Task.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcceptsPingMessages()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection,
|
||||
new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
// Ignore handshake message
|
||||
await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
// Send an invocation
|
||||
var invokeTask = hubConnection.InvokeAsync("Foo");
|
||||
|
||||
// Receive the ping mid-invocation so we can see that the rest of the flow works fine
|
||||
await connection.ReceiveJsonMessage(new { type = 6 }).OrTimeout();
|
||||
|
||||
// Receive a completion
|
||||
await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout();
|
||||
|
||||
// Ensure the invokeTask completes properly
|
||||
await invokeTask.OrTimeout();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public partial class HubConnectionTests
|
||||
{
|
||||
public class ConnectionLifecycle
|
||||
{
|
||||
// This tactic (using names and a dictionary) allows non-serializable data (like a Func) to be used in a theory AND get it to show in the new hierarchical view in Test Explorer as separate tests you can run individually.
|
||||
private static readonly IDictionary<string, Func<HubConnection, Task>> MethodsThatRequireActiveConnection = new Dictionary<string, Func<HubConnection, Task>>()
|
||||
{
|
||||
{ nameof(HubConnection.InvokeAsync), (connection) => connection.InvokeAsync("Foo") },
|
||||
{ nameof(HubConnection.SendAsync), (connection) => connection.SendAsync("Foo") },
|
||||
{ nameof(HubConnection.StreamAsChannelAsync), (connection) => connection.StreamAsChannelAsync<object>("Foo") },
|
||||
};
|
||||
|
||||
public static IEnumerable<object[]> MethodsNamesThatRequireActiveConnection => MethodsThatRequireActiveConnection.Keys.Select(k => new object[] { k });
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsyncStartsTheUnderlyingConnection()
|
||||
{
|
||||
var testConnection = new TestConnection();
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
await connection.StartAsync();
|
||||
Assert.True(testConnection.Started.IsCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsyncWaitsForPreviousStartIfAlreadyStarting()
|
||||
{
|
||||
// Set up StartAsync to wait on the syncPoint when starting
|
||||
var testConnection = new TestConnection(onStart: SyncPoint.Create(out var syncPoint));
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
var firstStart = connection.StartAsync().OrTimeout();
|
||||
Assert.False(firstStart.IsCompleted);
|
||||
|
||||
// Wait for us to be in IConnection.StartAsync
|
||||
await syncPoint.WaitForSyncPoint();
|
||||
|
||||
// Try starting again
|
||||
var secondStart = connection.StartAsync().OrTimeout();
|
||||
Assert.False(secondStart.IsCompleted);
|
||||
|
||||
// Release the sync point
|
||||
syncPoint.Continue();
|
||||
|
||||
// Both starts should finish fine
|
||||
await firstStart;
|
||||
await secondStart;
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartingAfterStopCreatesANewConnection()
|
||||
{
|
||||
// Set up StartAsync to wait on the syncPoint when starting
|
||||
var createCount = 0;
|
||||
IConnection ConnectionFactory()
|
||||
{
|
||||
createCount += 1;
|
||||
return new TestConnection();
|
||||
}
|
||||
|
||||
await AsyncUsing(new HubConnection(ConnectionFactory, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.Equal(1, createCount);
|
||||
await connection.StopAsync().OrTimeout();
|
||||
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.Equal(2, createCount);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartingDuringStopCreatesANewConnection()
|
||||
{
|
||||
// Set up StartAsync to wait on the syncPoint when starting
|
||||
var createCount = 0;
|
||||
var onDisposeForFirstConnection = SyncPoint.Create(out var syncPoint);
|
||||
IConnection ConnectionFactory()
|
||||
{
|
||||
createCount += 1;
|
||||
return new TestConnection(onDispose: createCount == 1 ? onDisposeForFirstConnection : null);
|
||||
}
|
||||
|
||||
await AsyncUsing(new HubConnection(ConnectionFactory, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.Equal(1, createCount);
|
||||
|
||||
var stopTask = connection.StopAsync().OrTimeout();
|
||||
|
||||
// Wait to hit DisposeAsync on TestConnection (which should be after StopAsync has cleared the connection state)
|
||||
await syncPoint.WaitForSyncPoint();
|
||||
|
||||
// We should be able to start now, and StopAsync hasn't completed, nor will it complete while Starting
|
||||
Assert.False(stopTask.IsCompleted);
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.False(stopTask.IsCompleted);
|
||||
|
||||
// When we release the sync point, the StopAsync task will finish
|
||||
syncPoint.Continue();
|
||||
await stopTask;
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MethodsNamesThatRequireActiveConnection))]
|
||||
public async Task MethodsThatRequireStartedConnectionFailIfConnectionNotYetStarted(string name)
|
||||
{
|
||||
var method = MethodsThatRequireActiveConnection[name];
|
||||
|
||||
var testConnection = new TestConnection();
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => method(connection));
|
||||
Assert.Equal($"The '{name}' method cannot be called if the connection is not active", ex.Message);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MethodsNamesThatRequireActiveConnection))]
|
||||
public async Task MethodsThatRequireStartedConnectionWaitForStartIfConnectionIsCurrentlyStarting(string name)
|
||||
{
|
||||
var method = MethodsThatRequireActiveConnection[name];
|
||||
|
||||
// Set up StartAsync to wait on the syncPoint when starting
|
||||
var testConnection = new TestConnection(onStart: SyncPoint.Create(out var syncPoint));
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
// Start, and wait for the sync point to be hit
|
||||
var startTask = connection.StartAsync().OrTimeout();
|
||||
Assert.False(startTask.IsCompleted);
|
||||
await syncPoint.WaitForSyncPoint();
|
||||
|
||||
// Run the method, but it will be waiting for the lock
|
||||
var targetTask = method(connection).OrTimeout();
|
||||
|
||||
// Release the SyncPoint
|
||||
syncPoint.Continue();
|
||||
|
||||
// Wait for start to finish
|
||||
await startTask;
|
||||
|
||||
// We need some special logic to ensure InvokeAsync completes.
|
||||
if (string.Equals(name, nameof(HubConnection.InvokeAsync)))
|
||||
{
|
||||
await ForceLastInvocationToComplete(testConnection);
|
||||
}
|
||||
|
||||
// Wait for the method to complete.
|
||||
await targetTask;
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsyncStopsConnection()
|
||||
{
|
||||
var testConnection = new TestConnection();
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.True(testConnection.Started.IsCompleted);
|
||||
|
||||
await connection.StopAsync().OrTimeout();
|
||||
Assert.True(testConnection.Disposed.IsCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsyncNoOpsIfConnectionNotYetStarted()
|
||||
{
|
||||
var testConnection = new TestConnection();
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
await connection.StopAsync().OrTimeout();
|
||||
Assert.False(testConnection.Disposed.IsCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsyncNoOpsIfConnectionAlreadyStopped()
|
||||
{
|
||||
var testConnection = new TestConnection();
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.True(testConnection.Started.IsCompleted);
|
||||
|
||||
await connection.StopAsync().OrTimeout();
|
||||
Assert.True(testConnection.Disposed.IsCompleted);
|
||||
|
||||
await connection.StopAsync().OrTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompletingTheTransportSideMarksConnectionAsClosed()
|
||||
{
|
||||
var testConnection = new TestConnection();
|
||||
var closed = new TaskCompletionSource<object>();
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
connection.Closed += (e) => closed.TrySetResult(null);
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.True(testConnection.Started.IsCompleted);
|
||||
|
||||
// Complete the transport side and wait for the connection to close
|
||||
testConnection.CompleteFromTransport();
|
||||
await closed.Task.OrTimeout();
|
||||
|
||||
// We should be stopped now
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo").OrTimeout());
|
||||
Assert.Equal($"The '{nameof(HubConnection.SendAsync)}' method cannot be called if the connection is not active", ex.Message);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TransportCompletionWhileShuttingDownIsNoOp()
|
||||
{
|
||||
var testConnection = new TestConnection();
|
||||
var testConnectionClosed = new TaskCompletionSource<object>();
|
||||
var connectionClosed = new TaskCompletionSource<object>();
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
// We're hooking the TestConnection shutting down here because the HubConnection one will be blocked on the lock
|
||||
testConnection.Transport.Input.OnWriterCompleted((_, __) => testConnectionClosed.TrySetResult(null), null);
|
||||
connection.Closed += (e) => connectionClosed.TrySetResult(null);
|
||||
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.True(testConnection.Started.IsCompleted);
|
||||
|
||||
// Start shutting down and complete the transport side
|
||||
var stopTask = connection.StopAsync().OrTimeout();
|
||||
testConnection.CompleteFromTransport();
|
||||
|
||||
// Wait for the connection to close.
|
||||
await testConnectionClosed.Task.OrTimeout();
|
||||
|
||||
// The stop should be completed.
|
||||
await stopTask;
|
||||
|
||||
// The HubConnection should now be closed.
|
||||
await connectionClosed.Task.OrTimeout();
|
||||
|
||||
// We should be stopped now
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo").OrTimeout());
|
||||
Assert.Equal($"The '{nameof(HubConnection.SendAsync)}' method cannot be called if the connection is not active", ex.Message);
|
||||
|
||||
Assert.Equal(1, testConnection.DisposeCount);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsyncDuringUnderlyingConnectionCloseWaitsAndNoOps()
|
||||
{
|
||||
var testConnection = new TestConnection();
|
||||
var connectionClosed = new TaskCompletionSource<object>();
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
connection.Closed += (e) => connectionClosed.TrySetResult(null);
|
||||
|
||||
await connection.StartAsync().OrTimeout();
|
||||
Assert.True(testConnection.Started.IsCompleted);
|
||||
|
||||
// Complete the transport side and wait for the connection to close
|
||||
testConnection.CompleteFromTransport();
|
||||
|
||||
// Start stopping manually (these can't be synchronized by a Sync Point because the transport is disposed outside the lock)
|
||||
var stopTask = connection.StopAsync().OrTimeout();
|
||||
|
||||
await testConnection.Disposed.OrTimeout();
|
||||
|
||||
// Wait for the stop task to complete and the closed event to fire
|
||||
await stopTask;
|
||||
await connectionClosed.Task.OrTimeout();
|
||||
|
||||
// We should be stopped now
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync("Foo").OrTimeout());
|
||||
Assert.Equal($"The '{nameof(HubConnection.SendAsync)}' method cannot be called if the connection is not active", ex.Message);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MethodsNamesThatRequireActiveConnection))]
|
||||
public async Task MethodsThatRequireActiveConnectionWaitForStopAndFailIfConnectionIsCurrentlyStopping(string methodName)
|
||||
{
|
||||
var method = MethodsThatRequireActiveConnection[methodName];
|
||||
|
||||
// Set up StartAsync to wait on the syncPoint when starting
|
||||
var testConnection = new TestConnection(onDispose: SyncPoint.Create(out var syncPoint));
|
||||
await AsyncUsing(new HubConnection(() => testConnection, new JsonHubProtocol()), async connection =>
|
||||
{
|
||||
await connection.StartAsync().OrTimeout();
|
||||
|
||||
// Stop and invoke the method. These two aren't synchronizable via a Sync Point any more because the transport is disposed
|
||||
// outside the lock :(
|
||||
var disposeTask = connection.StopAsync().OrTimeout();
|
||||
var targetTask = method(connection).OrTimeout();
|
||||
|
||||
// Release the sync point
|
||||
syncPoint.Continue();
|
||||
|
||||
// Wait for the method to complete, with an expected error.
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => targetTask);
|
||||
Assert.Equal($"The '{methodName}' method cannot be called if the connection is not active", ex.Message);
|
||||
|
||||
await disposeTask;
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task ForceLastInvocationToComplete(TestConnection testConnection)
|
||||
{
|
||||
// We need to "complete" the invocation
|
||||
var message = await testConnection.ReadSentTextMessageAsync();
|
||||
var json = JObject.Parse(message); // Gotta remove the record separator.
|
||||
await testConnection.ReceiveJsonMessage(new
|
||||
{
|
||||
type = HubProtocolConstants.CompletionMessageType,
|
||||
invocationId = json["invocationId"],
|
||||
});
|
||||
}
|
||||
|
||||
// A helper that we wouldn't want to use in product code, but is fine for testing until IAsyncDisposable arrives :)
|
||||
private static async Task AsyncUsing(HubConnection connection, Func<HubConnection, Task> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
await action(connection);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Dispose isn't under test here, so fire and forget so that errors/timeouts here don't cause
|
||||
// test errors that mask the real errors.
|
||||
_ = connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public partial class HubConnectionTests
|
||||
{
|
||||
public class Extensions
|
||||
{
|
||||
[Fact]
|
||||
public async Task On()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On("Foo",
|
||||
() => tcs.SetResult(new object[0])),
|
||||
new object[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT1()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int>("Foo",
|
||||
r => tcs.SetResult(new object[] {r})),
|
||||
new object[] {42});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT2()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string>("Foo",
|
||||
(r1, r2) => tcs.SetResult(new object[] {r1, r2})),
|
||||
new object[] {42, "abc"});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT3()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float>("Foo",
|
||||
(r1, r2, r3) => tcs.SetResult(new object[] {r1, r2, r3})),
|
||||
new object[] {42, "abc", 24.0f});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT4()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double>("Foo",
|
||||
(r1, r2, r3, r4) => tcs.SetResult(new object[] {r1, r2, r3, r4})),
|
||||
new object[] {42, "abc", 24.0f, 10d});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT5()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double, string>("Foo",
|
||||
(r1, r2, r3, r4, r5) => tcs.SetResult(new object[] {r1, r2, r3, r4, r5})),
|
||||
new object[] {42, "abc", 24.0f, 10d, "123"});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT6()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double, string, byte>("Foo",
|
||||
(r1, r2, r3, r4, r5, r6) => tcs.SetResult(new object[] {r1, r2, r3, r4, r5, r6})),
|
||||
new object[] {42, "abc", 24.0f, 10d, "123", 24});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT7()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double, string, byte, char>("Foo",
|
||||
(r1, r2, r3, r4, r5, r6, r7) => tcs.SetResult(new object[] {r1, r2, r3, r4, r5, r6, r7})),
|
||||
new object[] {42, "abc", 24.0f, 10d, "123", 24, 'c'});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnT8()
|
||||
{
|
||||
await InvokeOn(
|
||||
(hubConnection, tcs) => hubConnection.On<int, string, float, double, string, byte, char, string>("Foo",
|
||||
(r1, r2, r3, r4, r5, r6, r7, r8) => tcs.SetResult(new object[] {r1, r2, r3, r4, r5, r6, r7, r8})),
|
||||
new object[] {42, "abc", 24.0f, 10d, "123", 24, 'c', "XYZ"});
|
||||
}
|
||||
|
||||
private async Task InvokeOn(Action<HubConnection, TaskCompletionSource<object[]>> onAction, object[] args)
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
var handlerTcs = new TaskCompletionSource<object[]>();
|
||||
try
|
||||
{
|
||||
onAction(hubConnection, handlerTcs);
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "1",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = args
|
||||
}).OrTimeout();
|
||||
|
||||
await handlerTcs.Task.OrTimeout();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionNotClosedOnCallbackArgumentCountMismatch()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
var receiveTcs = new TaskCompletionSource<int>();
|
||||
|
||||
try
|
||||
{
|
||||
hubConnection.On<int>("Foo", r => { receiveTcs.SetResult(r); });
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "1",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = new object[] {42, "42"}
|
||||
}).OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "2",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = new object[] {42}
|
||||
}).OrTimeout();
|
||||
|
||||
Assert.Equal(42, await receiveTcs.Task.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionNotClosedOnCallbackArgumentTypeMismatch()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
var receiveTcs = new TaskCompletionSource<int>();
|
||||
|
||||
try
|
||||
{
|
||||
hubConnection.On<int>("Foo", r => { receiveTcs.SetResult(r); });
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "1",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = new object[] {"xxx"}
|
||||
}).OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(
|
||||
new
|
||||
{
|
||||
invocationId = "2",
|
||||
type = 1,
|
||||
target = "Foo",
|
||||
arguments = new object[] {42}
|
||||
}).OrTimeout();
|
||||
|
||||
Assert.Equal(42, await receiveTcs.Task.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public partial class HubConnectionTests
|
||||
{
|
||||
private static HubConnection CreateHubConnection(TestConnection connection, IHubProtocol protocol = null)
|
||||
{
|
||||
return new HubConnection(() => connection, protocol ?? new JsonHubProtocol(), new LoggerFactory());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Internal.Protocol;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
// This includes tests that verify HubConnection conforms to the Hub Protocol, without setting up a full server (even TestServer).
|
||||
// We can also have more control over the messages we send to HubConnection in order to ensure that protocol errors and other quirks
|
||||
// don't cause problems.
|
||||
public partial class HubConnectionTests
|
||||
{
|
||||
public class Protocol
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendAsyncSendsANonBlockingInvocationMessage()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.SendAsync("Foo").OrTimeout();
|
||||
|
||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
||||
Assert.Equal("{\"type\":1,\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClientSendsHandshakeMessageWhenStartingConnection()
|
||||
{
|
||||
var connection = new TestConnection(autoNegotiate: false);
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
// We can't await StartAsync because it depends on the negotiate process!
|
||||
var startTask = hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var handshakeMessage = await connection.ReadHandshakeAndSendResponseAsync().OrTimeout();
|
||||
|
||||
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
||||
Assert.Equal("{\"protocol\":\"json\",\"version\":1}", handshakeMessage);
|
||||
|
||||
await startTask;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeSendsAnInvocationMessage()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout();
|
||||
|
||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
||||
Assert.Equal("{\"type\":1,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReceiveCloseMessageWithoutErrorWillCloseHubConnection()
|
||||
{
|
||||
TaskCompletionSource<Exception> closedTcs = new TaskCompletionSource<Exception>();
|
||||
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
hubConnection.Closed += e => closedTcs.SetResult(e);
|
||||
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {type = 7}).OrTimeout();
|
||||
|
||||
Exception closeException = await closedTcs.Task.OrTimeout();
|
||||
Assert.Null(closeException);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReceiveCloseMessageWithErrorWillCloseHubConnection()
|
||||
{
|
||||
TaskCompletionSource<Exception> closedTcs = new TaskCompletionSource<Exception>();
|
||||
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
hubConnection.Closed += e => closedTcs.SetResult(e);
|
||||
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {type = 7, error = "Error!"}).OrTimeout();
|
||||
|
||||
Exception closeException = await closedTcs.Task.OrTimeout();
|
||||
Assert.NotNull(closeException);
|
||||
Assert.Equal("The server closed the connection with the following error: Error!", closeException.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamSendsAnInvocationMessage()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<object>("Foo").OrTimeout();
|
||||
|
||||
var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout();
|
||||
|
||||
// ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines)
|
||||
Assert.Equal("{\"type\":4,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}", invokeMessage);
|
||||
|
||||
// Complete the channel
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3}).OrTimeout();
|
||||
await channel.Completion;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeCompletedWhenCompletionMessageReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3}).OrTimeout();
|
||||
|
||||
await invokeTask.OrTimeout();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamCompletesWhenCompletionMessageIsReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<int>("Foo").OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3}).OrTimeout();
|
||||
|
||||
Assert.Empty(await channel.ReadAllAsync());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeYieldsResultWhenCompletionMessageReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo").OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3, result = 42}).OrTimeout();
|
||||
|
||||
Assert.Equal(42, await invokeTask.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeFailsWithExceptionWhenCompletionWithErrorReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo").OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3, error = "An error occurred"}).OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<HubException>(() => invokeTask).OrTimeout();
|
||||
Assert.Equal("An error occurred", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamFailsIfCompletionMessageHasPayload()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<string>("Foo").OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3, result = "Oops"}).OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await channel.ReadAllAsync().OrTimeout());
|
||||
Assert.Equal("Server provided a result in a completion response to a streamed invocation.", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamFailsWithExceptionWhenCompletionWithErrorReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<int>("Foo").OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3, error = "An error occurred"}).OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<HubException>(async () => await channel.ReadAllAsync().OrTimeout());
|
||||
Assert.Equal("An error occurred", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeFailsWithErrorWhenStreamingItemReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("Foo").OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 2, item = 42}).OrTimeout();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => invokeTask).OrTimeout();
|
||||
Assert.Equal("Streaming hub methods must be invoked with the 'HubConnection.StreamAsChannelAsync' method.", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamYieldsItemsAsTheyArrive()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var channel = await hubConnection.StreamAsChannelAsync<string>("Foo").OrTimeout();
|
||||
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 2, item = "1"}).OrTimeout();
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 2, item = "2"}).OrTimeout();
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 2, item = "3"}).OrTimeout();
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3}).OrTimeout();
|
||||
|
||||
var notifications = await channel.ReadAllAsync().OrTimeout();
|
||||
|
||||
Assert.Equal(new[] {"1", "2", "3",}, notifications.ToArray());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandlerRegisteredWithOnIsFiredWhenInvocationReceived()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
var handlerCalled = new TaskCompletionSource<object[]>();
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
hubConnection.On<int, string, float>("Foo", (r1, r2, r3) => handlerCalled.TrySetResult(new object[] {r1, r2, r3}));
|
||||
|
||||
var args = new object[] {1, "Foo", 2.0f};
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 1, target = "Foo", arguments = args}).OrTimeout();
|
||||
|
||||
Assert.Equal(args, await handlerCalled.Task.OrTimeout());
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcceptsPingMessages()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection);
|
||||
|
||||
try
|
||||
{
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
// Send an invocation
|
||||
var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout();
|
||||
|
||||
// Receive the ping mid-invocation so we can see that the rest of the flow works fine
|
||||
await connection.ReceiveJsonMessage(new {type = 6}).OrTimeout();
|
||||
|
||||
// Receive a completion
|
||||
await connection.ReceiveJsonMessage(new {invocationId = "1", type = 3}).OrTimeout();
|
||||
|
||||
// Ensure the invokeTask completes properly
|
||||
await invokeTask.OrTimeout();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await connection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,44 +16,17 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public class HubConnectionTests
|
||||
public partial class HubConnectionTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StartAsyncCallsConnectionStart()
|
||||
{
|
||||
var connection = new Mock<IConnection>();
|
||||
var protocol = new Mock<IHubProtocol>();
|
||||
protocol.SetupGet(p => p.TransferFormat).Returns(TransferFormat.Text);
|
||||
connection.SetupGet(p => p.Features).Returns(new FeatureCollection());
|
||||
connection.Setup(m => m.StartAsync(TransferFormat.Text)).Returns(Task.CompletedTask).Verifiable();
|
||||
var hubConnection = new HubConnection(connection.Object, protocol.Object, null);
|
||||
await hubConnection.StartAsync();
|
||||
|
||||
connection.Verify(c => c.StartAsync(TransferFormat.Text), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsyncCallsConnectionStart()
|
||||
{
|
||||
var connection = new Mock<IConnection>();
|
||||
connection.Setup(m => m.Features).Returns(new FeatureCollection());
|
||||
connection.Setup(m => m.StartAsync(TransferFormat.Text)).Verifiable();
|
||||
var hubConnection = new HubConnection(connection.Object, Mock.Of<IHubProtocol>(), null);
|
||||
await hubConnection.DisposeAsync();
|
||||
|
||||
connection.Verify(c => c.DisposeAsync(), Times.Once());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeThrowsIfSerializingMessageFails()
|
||||
{
|
||||
var exception = new InvalidOperationException();
|
||||
var mockProtocol = MockHubProtocol.Throw(exception);
|
||||
var hubConnection = new HubConnection(new TestConnection(), mockProtocol, null);
|
||||
await hubConnection.StartAsync();
|
||||
var hubConnection = CreateHubConnection(new TestConnection(), protocol: MockHubProtocol.Throw(exception));
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var actualException =
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await hubConnection.InvokeAsync<int>("test"));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await hubConnection.InvokeAsync<int>("test").OrTimeout());
|
||||
Assert.Same(exception, actualException);
|
||||
}
|
||||
|
||||
|
|
@ -61,133 +34,49 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
public async Task SendAsyncThrowsIfSerializingMessageFails()
|
||||
{
|
||||
var exception = new InvalidOperationException();
|
||||
var mockProtocol = MockHubProtocol.Throw(exception);
|
||||
var hubConnection = new HubConnection(new TestConnection(), mockProtocol, null);
|
||||
await hubConnection.StartAsync();
|
||||
var hubConnection = CreateHubConnection(new TestConnection(), protocol: MockHubProtocol.Throw(exception));
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
var actualException =
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await hubConnection.SendAsync("test"));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await hubConnection.SendAsync("test").OrTimeout());
|
||||
Assert.Same(exception, actualException);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClosedEventRaisedWhenTheClientIsStopped()
|
||||
{
|
||||
var hubConnection = new HubConnection(new TestConnection(), Mock.Of<IHubProtocol>(), null);
|
||||
var hubConnection = new HubConnection(() => new TestConnection(), Mock.Of<IHubProtocol>(), null);
|
||||
var closedEventTcs = new TaskCompletionSource<Exception>();
|
||||
hubConnection.Closed += e => closedEventTcs.SetResult(e);
|
||||
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
await hubConnection.StopAsync().OrTimeout();
|
||||
Assert.Null(await closedEventTcs.Task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotCallInvokeOnNotStartedHubConnection()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => hubConnection.InvokeAsync<int>("test"));
|
||||
|
||||
Assert.Equal("The 'InvokeAsync' method cannot be called before the connection has been started.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotCallInvokeOnClosedHubConnection()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
await hubConnection.StartAsync();
|
||||
await hubConnection.DisposeAsync();
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => hubConnection.InvokeAsync<int>("test"));
|
||||
|
||||
Assert.Equal("Connection has been terminated.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotCallSendOnNotStartedHubConnection()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => hubConnection.SendAsync("test"));
|
||||
|
||||
Assert.Equal("The 'SendAsync' method cannot be called before the connection has been started.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotCallSendOnClosedHubConnection()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
await hubConnection.StartAsync();
|
||||
await hubConnection.DisposeAsync();
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => hubConnection.SendAsync("test"));
|
||||
|
||||
Assert.Equal("Connection has been terminated.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotCallStreamOnClosedHubConnection()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
await hubConnection.StartAsync();
|
||||
await hubConnection.DisposeAsync();
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => hubConnection.StreamAsChannelAsync<int>("test"));
|
||||
|
||||
Assert.Equal("Connection has been terminated.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotCallStreamOnNotStartedHubConnection()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => hubConnection.StreamAsChannelAsync<int>("test"));
|
||||
|
||||
Assert.Equal("The 'StreamAsChannelAsync' method cannot be called before the connection has been started.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PendingInvocationsAreCancelledWhenConnectionClosesCleanly()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
var hubConnection = CreateHubConnection(new TestConnection());
|
||||
|
||||
await hubConnection.StartAsync();
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("testMethod");
|
||||
await hubConnection.DisposeAsync();
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("testMethod").OrTimeout();
|
||||
await hubConnection.StopAsync().OrTimeout();
|
||||
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(async () => await invokeTask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PendingInvocationsAreTerminatedWithExceptionWhenConnectionClosesDueToError()
|
||||
public async Task PendingInvocationsAreTerminatedWithExceptionWhenTransportCompletesWithError()
|
||||
{
|
||||
var mockConnection = new Mock<IConnection>();
|
||||
mockConnection.SetupGet(p => p.Features).Returns(new FeatureCollection());
|
||||
mockConnection
|
||||
.Setup(m => m.DisposeAsync())
|
||||
.Returns(Task.FromResult<object>(null));
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = CreateHubConnection(connection, protocol: Mock.Of<IHubProtocol>());
|
||||
|
||||
var hubConnection = new HubConnection(mockConnection.Object, Mock.Of<IHubProtocol>(), new LoggerFactory());
|
||||
|
||||
await hubConnection.StartAsync();
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("testMethod");
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
var invokeTask = hubConnection.InvokeAsync<int>("testMethod").OrTimeout();
|
||||
|
||||
var exception = new InvalidOperationException();
|
||||
mockConnection.Raise(m => m.Closed += null, exception);
|
||||
connection.CompleteFromTransport(exception);
|
||||
|
||||
var actualException = await Assert.ThrowsAsync<InvalidOperationException>(async () => await invokeTask);
|
||||
Assert.Equal(exception, actualException);
|
||||
|
|
@ -196,9 +85,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
[Fact]
|
||||
public async Task ConnectionTerminatedIfServerTimeoutIntervalElapsesWithNoMessages()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
|
||||
var hubConnection = CreateHubConnection(new TestConnection());
|
||||
hubConnection.ServerTimeout = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
var closeTcs = new TaskCompletionSource<Exception>();
|
||||
|
|
@ -211,18 +98,18 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnReceivedAfterTimerDisposedDoesNotThrow()
|
||||
public async Task PendingInvocationsAreTerminatedIfServerTimeoutIntervalElapsesWithNoMessages()
|
||||
{
|
||||
var connection = new TestConnection();
|
||||
var hubConnection = new HubConnection(connection, new JsonHubProtocol(), new LoggerFactory());
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
var hubConnection = CreateHubConnection(new TestConnection());
|
||||
hubConnection.ServerTimeout = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
// Fire callbacks, they shouldn't fail
|
||||
foreach (var registration in connection.Callbacks)
|
||||
{
|
||||
await registration.InvokeAsync(new byte[0]);
|
||||
}
|
||||
await hubConnection.StartAsync().OrTimeout();
|
||||
|
||||
// Start an invocation (but we won't complete it)
|
||||
var invokeTask = hubConnection.InvokeAsync("Method").OrTimeout();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<TimeoutException>(() => invokeTask);
|
||||
Assert.Equal("Server timeout (500.00ms) elapsed without receiving a message from the server.", exception.Message);
|
||||
}
|
||||
|
||||
// Moq really doesn't handle out parameters well, so to make these tests work I added a manual mock -anurse
|
||||
|
|
@ -231,9 +118,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
private HubInvocationMessage _parsed;
|
||||
private Exception _error;
|
||||
|
||||
public int ParseCalls { get; private set; } = 0;
|
||||
public int WriteCalls { get; private set; } = 0;
|
||||
|
||||
public static MockHubProtocol ReturnOnParse(HubInvocationMessage parsed)
|
||||
{
|
||||
return new MockHubProtocol
|
||||
|
|
@ -262,7 +146,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
public bool TryParseMessages(ReadOnlyMemory<byte> input, IInvocationBinder binder, IList<HubMessage> messages)
|
||||
{
|
||||
ParseCalls += 1;
|
||||
if (_error != null)
|
||||
{
|
||||
throw _error;
|
||||
|
|
@ -278,8 +161,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
public void WriteMessage(HubMessage message, Stream output)
|
||||
{
|
||||
WriteCalls += 1;
|
||||
|
||||
if (_error != null)
|
||||
{
|
||||
throw _error;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
|
@ -13,16 +12,15 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Client.Tests;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.SignalR.Client.Tests;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Client.Tests
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
public class LongPollingTransportTests
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
|
|
@ -17,6 +16,7 @@ using Microsoft.AspNetCore.Connections;
|
|||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using Xunit;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||
{
|
||||
// Possibly useful as a general-purpose async testing helper?
|
||||
public class SyncPoint
|
||||
{
|
||||
private readonly TaskCompletionSource<object> _atSyncPoint = new TaskCompletionSource<object>();
|
||||
private readonly TaskCompletionSource<object> _continueFromSyncPoint = new TaskCompletionSource<object>();
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the code-under-test to reach <see cref="WaitToContinue"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task WaitForSyncPoint() => _atSyncPoint.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the code-under-test to continue past where it waited for <see cref="WaitToContinue"/>.
|
||||
/// </summary>
|
||||
public void Continue() => _continueFromSyncPoint.TrySetResult(null);
|
||||
|
||||
/// <summary>
|
||||
/// Used by the code-under-test to wait for the test code to sync up.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This code will unblock <see cref="WaitForSyncPoint"/> and then block waiting for <see cref="Continue"/> to be called.
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
public Task WaitToContinue()
|
||||
{
|
||||
_atSyncPoint.TrySetResult(null);
|
||||
return _continueFromSyncPoint.Task;
|
||||
}
|
||||
|
||||
public static Func<Task> Create(out SyncPoint syncPoint)
|
||||
{
|
||||
var handler = Create(1, out var syncPoints);
|
||||
syncPoint = syncPoints[0];
|
||||
return handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a re-entrant function that waits for sync points in sequence.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of sync points to expect</param>
|
||||
/// <param name="syncPoints">The <see cref="SyncPoint"/> objects that can be used to coordinate the sync point</param>
|
||||
/// <returns></returns>
|
||||
public static Func<Task> Create(int count, out SyncPoint[] syncPoints)
|
||||
{
|
||||
// Need to use a local so the closure can capture it. You can't use out vars in a closure.
|
||||
var localSyncPoints = new SyncPoint[count];
|
||||
for (var i = 0; i < count; i += 1)
|
||||
{
|
||||
localSyncPoints[i] = new SyncPoint();
|
||||
}
|
||||
|
||||
syncPoints = localSyncPoints;
|
||||
|
||||
var counter = 0;
|
||||
return () =>
|
||||
{
|
||||
if (counter >= localSyncPoints.Length)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
var syncPoint = localSyncPoints[counter];
|
||||
|
||||
counter += 1;
|
||||
return syncPoint.WaitToContinue();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Client.Tests
|
||||
{
|
||||
public class TaskQueueTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task DrainingTaskQueueShutsQueueOff()
|
||||
{
|
||||
var queue = new TaskQueue();
|
||||
await queue.Enqueue(() => Task.CompletedTask);
|
||||
await queue.Drain();
|
||||
|
||||
// This would throw if the task was queued successfully
|
||||
await queue.Enqueue(() => Task.FromException(new Exception()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TaskQueueDoesNotQueueNewTasksIfPreviousTaskFaulted()
|
||||
{
|
||||
var exception = new Exception();
|
||||
var queue = new TaskQueue();
|
||||
var ignore = queue.Enqueue(() => Task.FromException(exception));
|
||||
var task = queue.Enqueue(() => Task.CompletedTask);
|
||||
|
||||
var actual = await Assert.ThrowsAsync<Exception>(async () => await task);
|
||||
Assert.Same(exception, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskQueueRunsTasksInSequence()
|
||||
{
|
||||
var queue = new TaskQueue();
|
||||
int n = 0;
|
||||
queue.Enqueue(() =>
|
||||
{
|
||||
n = 1;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
Task task = queue.Enqueue(() =>
|
||||
{
|
||||
return Task.Delay(100).ContinueWith(t => n = 2);
|
||||
});
|
||||
|
||||
task.Wait();
|
||||
Assert.Equal(2, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
|
|
@ -19,83 +19,64 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
{
|
||||
internal class TestConnection : IConnection
|
||||
{
|
||||
private TaskCompletionSource<object> _started = new TaskCompletionSource<object>();
|
||||
private TaskCompletionSource<object> _disposed = new TaskCompletionSource<object>();
|
||||
private readonly bool _autoNegotiate;
|
||||
private readonly TaskCompletionSource<object> _started = new TaskCompletionSource<object>();
|
||||
private readonly TaskCompletionSource<object> _disposed = new TaskCompletionSource<object>();
|
||||
|
||||
private Channel<byte[]> _sentMessages = Channel.CreateUnbounded<byte[]>();
|
||||
private Channel<byte[]> _receivedMessages = Channel.CreateUnbounded<byte[]>();
|
||||
private int _disposeCount = 0;
|
||||
|
||||
private CancellationTokenSource _receiveShutdownToken = new CancellationTokenSource();
|
||||
private Task _receiveLoop;
|
||||
|
||||
public event Action<Exception> Closed;
|
||||
public Task Started => _started.Task;
|
||||
public Task Disposed => _disposed.Task;
|
||||
public ChannelReader<byte[]> SentMessages => _sentMessages.Reader;
|
||||
public ChannelWriter<byte[]> ReceivedMessages => _receivedMessages.Writer;
|
||||
|
||||
private bool _closed;
|
||||
private object _closedLock = new object();
|
||||
private readonly Func<Task> _onStart;
|
||||
private readonly Func<Task> _onDispose;
|
||||
|
||||
public List<ReceiveCallback> Callbacks { get; } = new List<ReceiveCallback>();
|
||||
public IDuplexPipe Application { get; }
|
||||
public IDuplexPipe Transport { get; }
|
||||
|
||||
public IFeatureCollection Features { get; } = new FeatureCollection();
|
||||
public int DisposeCount => _disposeCount;
|
||||
|
||||
public TestConnection()
|
||||
public TestConnection(Func<Task> onStart = null, Func<Task> onDispose = null, bool autoNegotiate = true)
|
||||
{
|
||||
_receiveLoop = ReceiveLoopAsync(_receiveShutdownToken.Token);
|
||||
_autoNegotiate = autoNegotiate;
|
||||
_onStart = onStart ?? (() => Task.CompletedTask);
|
||||
_onDispose = onDispose ?? (() => Task.CompletedTask);
|
||||
|
||||
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
|
||||
Application = pair.Application;
|
||||
Transport = pair.Transport;
|
||||
|
||||
Application.Input.OnWriterCompleted((ex, _) => Application.Output.Complete(), null);
|
||||
}
|
||||
|
||||
public Task AbortAsync(Exception ex) => DisposeCoreAsync(ex);
|
||||
public Task DisposeAsync() => DisposeCoreAsync();
|
||||
|
||||
// TestConnection isn't restartable
|
||||
public Task StopAsync() => DisposeAsync();
|
||||
public Task StartAsync() => StartAsync(TransferFormat.Binary);
|
||||
|
||||
private Task DisposeCoreAsync(Exception ex = null)
|
||||
{
|
||||
TriggerClosed(ex);
|
||||
_receiveShutdownToken.Cancel();
|
||||
return _receiveLoop;
|
||||
}
|
||||
|
||||
public async Task SendAsync(byte[] data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_started.Task.IsCompleted)
|
||||
{
|
||||
throw new InvalidOperationException("Connection must be started before SendAsync can be called");
|
||||
}
|
||||
|
||||
while (await _sentMessages.Writer.WaitToWriteAsync(cancellationToken))
|
||||
{
|
||||
if (_sentMessages.Writer.TryWrite(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new ObjectDisposedException("Unable to send message, underlying channel was closed");
|
||||
}
|
||||
|
||||
public Task StartAsync(TransferFormat transferFormat)
|
||||
public async Task StartAsync(TransferFormat transferFormat)
|
||||
{
|
||||
_started.TrySetResult(null);
|
||||
return Task.CompletedTask;
|
||||
|
||||
await _onStart();
|
||||
|
||||
if (_autoNegotiate)
|
||||
{
|
||||
// We can't await this as it will block StartAsync which will block
|
||||
// HubConnection.StartAsync which sends the Handshake in the first place!
|
||||
_ = ReadHandshakeAndSendResponseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReadHandshakeAndSendResponseAsync()
|
||||
public async Task<string> ReadHandshakeAndSendResponseAsync()
|
||||
{
|
||||
await SentMessages.ReadAsync();
|
||||
var s = await ReadSentTextMessageAsync();
|
||||
|
||||
var output = new MemoryStream();
|
||||
HandshakeProtocol.WriteResponseMessage(HandshakeResponseMessage.Empty, output);
|
||||
await Application.Output.WriteAsync(output.ToArray());
|
||||
|
||||
await _receivedMessages.Writer.WriteAsync(output.ToArray());
|
||||
}
|
||||
|
||||
public async Task<string> ReadSentTextMessageAsync()
|
||||
{
|
||||
var message = await SentMessages.ReadAsync();
|
||||
return Encoding.UTF8.GetString(message);
|
||||
return s;
|
||||
}
|
||||
|
||||
public Task ReceiveJsonMessage(object jsonObject)
|
||||
|
|
@ -103,7 +84,51 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
var json = JsonConvert.SerializeObject(jsonObject, Formatting.None);
|
||||
var bytes = FormatMessageToArray(Encoding.UTF8.GetBytes(json));
|
||||
|
||||
return _receivedMessages.Writer.WriteAsync(bytes).AsTask();
|
||||
return Application.Output.WriteAsync(bytes).AsTask();
|
||||
}
|
||||
|
||||
public async Task<string> ReadSentTextMessageAsync()
|
||||
{
|
||||
// Read a single text message from the Application Input pipe
|
||||
while (true)
|
||||
{
|
||||
var result = await Application.Input.ReadAsync();
|
||||
var buffer = result.Buffer;
|
||||
var consumed = buffer.Start;
|
||||
|
||||
try
|
||||
{
|
||||
if (TextMessageParser.TryParseMessage(ref buffer, out var payload))
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
return Encoding.UTF8.GetString(payload.ToArray());
|
||||
}
|
||||
else if (result.IsCompleted)
|
||||
{
|
||||
throw new InvalidOperationException("Out of data!");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Application.Input.AdvanceTo(consumed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CompleteFromTransport(Exception ex = null)
|
||||
{
|
||||
Application.Output.Complete(ex);
|
||||
}
|
||||
|
||||
private async Task DisposeCoreAsync(Exception ex = null)
|
||||
{
|
||||
Interlocked.Increment(ref _disposeCount);
|
||||
_disposed.TrySetResult(null);
|
||||
await _onDispose();
|
||||
|
||||
// Simulate HttpConnection's behavior by Completing the Transport pipe.
|
||||
Transport.Input.Complete();
|
||||
Transport.Output.Complete();
|
||||
}
|
||||
|
||||
private byte[] FormatMessageToArray(byte[] message)
|
||||
|
|
@ -113,99 +138,6 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
TextMessageFormatter.WriteRecordSeparator(output);
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
while (await _receivedMessages.Reader.WaitToReadAsync(token))
|
||||
{
|
||||
while (_receivedMessages.Reader.TryRead(out var message))
|
||||
{
|
||||
ReceiveCallback[] callbackCopies;
|
||||
lock (Callbacks)
|
||||
{
|
||||
callbackCopies = Callbacks.ToArray();
|
||||
}
|
||||
|
||||
foreach (var callback in callbackCopies)
|
||||
{
|
||||
await callback.InvokeAsync(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TriggerClosed();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Do nothing, we were just asked to shut down.
|
||||
TriggerClosed();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TriggerClosed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerClosed(Exception ex = null)
|
||||
{
|
||||
lock (_closedLock)
|
||||
{
|
||||
if (!_closed)
|
||||
{
|
||||
_closed = true;
|
||||
Closed?.Invoke(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable OnReceived(Func<byte[], object, Task> callback, object state)
|
||||
{
|
||||
var receiveCallBack = new ReceiveCallback(callback, state);
|
||||
lock (Callbacks)
|
||||
{
|
||||
Callbacks.Add(receiveCallBack);
|
||||
}
|
||||
return new Subscription(receiveCallBack, Callbacks);
|
||||
}
|
||||
|
||||
public class ReceiveCallback
|
||||
{
|
||||
private readonly Func<byte[], object, Task> _callback;
|
||||
private readonly object _state;
|
||||
|
||||
public ReceiveCallback(Func<byte[], object, Task> callback, object state)
|
||||
{
|
||||
_callback = callback;
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public Task InvokeAsync(byte[] data)
|
||||
{
|
||||
return _callback(data, _state);
|
||||
}
|
||||
}
|
||||
|
||||
private class Subscription : IDisposable
|
||||
{
|
||||
private readonly ReceiveCallback _callback;
|
||||
private readonly List<ReceiveCallback> _callbacks;
|
||||
public Subscription(ReceiveCallback callback, List<ReceiveCallback> callbacks)
|
||||
{
|
||||
_callback = callback;
|
||||
_callbacks = callbacks;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_callbacks)
|
||||
{
|
||||
_callbacks.Remove(_callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
return await _handler(request, cancellationToken);
|
||||
}
|
||||
|
||||
public static HttpMessageHandler CreateDefault()
|
||||
public static TestHttpMessageHandler CreateDefault()
|
||||
{
|
||||
var testHttpMessageHandler = new TestHttpMessageHandler();
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
|
||||
public TransferFormat? Format { get; }
|
||||
public IDuplexPipe Application { get; private set; }
|
||||
public Task Receiving { get; private set; }
|
||||
|
||||
public TestTransport(Func<Task> onTransportStop = null, Func<Task> onTransportStart = null, TransferFormat transferFormat = TransferFormat.Text)
|
||||
{
|
||||
|
|
@ -22,20 +23,51 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
|||
Format = transferFormat;
|
||||
}
|
||||
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
|
||||
public async Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
|
||||
{
|
||||
if ((Format & transferFormat) == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"The '{transferFormat}' transfer format is not supported by this transport.");
|
||||
}
|
||||
Application = application;
|
||||
return _startHandler();
|
||||
await _startHandler();
|
||||
|
||||
// Start a loop to read from the pipe
|
||||
Receiving = ReceiveLoop();
|
||||
async Task ReceiveLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var result = await Application.Input.ReadAsync();
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (result.IsCanceled)
|
||||
{
|
||||
// This is useful for detecting that the connection tried to gracefully terminate.
|
||||
// If the Receiving task is faulted/cancelled, it means StopAsync was the thing that
|
||||
// actually terminated the connection (not ideal, we want the transport pipe to
|
||||
// shut down gracefully)
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
|
||||
Application.Input.AdvanceTo(result.Buffer.End);
|
||||
}
|
||||
|
||||
// Call the transport stop handler
|
||||
await _stopHandler();
|
||||
|
||||
// Complete our end of the pipe
|
||||
Application.Output.Complete();
|
||||
Application.Input.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
public Task StopAsync()
|
||||
{
|
||||
await _stopHandler();
|
||||
Application.Output.Complete();
|
||||
Application.Input.CancelPendingRead();
|
||||
return Receiving;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,24 +3,35 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace System.Threading.Channels
|
||||
{
|
||||
public static class ChannelExtensions
|
||||
{
|
||||
public static async Task<List<T>> ReadAllAsync<T>(this ChannelReader<T> channel)
|
||||
public static async Task<List<T>> ReadAllAsync<T>(this ChannelReader<T> channel, bool suppressExceptions = false)
|
||||
{
|
||||
var list = new List<T>();
|
||||
while (await channel.WaitToReadAsync())
|
||||
try
|
||||
{
|
||||
while (channel.TryRead(out var item))
|
||||
while (await channel.WaitToReadAsync())
|
||||
{
|
||||
list.Add(item);
|
||||
while (channel.TryRead(out var item))
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Manifest any error from channel.Completion (which should be completed now)
|
||||
if (!suppressExceptions)
|
||||
{
|
||||
await channel.Completion;
|
||||
}
|
||||
}
|
||||
|
||||
// Manifest any error from channel.Completion (which should be completed now)
|
||||
await channel.Completion;
|
||||
catch (Exception) when (suppressExceptions)
|
||||
{
|
||||
// Suppress the exception
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,4 +21,8 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System.IO.Pipelines
|
||||
{
|
||||
public static class PipeCompletionExtensions
|
||||
{
|
||||
public static Task WaitForWriterToComplete(this PipeReader reader)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
reader.OnWriterCompleted((ex, state) =>
|
||||
{
|
||||
if (ex != null)
|
||||
{
|
||||
((TaskCompletionSource<object>)state).TrySetException(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
((TaskCompletionSource<object>)state).TrySetResult(null);
|
||||
}
|
||||
}, tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public static Task WaitForReaderToComplete(this PipeWriter writer)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
writer.OnReaderCompleted((ex, state) =>
|
||||
{
|
||||
if (ex != null)
|
||||
{
|
||||
((TaskCompletionSource<object>)state).TrySetException(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
((TaskCompletionSource<object>)state).TrySetResult(null);
|
||||
}
|
||||
}, tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@ namespace System.IO.Pipelines
|
|||
pipeReader.AdvanceTo(result.Buffer.Start, result.Buffer.End);
|
||||
continue;
|
||||
}
|
||||
|
||||
pipeReader.AdvanceTo(result.Buffer.GetPosition(numBytes));
|
||||
break;
|
||||
}
|
||||
|
|
@ -72,19 +73,14 @@ namespace System.IO.Pipelines
|
|||
{
|
||||
var result = await pipeReader.ReadAsync();
|
||||
|
||||
try
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
return result.Buffer.ToArray();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Consume nothing, just wait for everything
|
||||
pipeReader.AdvanceTo(result.Buffer.Start, result.Buffer.End);
|
||||
return result.Buffer.ToArray();
|
||||
}
|
||||
|
||||
// Consume nothing, just wait for everything
|
||||
pipeReader.AdvanceTo(result.Buffer.Start, result.Buffer.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,12 +33,25 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
public string Url { get; private set; }
|
||||
|
||||
public ServerFixture()
|
||||
public ServerFixture() : this(loggerFactory: null)
|
||||
{
|
||||
}
|
||||
|
||||
public ServerFixture(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logSinkProvider = new LogSinkProvider();
|
||||
|
||||
var testLog = AssemblyTestLog.ForAssembly(typeof(TStartup).Assembly);
|
||||
_logToken = testLog.StartTestLog(null, $"{nameof(ServerFixture<TStartup>)}_{typeof(TStartup).Name}", out _loggerFactory, "ServerFixture");
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
var testLog = AssemblyTestLog.ForAssembly(typeof(TStartup).Assembly);
|
||||
_logToken = testLog.StartTestLog(null, $"{nameof(ServerFixture<TStartup>)}_{typeof(TStartup).Name}",
|
||||
out _loggerFactory, "ServerFixture");
|
||||
}
|
||||
else
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
_logger = _loggerFactory.CreateLogger<ServerFixture<TStartup>>();
|
||||
|
||||
StartServer();
|
||||
|
|
@ -51,6 +64,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
_host = new WebHostBuilder()
|
||||
.ConfigureLogging(builder => builder
|
||||
.SetMinimumLevel(LogLevel.Debug)
|
||||
.AddProvider(_logSinkProvider)
|
||||
.AddProvider(new ForwardingLoggerProvider(_loggerFactory)))
|
||||
.UseStartup(typeof(TStartup))
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ namespace System.Threading.Tasks
|
|||
await task;
|
||||
}
|
||||
|
||||
public static Task<T> OrTimeout<T>(this ValueTask<T> task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) =>
|
||||
OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber);
|
||||
|
||||
public static Task<T> OrTimeout<T>(this ValueTask<T> task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) =>
|
||||
task.AsTask().OrTimeout(timeout, memberName, filePath, lineNumber);
|
||||
|
||||
public static Task<T> OrTimeout<T>(this Task<T> task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null)
|
||||
{
|
||||
return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber);
|
||||
|
|
@ -61,7 +67,7 @@ namespace System.Threading.Tasks
|
|||
public static async Task OrThrowIfOtherFails(this Task task, Task otherTask)
|
||||
{
|
||||
var completed = await Task.WhenAny(task, otherTask);
|
||||
if(completed == otherTask && otherTask.IsFaulted)
|
||||
if (completed == otherTask && otherTask.IsFaulted)
|
||||
{
|
||||
// Manifest the exception
|
||||
otherTask.GetAwaiter().GetResult();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Net.Http;
|
||||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
[ConditionalFact]
|
||||
[WebSocketsSupportedCondition]
|
||||
public async Task HTTPRequestsNotSentWhenWebSocketsTransportRequested()
|
||||
public async Task HttpRequestsNotSentWhenWebSocketsTransportRequested()
|
||||
{
|
||||
using (StartLog(out var loggerFactory))
|
||||
{
|
||||
|
|
@ -136,19 +136,12 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
try
|
||||
{
|
||||
var receiveTcs = new TaskCompletionSource<byte[]>();
|
||||
connection.OnReceived((data, state) =>
|
||||
{
|
||||
var tcs = (TaskCompletionSource<byte[]>)state;
|
||||
tcs.TrySetResult(data);
|
||||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
var message = new byte[] { 42 };
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
await connection.SendAsync(message).OrTimeout();
|
||||
|
||||
var receivedData = await receiveTcs.Task.OrTimeout();
|
||||
await connection.Transport.Output.WriteAsync(message).OrTimeout();
|
||||
|
||||
var receivedData = await connection.Transport.Input.ReadAllAsync();
|
||||
Assert.Equal(message, receivedData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -179,28 +172,6 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
var connection = new HttpConnection(new Uri(url), transportType, loggerFactory);
|
||||
try
|
||||
{
|
||||
var closeTcs = new TaskCompletionSource<object>();
|
||||
connection.Closed += e =>
|
||||
{
|
||||
if (e != null)
|
||||
{
|
||||
closeTcs.SetException(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
closeTcs.SetResult(null);
|
||||
}
|
||||
};
|
||||
|
||||
var receiveTcs = new TaskCompletionSource<string>();
|
||||
connection.OnReceived((data, state) =>
|
||||
{
|
||||
logger.LogInformation("Received {length} byte message", data.Length);
|
||||
var tcs = (TaskCompletionSource<string>)state;
|
||||
tcs.TrySetResult(Encoding.UTF8.GetString(data));
|
||||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
logger.LogInformation("Starting connection to {url}", url);
|
||||
await connection.StartAsync(requestedTransferFormat).OrTimeout();
|
||||
logger.LogInformation("Started connection to {url}", url);
|
||||
|
|
@ -210,7 +181,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
logger.LogInformation("Sending {length} byte message", bytes.Length);
|
||||
try
|
||||
{
|
||||
await connection.SendAsync(bytes).OrTimeout();
|
||||
await connection.Transport.Output.WriteAsync(bytes).OrTimeout();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
|
@ -220,12 +191,11 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
// Our solution to this is to just catch OperationCanceledException from the sent message if the race happens
|
||||
// because we know the send went through, and its safe to check the response.
|
||||
}
|
||||
logger.LogInformation("Sent message", bytes.Length);
|
||||
logger.LogInformation("Sent message");
|
||||
|
||||
logger.LogInformation("Receiving message");
|
||||
Assert.Equal(message, await receiveTcs.Task.OrTimeout());
|
||||
Assert.Equal(message, Encoding.UTF8.GetString(await connection.Transport.Input.ReadAllAsync()));
|
||||
logger.LogInformation("Completed receive");
|
||||
await closeTcs.Task.OrTimeout();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -264,27 +234,18 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
try
|
||||
{
|
||||
var receiveTcs = new TaskCompletionSource<byte[]>();
|
||||
connection.OnReceived((data, state) =>
|
||||
{
|
||||
logger.LogInformation("Received {length} byte message", data.Length);
|
||||
var tcs = (TaskCompletionSource<byte[]>)state;
|
||||
tcs.TrySetResult(data);
|
||||
return Task.CompletedTask;
|
||||
}, receiveTcs);
|
||||
|
||||
logger.LogInformation("Starting connection to {url}", url);
|
||||
await connection.StartAsync(TransferFormat.Binary).OrTimeout();
|
||||
logger.LogInformation("Started connection to {url}", url);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
logger.LogInformation("Sending {length} byte message", bytes.Length);
|
||||
await connection.SendAsync(bytes).OrTimeout();
|
||||
logger.LogInformation("Sent message", bytes.Length);
|
||||
await connection.Transport.Output.WriteAsync(bytes).OrTimeout();
|
||||
logger.LogInformation("Sent message");
|
||||
|
||||
logger.LogInformation("Receiving message");
|
||||
// Big timeout here because it can take a while to receive all the bytes
|
||||
var receivedData = await receiveTcs.Task.OrTimeout(TimeSpan.FromSeconds(30));
|
||||
var receivedData = await connection.Transport.Input.ReadAllAsync();
|
||||
Assert.Equal(message, Encoding.UTF8.GetString(receivedData));
|
||||
logger.LogInformation("Completed receive");
|
||||
}
|
||||
|
|
@ -406,26 +367,26 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
private class FakeTransport : ITransport
|
||||
{
|
||||
public string prevConnectionId = null;
|
||||
private int tries = 0;
|
||||
private int _tries;
|
||||
private string _prevConnectionId = null;
|
||||
private IDuplexPipe _application;
|
||||
|
||||
public Task StartAsync(Uri url, IDuplexPipe application, TransferFormat transferFormat, IConnection connection)
|
||||
{
|
||||
_application = application;
|
||||
tries++;
|
||||
Assert.True(QueryHelpers.ParseQuery(url.Query.ToString()).TryGetValue("id", out var id));
|
||||
if (prevConnectionId == null)
|
||||
_tries++;
|
||||
Assert.True(QueryHelpers.ParseQuery(url.Query).TryGetValue("id", out var id));
|
||||
if (_prevConnectionId == null)
|
||||
{
|
||||
prevConnectionId = id;
|
||||
_prevConnectionId = id;
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(prevConnectionId != id);
|
||||
prevConnectionId = id;
|
||||
Assert.True(_prevConnectionId != id);
|
||||
_prevConnectionId = id;
|
||||
}
|
||||
|
||||
if (tries < 3)
|
||||
if (_tries < 3)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
|
@ -462,11 +423,11 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
{
|
||||
foreach (var transport in TransportTypes)
|
||||
{
|
||||
yield return new object[] { transport[0], TransferFormat.Text };
|
||||
yield return new[] { transport[0], TransferFormat.Text };
|
||||
|
||||
if ((TransportType)transport[0] != TransportType.ServerSentEvents)
|
||||
{
|
||||
yield return new object[] { transport[0], TransferFormat.Binary };
|
||||
yield return new[] { transport[0], TransferFormat.Binary };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Connections;
|
|||
using Microsoft.AspNetCore.Sockets;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||
using Microsoft.AspNetCore.Sockets.Client.Internal;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Moq;
|
||||
|
|
|
|||
Loading…
Reference in New Issue