parent
75e102f97a
commit
66ab939cff
|
|
@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DA69F624-539
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{83B2C3EB-A3D8-4E6F-9A3C-A380B005EF31}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{83B2C3EB-A3D8-4E6F-9A3C-A380B005EF31}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
build\dependencies.props = build\dependencies.props
|
||||||
Directory.Build.props = Directory.Build.props
|
Directory.Build.props = Directory.Build.props
|
||||||
Directory.Build.targets = Directory.Build.targets
|
Directory.Build.targets = Directory.Build.targets
|
||||||
build\Key.snk = build\Key.snk
|
build\Key.snk = build\Key.snk
|
||||||
|
|
@ -85,7 +86,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtSample", "samples\JwtSam
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtClientSample", "samples\JwtClientSample\JwtClientSample.csproj", "{1A953296-E869-4DE2-A693-FD5FCDE27057}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtClientSample", "samples\JwtClientSample\JwtClientSample.csproj", "{1A953296-E869-4DE2-A693-FD5FCDE27057}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SignalR.Tests.Utils", "test\Microsoft.AspNetCore.SignalR.Tests.Utils\Microsoft.AspNetCore.SignalR.Tests.Utils.csproj", "{0A0A6135-EA24-4307-95C2-CE1B7E164A5E}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Tests.Utils", "test\Microsoft.AspNetCore.SignalR.Tests.Utils\Microsoft.AspNetCore.SignalR.Tests.Utils.csproj", "{0A0A6135-EA24-4307-95C2-CE1B7E164A5E}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,16 @@
|
||||||
"integrity": "sha512-zT+t9841g1HsjLtPMCYxmb1U4pcZ2TOegAKiomlmj6bIziuaEYHUavxLE9NRwdntY0vOCrgHho6OXjDX7fm/Kw==",
|
"integrity": "sha512-zT+t9841g1HsjLtPMCYxmb1U4pcZ2TOegAKiomlmj6bIziuaEYHUavxLE9NRwdntY0vOCrgHho6OXjDX7fm/Kw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"JSONStream": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
|
||||||
|
"integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"jsonparse": "1.3.1",
|
||||||
|
"through": "2.3.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"acorn": {
|
"acorn": {
|
||||||
"version": "4.0.13",
|
"version": "4.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
|
||||||
|
|
@ -1017,9 +1027,9 @@
|
||||||
"integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=",
|
"integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"JSONStream": "1.3.1",
|
||||||
"combine-source-map": "0.7.2",
|
"combine-source-map": "0.7.2",
|
||||||
"defined": "1.0.0",
|
"defined": "1.0.0",
|
||||||
"JSONStream": "1.3.1",
|
|
||||||
"through2": "2.0.3",
|
"through2": "2.0.3",
|
||||||
"umd": "3.0.1"
|
"umd": "3.0.1"
|
||||||
}
|
}
|
||||||
|
|
@ -1047,6 +1057,7 @@
|
||||||
"integrity": "sha1-tanJAgJD8McORnW+yCI7xifkFc4=",
|
"integrity": "sha1-tanJAgJD8McORnW+yCI7xifkFc4=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"JSONStream": "1.3.1",
|
||||||
"assert": "1.4.1",
|
"assert": "1.4.1",
|
||||||
"browser-pack": "6.0.2",
|
"browser-pack": "6.0.2",
|
||||||
"browser-resolve": "1.11.2",
|
"browser-resolve": "1.11.2",
|
||||||
|
|
@ -1068,7 +1079,6 @@
|
||||||
"https-browserify": "0.0.1",
|
"https-browserify": "0.0.1",
|
||||||
"inherits": "2.0.3",
|
"inherits": "2.0.3",
|
||||||
"insert-module-globals": "7.0.1",
|
"insert-module-globals": "7.0.1",
|
||||||
"JSONStream": "1.3.1",
|
|
||||||
"labeled-stream-splicer": "2.0.0",
|
"labeled-stream-splicer": "2.0.0",
|
||||||
"module-deps": "4.1.1",
|
"module-deps": "4.1.1",
|
||||||
"os-browserify": "0.1.2",
|
"os-browserify": "0.1.2",
|
||||||
|
|
@ -2487,10 +2497,10 @@
|
||||||
"integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=",
|
"integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"JSONStream": "1.3.1",
|
||||||
"combine-source-map": "0.7.2",
|
"combine-source-map": "0.7.2",
|
||||||
"concat-stream": "1.5.2",
|
"concat-stream": "1.5.2",
|
||||||
"is-buffer": "1.1.5",
|
"is-buffer": "1.1.5",
|
||||||
"JSONStream": "1.3.1",
|
|
||||||
"lexical-scope": "1.2.0",
|
"lexical-scope": "1.2.0",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
"through2": "2.0.3",
|
"through2": "2.0.3",
|
||||||
|
|
@ -2763,16 +2773,6 @@
|
||||||
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
|
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"JSONStream": {
|
|
||||||
"version": "1.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
|
|
||||||
"integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"jsonparse": "1.3.1",
|
|
||||||
"through": "2.3.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||||
|
|
@ -3126,6 +3126,7 @@
|
||||||
"integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=",
|
"integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"JSONStream": "1.3.1",
|
||||||
"browser-resolve": "1.11.2",
|
"browser-resolve": "1.11.2",
|
||||||
"cached-path-relative": "1.0.1",
|
"cached-path-relative": "1.0.1",
|
||||||
"concat-stream": "1.5.2",
|
"concat-stream": "1.5.2",
|
||||||
|
|
@ -3133,7 +3134,6 @@
|
||||||
"detective": "4.5.0",
|
"detective": "4.5.0",
|
||||||
"duplexer2": "0.1.4",
|
"duplexer2": "0.1.4",
|
||||||
"inherits": "2.0.3",
|
"inherits": "2.0.3",
|
||||||
"JSONStream": "1.3.1",
|
|
||||||
"parents": "1.0.1",
|
"parents": "1.0.1",
|
||||||
"readable-stream": "2.2.11",
|
"readable-stream": "2.2.11",
|
||||||
"resolve": "1.3.3",
|
"resolve": "1.3.3",
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,22 @@ namespace ClientSample
|
||||||
baseUrl = string.IsNullOrEmpty(baseUrl) ? "http://localhost:5000/default" : baseUrl;
|
baseUrl = string.IsNullOrEmpty(baseUrl) ? "http://localhost:5000/default" : baseUrl;
|
||||||
|
|
||||||
Console.WriteLine("Connecting to {0}", baseUrl);
|
Console.WriteLine("Connecting to {0}", baseUrl);
|
||||||
HubConnection connection = await ConnectAsync(baseUrl);
|
var connection = new HubConnectionBuilder()
|
||||||
Console.WriteLine("Connected to {0}", baseUrl);
|
.WithUrl(baseUrl)
|
||||||
|
.WithConsoleLogger(LogLevel.Trace)
|
||||||
|
.Build();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e => closeTcs.SetResult(null);
|
||||||
|
// Set up handler
|
||||||
|
connection.On<string>("Send", Console.WriteLine);
|
||||||
|
|
||||||
|
await ConnectAsync(connection);
|
||||||
|
|
||||||
|
Console.WriteLine("Connected to {0}", baseUrl);
|
||||||
|
|
||||||
var sendCts = new CancellationTokenSource();
|
var sendCts = new CancellationTokenSource();
|
||||||
|
|
||||||
Console.CancelKeyPress += async (sender, a) =>
|
Console.CancelKeyPress += async (sender, a) =>
|
||||||
|
|
@ -45,13 +56,10 @@ namespace ClientSample
|
||||||
await connection.DisposeAsync();
|
await connection.DisposeAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up handler
|
while (!closeTcs.Task.IsCompleted)
|
||||||
connection.On<string>("Send", Console.WriteLine);
|
|
||||||
|
|
||||||
while (!connection.Closed.IsCompleted)
|
|
||||||
{
|
{
|
||||||
var completedTask = await Task.WhenAny(Task.Run(() => Console.ReadLine()), connection.Closed);
|
var completedTask = await Task.WhenAny(Task.Run(() => Console.ReadLine()), closeTcs.Task);
|
||||||
if (completedTask == connection.Closed)
|
if (completedTask == closeTcs.Task)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -79,19 +87,15 @@ namespace ClientSample
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<HubConnection> ConnectAsync(string baseUrl)
|
private static async Task ConnectAsync(HubConnection connection)
|
||||||
{
|
{
|
||||||
// Keep trying to until we can start
|
// Keep trying to until we can start
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var connection = new HubConnectionBuilder()
|
|
||||||
.WithUrl(baseUrl)
|
|
||||||
.WithConsoleLogger(LogLevel.Trace)
|
|
||||||
.Build();
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await connection.StartAsync();
|
await connection.StartAsync();
|
||||||
return connection;
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ namespace ClientSample
|
||||||
var connection = new HttpConnection(new Uri(baseUrl), loggerFactory);
|
var connection = new HttpConnection(new Uri(baseUrl), loggerFactory);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e => closeTcs.SetResult(null);
|
||||||
connection.OnReceived(data => Console.Out.WriteLineAsync($"{Encoding.UTF8.GetString(data)}"));
|
connection.OnReceived(data => Console.Out.WriteLineAsync($"{Encoding.UTF8.GetString(data)}"));
|
||||||
|
|
||||||
await connection.StartAsync();
|
await connection.StartAsync();
|
||||||
|
|
||||||
Console.WriteLine($"Connected to {baseUrl}");
|
Console.WriteLine($"Connected to {baseUrl}");
|
||||||
|
|
@ -51,7 +52,7 @@ namespace ClientSample
|
||||||
await connection.DisposeAsync();
|
await connection.DisposeAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
while (!connection.Closed.IsCompleted)
|
while (!closeTcs.Task.IsCompleted)
|
||||||
{
|
{
|
||||||
var line = await Task.Run(() => Console.ReadLine(), cts.Token);
|
var line = await Task.Run(() => Console.ReadLine(), cts.Token);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ namespace JwtClientSample
|
||||||
.WithJwtBearer(() => _tokens[userId])
|
.WithJwtBearer(() => _tokens[userId])
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
var closedTcs = new TaskCompletionSource<object>();
|
||||||
|
hubConnection.Closed += e => closedTcs.SetResult(null);
|
||||||
|
|
||||||
hubConnection.On<string, string>("Message", (sender, message) => Console.WriteLine($"[{userId}] {sender}: {message}"));
|
hubConnection.On<string, string>("Message", (sender, message) => Console.WriteLine($"[{userId}] {sender}: {message}"));
|
||||||
await hubConnection.StartAsync();
|
await hubConnection.StartAsync();
|
||||||
Console.WriteLine($"[{userId}] Connection Started");
|
Console.WriteLine($"[{userId}] Connection Started");
|
||||||
|
|
@ -43,7 +46,7 @@ namespace JwtClientSample
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (!hubConnection.Closed.IsCompleted)
|
while (!closedTcs.Task.IsCompleted)
|
||||||
{
|
{
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
ticks++;
|
ticks++;
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,16 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
private HubProtocolReaderWriter _protocolReaderWriter;
|
private HubProtocolReaderWriter _protocolReaderWriter;
|
||||||
|
|
||||||
private readonly object _pendingCallsLock = new object();
|
private readonly object _pendingCallsLock = new object();
|
||||||
private readonly CancellationTokenSource _connectionActive = new CancellationTokenSource();
|
|
||||||
private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>();
|
private readonly Dictionary<string, InvocationRequest> _pendingCalls = new Dictionary<string, InvocationRequest>();
|
||||||
private readonly ConcurrentDictionary<string, List<InvocationHandler>> _handlers = new ConcurrentDictionary<string, List<InvocationHandler>>();
|
private readonly ConcurrentDictionary<string, List<InvocationHandler>> _handlers = new ConcurrentDictionary<string, List<InvocationHandler>>();
|
||||||
|
private CancellationTokenSource _connectionActive;
|
||||||
|
|
||||||
private int _nextId = 0;
|
private int _nextId = 0;
|
||||||
private volatile bool _startCalled;
|
private volatile bool _startCalled;
|
||||||
private Timer _timeoutTimer;
|
private Timer _timeoutTimer;
|
||||||
private bool _needKeepAlive;
|
private bool _needKeepAlive;
|
||||||
|
|
||||||
public Task Closed { get; }
|
public event Action<Exception> Closed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the server timeout interval for the connection. Changes to this value
|
/// Gets or sets the server timeout interval for the connection. Changes to this value
|
||||||
|
|
@ -69,11 +69,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
|
||||||
_logger = _loggerFactory.CreateLogger<HubConnection>();
|
_logger = _loggerFactory.CreateLogger<HubConnection>();
|
||||||
_connection.OnReceived((data, state) => ((HubConnection)state).OnDataReceivedAsync(data), this);
|
_connection.OnReceived((data, state) => ((HubConnection)state).OnDataReceivedAsync(data), this);
|
||||||
Closed = _connection.Closed.ContinueWith(task =>
|
_connection.Closed += e => Shutdown(e);
|
||||||
{
|
|
||||||
Shutdown(task.Exception);
|
|
||||||
return task;
|
|
||||||
}).Unwrap();
|
|
||||||
|
|
||||||
// Create the timer for timeout, but disabled by default (we enable it when started).
|
// Create the timer for timeout, but disabled by default (we enable it when started).
|
||||||
_timeoutTimer = new Timer(state => ((HubConnection)state).TimeoutElapsed(), this, Timeout.Infinite, Timeout.Infinite);
|
_timeoutTimer = new Timer(state => ((HubConnection)state).TimeoutElapsed(), this, Timeout.Infinite, Timeout.Infinite);
|
||||||
|
|
@ -122,12 +118,14 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
transferModeFeature.TransferMode = requestedTransferMode;
|
transferModeFeature.TransferMode = requestedTransferMode;
|
||||||
await _connection.StartAsync();
|
await _connection.StartAsync();
|
||||||
_needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null;
|
_needKeepAlive = _connection.Features.Get<IConnectionInherentKeepAliveFeature>() == null;
|
||||||
|
|
||||||
var actualTransferMode = transferModeFeature.TransferMode;
|
var actualTransferMode = transferModeFeature.TransferMode;
|
||||||
|
|
||||||
_protocolReaderWriter = new HubProtocolReaderWriter(_protocol, GetDataEncoder(requestedTransferMode, actualTransferMode));
|
_protocolReaderWriter = new HubProtocolReaderWriter(_protocol, GetDataEncoder(requestedTransferMode, actualTransferMode));
|
||||||
|
|
||||||
_logger.HubProtocol(_protocol.Name);
|
_logger.HubProtocol(_protocol.Name);
|
||||||
|
|
||||||
|
_connectionActive = new CancellationTokenSource();
|
||||||
using (var memoryStream = new MemoryStream())
|
using (var memoryStream = new MemoryStream())
|
||||||
{
|
{
|
||||||
NegotiationProtocol.WriteMessage(new NegotiationMessage(_protocol.Name), memoryStream);
|
NegotiationProtocol.WriteMessage(new NegotiationMessage(_protocol.Name), memoryStream);
|
||||||
|
|
@ -151,13 +149,16 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
return new PassThroughEncoder();
|
return new PassThroughEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync() => await StopAsyncCore().ForceAsync();
|
||||||
|
|
||||||
|
private Task StopAsyncCore() => _connection.StopAsync();
|
||||||
|
|
||||||
public async Task DisposeAsync() => await DisposeAsyncCore().ForceAsync();
|
public async Task DisposeAsync() => await DisposeAsyncCore().ForceAsync();
|
||||||
|
|
||||||
private async Task DisposeAsyncCore()
|
private async Task DisposeAsyncCore()
|
||||||
{
|
{
|
||||||
_timeoutTimer.Dispose();
|
_timeoutTimer.Dispose();
|
||||||
await _connection.DisposeAsync();
|
await _connection.DisposeAsync();
|
||||||
await Closed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Client return values/tasks?
|
// TODO: Client return values/tasks?
|
||||||
|
|
@ -370,12 +371,12 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Shutdown(Exception ex = null)
|
private void Shutdown(Exception exception = null)
|
||||||
{
|
{
|
||||||
_logger.ShutdownConnection();
|
_logger.ShutdownConnection();
|
||||||
if (ex != null)
|
if (exception != null)
|
||||||
{
|
{
|
||||||
_logger.ShutdownWithError(ex);
|
_logger.ShutdownWithError(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_pendingCallsLock)
|
lock (_pendingCallsLock)
|
||||||
|
|
@ -388,14 +389,23 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
foreach (var outstandingCall in _pendingCalls.Values)
|
foreach (var outstandingCall in _pendingCalls.Values)
|
||||||
{
|
{
|
||||||
_logger.RemoveInvocation(outstandingCall.InvocationId);
|
_logger.RemoveInvocation(outstandingCall.InvocationId);
|
||||||
if (ex != null)
|
if (exception != null)
|
||||||
{
|
{
|
||||||
outstandingCall.Fail(ex);
|
outstandingCall.Fail(exception);
|
||||||
}
|
}
|
||||||
outstandingCall.Dispose();
|
outstandingCall.Dispose();
|
||||||
}
|
}
|
||||||
_pendingCalls.Clear();
|
_pendingCalls.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Closed?.Invoke(exception);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.ErrorDuringClosedEvent(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DispatchInvocationAsync(InvocationMessage invocation, CancellationToken cancellationToken)
|
private async Task DispatchInvocationAsync(InvocationMessage invocation, CancellationToken cancellationToken)
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Internal
|
||||||
private static readonly Action<ILogger, Exception> _resettingKeepAliveTimer =
|
private static readonly Action<ILogger, Exception> _resettingKeepAliveTimer =
|
||||||
LoggerMessage.Define(LogLevel.Trace, new EventId(25, nameof(ResettingKeepAliveTimer)), "Resetting keep-alive timer, received a message from the server.");
|
LoggerMessage.Define(LogLevel.Trace, new EventId(25, nameof(ResettingKeepAliveTimer)), "Resetting keep-alive timer, received a message from the server.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, Exception> _errorDuringClosedEvent =
|
||||||
|
LoggerMessage.Define(LogLevel.Error, new EventId(26, nameof(ErrorDuringClosedEvent)), "An exception was thrown in the handler for the Closed event.");
|
||||||
|
|
||||||
// Category: Streaming and NonStreaming
|
// Category: Streaming and NonStreaming
|
||||||
private static readonly Action<ILogger, string, Exception> _invocationCreated =
|
private static readonly Action<ILogger, string, Exception> _invocationCreated =
|
||||||
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(0, nameof(InvocationCreated)), "Invocation {invocationId} created.");
|
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(0, nameof(InvocationCreated)), "Invocation {invocationId} created.");
|
||||||
|
|
@ -292,5 +295,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Internal
|
||||||
{
|
{
|
||||||
_resettingKeepAliveTimer(logger, null);
|
_resettingKeepAliveTimer(logger, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ErrorDuringClosedEvent(this ILogger logger, Exception exception)
|
||||||
|
{
|
||||||
|
_errorDuringClosedEvent(logger, exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,13 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
{
|
{
|
||||||
Task StartAsync();
|
Task StartAsync();
|
||||||
Task SendAsync(byte[] data, CancellationToken cancellationToken);
|
Task SendAsync(byte[] data, CancellationToken cancellationToken);
|
||||||
|
Task StopAsync();
|
||||||
Task DisposeAsync();
|
Task DisposeAsync();
|
||||||
Task AbortAsync(Exception ex);
|
Task AbortAsync(Exception ex);
|
||||||
|
|
||||||
IDisposable OnReceived(Func<byte[], object, Task> callback, object state);
|
IDisposable OnReceived(Func<byte[], object, Task> callback, object state);
|
||||||
|
|
||||||
Task Closed { get; }
|
event Action<Exception> Closed;
|
||||||
|
|
||||||
IFeatureCollection Features { get; }
|
IFeatureCollection Features { get; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,17 +28,20 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private volatile int _connectionState = ConnectionState.Initial;
|
private volatile ConnectionState _connectionState = ConnectionState.Disconnected;
|
||||||
|
private readonly object _stateChangeLock = new object();
|
||||||
|
|
||||||
private volatile ChannelConnection<byte[], SendMessage> _transportChannel;
|
private volatile ChannelConnection<byte[], SendMessage> _transportChannel;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly HttpOptions _httpOptions;
|
private readonly HttpOptions _httpOptions;
|
||||||
private volatile ITransport _transport;
|
private volatile ITransport _transport;
|
||||||
private volatile Task _receiveLoopTask;
|
private volatile Task _receiveLoopTask;
|
||||||
private TaskCompletionSource<object> _startTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
private TaskCompletionSource<object> _startTcs;
|
||||||
private readonly TaskCompletionSource<object> _closedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
private TaskCompletionSource<object> _closeTcs;
|
||||||
private TaskQueue _eventQueue = new TaskQueue();
|
private TaskQueue _eventQueue;
|
||||||
private readonly ITransportFactory _transportFactory;
|
private readonly ITransportFactory _transportFactory;
|
||||||
private string _connectionId;
|
private string _connectionId;
|
||||||
|
private Exception _abortException;
|
||||||
private readonly TimeSpan _eventQueueDrainTimeout = TimeSpan.FromSeconds(5);
|
private readonly TimeSpan _eventQueueDrainTimeout = TimeSpan.FromSeconds(5);
|
||||||
private ChannelReader<byte[]> Input => _transportChannel.Input;
|
private ChannelReader<byte[]> Input => _transportChannel.Input;
|
||||||
private ChannelWriter<SendMessage> Output => _transportChannel.Output;
|
private ChannelWriter<SendMessage> Output => _transportChannel.Output;
|
||||||
|
|
@ -49,7 +52,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
public IFeatureCollection Features { get; } = new FeatureCollection();
|
public IFeatureCollection Features { get; } = new FeatureCollection();
|
||||||
|
|
||||||
public Task Closed => _closedTcs.Task;
|
public event Action<Exception> Closed;
|
||||||
|
|
||||||
public HttpConnection(Uri url)
|
public HttpConnection(Uri url)
|
||||||
: this(url, TransportType.All)
|
: this(url, TransportType.All)
|
||||||
|
|
@ -103,25 +106,26 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
|
|
||||||
private Task StartAsyncCore()
|
private Task StartAsyncCore()
|
||||||
{
|
{
|
||||||
if (Interlocked.CompareExchange(ref _connectionState, ConnectionState.Connecting, ConnectionState.Initial)
|
if (ChangeState(from: ConnectionState.Disconnected, to: ConnectionState.Connecting) != ConnectionState.Disconnected)
|
||||||
!= ConnectionState.Initial)
|
|
||||||
{
|
{
|
||||||
return Task.FromException(
|
return Task.FromException(
|
||||||
new InvalidOperationException("Cannot start a connection that is not in the Initial state."));
|
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()
|
StartAsyncInternal()
|
||||||
.ContinueWith(t =>
|
.ContinueWith(t =>
|
||||||
{
|
{
|
||||||
if (t.IsFaulted)
|
var abortException = _abortException;
|
||||||
|
if (t.IsFaulted || abortException != null)
|
||||||
{
|
{
|
||||||
_startTcs.SetException(t.Exception.InnerException);
|
_startTcs.SetException(_abortException ?? t.Exception.InnerException);
|
||||||
_closedTcs.TrySetException(t.Exception.InnerException);
|
|
||||||
}
|
}
|
||||||
else if (t.IsCanceled)
|
else if (t.IsCanceled)
|
||||||
{
|
{
|
||||||
_startTcs.SetCanceled();
|
_startTcs.SetCanceled();
|
||||||
_closedTcs.SetCanceled();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -148,8 +152,8 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
var negotiationResponse = await Negotiate(Url, _httpClient, _logger);
|
var negotiationResponse = await Negotiate(Url, _httpClient, _logger);
|
||||||
_connectionId = negotiationResponse.ConnectionId;
|
_connectionId = negotiationResponse.ConnectionId;
|
||||||
|
|
||||||
// Connection is being stopped while start was in progress
|
// Connection is being disposed while start was in progress
|
||||||
if (_connectionState == ConnectionState.Disconnected)
|
if (_connectionState == ConnectionState.Disposed)
|
||||||
{
|
{
|
||||||
_logger.HttpConnectionClosed(_connectionId);
|
_logger.HttpConnectionClosed(_connectionId);
|
||||||
return;
|
return;
|
||||||
|
|
@ -164,17 +168,25 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _connectionState, ConnectionState.Disconnected);
|
// 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;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the connection is not in the Connecting state here it means the user called DisposeAsync
|
// if the connection is not in the Connecting state here it means the user called DisposeAsync while
|
||||||
if (Interlocked.CompareExchange(ref _connectionState, ConnectionState.Connected, ConnectionState.Connecting)
|
// the connection was starting
|
||||||
== ConnectionState.Connecting)
|
if (ChangeState(from: ConnectionState.Connecting, to: ConnectionState.Connected) == ConnectionState.Connecting)
|
||||||
{
|
{
|
||||||
|
_closeTcs = new TaskCompletionSource<object>();
|
||||||
|
|
||||||
_ = Input.Completion.ContinueWith(async t =>
|
_ = Input.Completion.ContinueWith(async t =>
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _connectionState, ConnectionState.Disconnected);
|
// 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
|
// 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
|
// makes Input.Completion task completed and runs this continuation. We need to await _receiveLoopTask
|
||||||
|
|
@ -187,28 +199,38 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
await _receiveLoopTask;
|
await _receiveLoopTask;
|
||||||
|
|
||||||
_logger.DrainEvents(_connectionId);
|
_logger.DrainEvents(_connectionId);
|
||||||
await _eventQueue.Drain();
|
|
||||||
|
|
||||||
await Task.WhenAny(_eventQueue.Drain().NoThrow(), Task.Delay(_eventQueueDrainTimeout));
|
await Task.WhenAny(_eventQueue.Drain().NoThrow(), Task.Delay(_eventQueueDrainTimeout));
|
||||||
_httpClient?.Dispose();
|
|
||||||
|
|
||||||
_logger.CompleteClosed(_connectionId);
|
_logger.CompleteClosed(_connectionId);
|
||||||
if (t.IsFaulted)
|
|
||||||
|
// 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
|
||||||
{
|
{
|
||||||
_closedTcs.TrySetException(t.Exception.InnerException);
|
if (t.IsFaulted)
|
||||||
|
{
|
||||||
|
Closed?.Invoke(t.Exception.InnerException);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (t.IsCanceled)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_closedTcs.TrySetCanceled();
|
// Suppress (but log) the exception, this is user code
|
||||||
}
|
_logger.ErrorDuringClosedEvent(ex);
|
||||||
else
|
|
||||||
{
|
|
||||||
_closedTcs.TrySetResult(null);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// start receive loop only after the Connected event was raised to
|
|
||||||
// avoid Received event being raised before the Connected event
|
|
||||||
_receiveLoopTask = ReceiveAsync();
|
_receiveLoopTask = ReceiveAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -306,7 +328,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
await _transport.StartAsync(connectUrl, applicationSide, GetTransferMode(), _connectionId, this);
|
await _transport.StartAsync(connectUrl, applicationSide, GetTransferMode(), _connectionId, this);
|
||||||
|
|
||||||
// actual transfer mode can differ from the one that was requested so set it on the feature
|
// actual transfer mode can differ from the one that was requested so set it on the feature
|
||||||
Debug.Assert(_transport.Mode.HasValue, "transfer mode not set after transport started");
|
if (!_transport.Mode.HasValue)
|
||||||
|
{
|
||||||
|
// This can happen with custom transports so it should be an exception, not an assert.
|
||||||
|
throw new InvalidOperationException("Transport was expected to set the Mode property after StartAsync, but it has not been set.");
|
||||||
|
}
|
||||||
SetTransferMode(_transport.Mode.Value);
|
SetTransferMode(_transport.Mode.Value);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -435,31 +461,42 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AbortAsync(Exception ex) => await DisposeAsyncCore(ex ?? new InvalidOperationException("Connection aborted")).ForceAsync();
|
// 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 DisposeAsync() => await DisposeAsyncCore().ForceAsync();
|
public async Task StopAsync() => await StopAsyncCore(exception: null).ForceAsync();
|
||||||
|
|
||||||
private async Task DisposeAsyncCore(Exception ex = null)
|
private async Task StopAsyncCore(Exception exception)
|
||||||
{
|
{
|
||||||
if (ex != null)
|
lock (_stateChangeLock)
|
||||||
{
|
{
|
||||||
_logger.AbortingClient(_connectionId, ex);
|
if (!(_connectionState == ConnectionState.Connecting || _connectionState == ConnectionState.Connected))
|
||||||
|
{
|
||||||
// Immediately fault the close task. When the transport shuts down,
|
_logger.SkippingStop(_connectionId);
|
||||||
// it will trigger the close task to be completed, so we want it to be
|
return;
|
||||||
// marked faulted before that happens
|
}
|
||||||
_closedTcs.TrySetException(ex);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.StoppingClient(_connectionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Interlocked.Exchange(ref _connectionState, ConnectionState.Disconnected) == ConnectionState.Initial)
|
// 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
|
||||||
// the connection was never started so there is nothing to clean up
|
// channel can be closed only once. As a result the continuation that does actual job and raises the Closed
|
||||||
return;
|
// event runs always only once.
|
||||||
}
|
|
||||||
|
// See comment at AbortAsync for more discussion on the thread-safety of this.
|
||||||
|
_abortException = exception;
|
||||||
|
|
||||||
|
_logger.StoppingClient(_connectionId);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -486,8 +523,29 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
await _receiveLoopTask;
|
await _receiveLoopTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we haven't already done so, trigger the Closed task.
|
if (_closeTcs != null)
|
||||||
_closedTcs.TrySetResult(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)
|
||||||
|
{
|
||||||
|
_logger.SkippingDispose(_connectionId);
|
||||||
|
|
||||||
|
// the connection was already disposed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.DisposingClient(_connectionId);
|
||||||
|
|
||||||
_httpClient?.Dispose();
|
_httpClient?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,12 +595,39 @@ namespace Microsoft.AspNetCore.Sockets.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ConnectionState
|
private ConnectionState ChangeState(ConnectionState from, ConnectionState to)
|
||||||
{
|
{
|
||||||
public const int Initial = 0;
|
lock (_stateChangeLock)
|
||||||
public const int Connecting = 1;
|
{
|
||||||
public const int Connected = 2;
|
var state = _connectionState;
|
||||||
public const int Disconnected = 3;
|
if (_connectionState == from)
|
||||||
|
{
|
||||||
|
_connectionState = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.ConnectionStateChanged(_connectionId, state, to);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConnectionState ChangeState(ConnectionState to)
|
||||||
|
{
|
||||||
|
lock (_stateChangeLock)
|
||||||
|
{
|
||||||
|
var state = _connectionState;
|
||||||
|
_connectionState = to;
|
||||||
|
_logger.ConnectionStateChanged(_connectionId, state, to);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal because it's used by logging to avoid ToStringing prematurely.
|
||||||
|
internal enum ConnectionState
|
||||||
|
{
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Disposed
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NegotiationResponse
|
private class NegotiationResponse
|
||||||
|
|
|
||||||
|
|
@ -151,11 +151,25 @@ namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||||
LoggerMessage.Define<DateTime, string>(LogLevel.Information, new EventId(18, nameof(StoppingClient)), "{time}: Connection Id {connectionId}: Stopping client.");
|
LoggerMessage.Define<DateTime, string>(LogLevel.Information, new EventId(18, nameof(StoppingClient)), "{time}: Connection Id {connectionId}: Stopping client.");
|
||||||
|
|
||||||
private static readonly Action<ILogger, DateTime, string, string, Exception> _exceptionThrownFromCallback =
|
private static readonly Action<ILogger, DateTime, string, string, Exception> _exceptionThrownFromCallback =
|
||||||
LoggerMessage.Define<DateTime, string, string>(LogLevel.Error, new EventId(19, nameof(ExceptionThrownFromCallback)), "{time}: Connection Id {connectionId}: An exception was thrown from the '{callback}' callback");
|
LoggerMessage.Define<DateTime, string, string>(LogLevel.Error, new EventId(19, nameof(ExceptionThrownFromCallback)), "{time}: Connection Id {connectionId}: An exception was thrown from the '{callback}' callback.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, DateTime, string, Exception> _disposingClient =
|
||||||
|
LoggerMessage.Define<DateTime, string>(LogLevel.Information, new EventId(20, nameof(DisposingClient)), "{time}: Connection Id {connectionId}: Disposing client.");
|
||||||
|
|
||||||
private static readonly Action<ILogger, DateTime, string, Exception> _abortingClient =
|
private static readonly Action<ILogger, DateTime, string, Exception> _abortingClient =
|
||||||
LoggerMessage.Define<DateTime, string>(LogLevel.Error, new EventId(20, nameof(AbortingClient)), "{time}: Connection Id {connectionId}: Aborting client.");
|
LoggerMessage.Define<DateTime, string>(LogLevel.Error, new EventId(21, nameof(AbortingClient)), "{time}: Connection Id {connectionId}: Aborting client.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, Exception> _errorDuringClosedEvent =
|
||||||
|
LoggerMessage.Define(LogLevel.Error, new EventId(22, nameof(ErrorDuringClosedEvent)), "An exception was thrown in the handler for the Closed event.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, DateTime, string, Exception> _skippingStop =
|
||||||
|
LoggerMessage.Define<DateTime, string>(LogLevel.Debug, new EventId(23, nameof(SkippingStop)), "{time}: Connection Id {connectionId}: Skipping stop, connection is already stopped.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, DateTime, string, Exception> _skippingDispose =
|
||||||
|
LoggerMessage.Define<DateTime, string>(LogLevel.Debug, new EventId(24, nameof(SkippingDispose)), "{time}: Connection Id {connectionId}: Skipping dispose, connection is already disposed.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, DateTime, string, string, string, Exception> _connectionStateChanged =
|
||||||
|
LoggerMessage.Define<DateTime, string, string, string>(LogLevel.Debug, new EventId(25, nameof(ConnectionStateChanged)), "{time}: Connection Id {connectionId}: Connection state changed from {previousState} to {newState}.");
|
||||||
|
|
||||||
public static void StartTransport(this ILogger logger, string connectionId, TransferMode transferMode)
|
public static void StartTransport(this ILogger logger, string connectionId, TransferMode transferMode)
|
||||||
{
|
{
|
||||||
|
|
@ -525,6 +539,38 @@ namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void DisposingClient(this ILogger logger, string connectionId)
|
||||||
|
{
|
||||||
|
if (logger.IsEnabled(LogLevel.Information))
|
||||||
|
{
|
||||||
|
_disposingClient(logger, DateTime.Now, connectionId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SkippingDispose(this ILogger logger, string connectionId)
|
||||||
|
{
|
||||||
|
if (logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
_skippingDispose(logger, DateTime.Now, connectionId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ConnectionStateChanged(this ILogger logger, string connectionId, HttpConnection.ConnectionState previousState, HttpConnection.ConnectionState newState)
|
||||||
|
{
|
||||||
|
if (logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
_connectionStateChanged(logger, DateTime.Now, connectionId, previousState.ToString(), newState.ToString(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SkippingStop(this ILogger logger, string connectionId)
|
||||||
|
{
|
||||||
|
if (logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
_skippingStop(logger, DateTime.Now, connectionId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void ExceptionThrownFromCallback(this ILogger logger, string connectionId, string callbackName, Exception exception)
|
public static void ExceptionThrownFromCallback(this ILogger logger, string connectionId, string callbackName, Exception exception)
|
||||||
{
|
{
|
||||||
if (logger.IsEnabled(LogLevel.Error))
|
if (logger.IsEnabled(LogLevel.Error))
|
||||||
|
|
@ -532,5 +578,10 @@ namespace Microsoft.AspNetCore.Sockets.Client.Internal
|
||||||
_exceptionThrownFromCallback(logger, DateTime.Now, connectionId, callbackName, exception);
|
_exceptionThrownFromCallback(logger, DateTime.Now, connectionId, callbackName, exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ErrorDuringClosedEvent(this ILogger logger, Exception exception)
|
||||||
|
{
|
||||||
|
_errorDuringClosedEvent(logger, exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,88 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||||
|
public async Task CanStopAndStartConnection(IHubProtocol protocol, TransportType transportType, string path)
|
||||||
|
{
|
||||||
|
using (StartLog(out var loggerFactory))
|
||||||
|
{
|
||||||
|
const string originalMessage = "SignalR";
|
||||||
|
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||||
|
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
var result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||||
|
Assert.Equal(originalMessage, result);
|
||||||
|
await connection.StopAsync().OrTimeout();
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||||
|
Assert.Equal(originalMessage, result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanStartConnectionFromClosedEvent()
|
||||||
|
{
|
||||||
|
using (StartLog(out var loggerFactory))
|
||||||
|
{
|
||||||
|
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 restartTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += async e =>
|
||||||
|
{
|
||||||
|
logger.LogInformation("Closed event triggered");
|
||||||
|
if (!restartTcs.Task.IsCompleted)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Restarting connection");
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
logger.LogInformation("Restarted connection");
|
||||||
|
restartTcs.SetResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
var result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||||
|
Assert.Equal(originalMessage, result);
|
||||||
|
|
||||||
|
logger.LogInformation("Stopping connection");
|
||||||
|
await connection.StopAsync().OrTimeout();
|
||||||
|
|
||||||
|
logger.LogInformation("Waiting for reconnect");
|
||||||
|
await restartTcs.Task.OrTimeout();
|
||||||
|
logger.LogInformation("Reconnection complete");
|
||||||
|
|
||||||
|
result = await connection.InvokeAsync<string>("Echo", originalMessage).OrTimeout();
|
||||||
|
Assert.Equal(originalMessage, result);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "Exception from test");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||||
public async Task MethodsAreCaseInsensitive(IHubProtocol protocol, TransportType transportType, string path)
|
public async Task MethodsAreCaseInsensitive(IHubProtocol protocol, TransportType transportType, string path)
|
||||||
|
|
@ -174,12 +256,30 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
{
|
{
|
||||||
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
var httpConnection = new HttpConnection(new Uri(_serverFixture.Url + path), transportType, loggerFactory);
|
||||||
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
var connection = new HubConnection(httpConnection, protocol, loggerFactory);
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e =>
|
||||||
|
{
|
||||||
|
if (e != null)
|
||||||
|
{
|
||||||
|
closeTcs.SetException(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
closeTcs.SetResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await connection.StartAsync().OrTimeout();
|
await connection.StartAsync().OrTimeout();
|
||||||
await connection.InvokeAsync("CallHandlerThatDoesntExist").OrTimeout();
|
await connection.InvokeAsync("CallHandlerThatDoesntExist").OrTimeout();
|
||||||
await connection.DisposeAsync().OrTimeout();
|
await connection.DisposeAsync().OrTimeout();
|
||||||
await connection.Closed.OrTimeout();
|
await closeTcs.Task.OrTimeout();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
loggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} during test: {Message}", ex.GetType().Name, ex.Message);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@
|
||||||
<PackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
<PackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\xunit.runner.json" Link="xunit.runner.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Client\Microsoft.AspNetCore.SignalR.Client.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Client\Microsoft.AspNetCore.SignalR.Client.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
// 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.Sockets;
|
||||||
|
using Microsoft.AspNetCore.Sockets.Client;
|
||||||
|
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||||
|
using Microsoft.AspNetCore.Sockets.Client.Tests;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
{
|
||||||
|
public partial class HttpConnectionTests
|
||||||
|
{
|
||||||
|
// Nested class for grouping
|
||||||
|
public class AbortAsync
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task AbortAsyncTriggersClosedEventWithException()
|
||||||
|
{
|
||||||
|
var connection = CreateConnection(out var closedTask);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Start the connection
|
||||||
|
await connection.StartAsync().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 closedTask.OrTimeout());
|
||||||
|
Assert.Same(expected, actual);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Dispose should be clean and exception free.
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AbortAsyncWhileStoppingTriggersClosedEventWithException()
|
||||||
|
{
|
||||||
|
var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(2, out var syncPoints));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Start the connection
|
||||||
|
await connection.StartAsync().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 closedTask.OrTimeout());
|
||||||
|
Assert.Same(expected, actual);
|
||||||
|
|
||||||
|
// Clean-up
|
||||||
|
syncPoints[1].Continue();
|
||||||
|
await Task.WhenAll(stopTask, abortTask).OrTimeout();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Dispose should be clean and exception free.
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StopAsyncWhileAbortingTriggersClosedEventWithoutException()
|
||||||
|
{
|
||||||
|
var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(2, out var syncPoints));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Start the connection
|
||||||
|
await connection.StartAsync().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 closedTask.OrTimeout();
|
||||||
|
|
||||||
|
// Clean-up
|
||||||
|
syncPoints[0].Continue();
|
||||||
|
await abortTask.OrTimeout();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Dispose should be clean and exception free.
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsyncCannotBeCalledWhileAbortAsyncInProgress()
|
||||||
|
{
|
||||||
|
var connection = CreateConnection(out var closedTask, stopHandler: SyncPoint.Create(out var syncPoint));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Start the connection
|
||||||
|
await connection.StartAsync().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().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>(() => closedTask.OrTimeout());
|
||||||
|
Assert.Same(expected, actual);
|
||||||
|
|
||||||
|
// We can start now
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
|
// And we can stop without getting the abort exception.
|
||||||
|
await connection.StopAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Dispose should be clean and exception free.
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpConnection CreateConnection(out Task closedTask, Func<Task> stopHandler = null)
|
||||||
|
{
|
||||||
|
var httpHandler = new TestHttpMessageHandler();
|
||||||
|
var transportFactory = new TestTransportFactory(new TestTransport(stopHandler));
|
||||||
|
var connection = new HttpConnection(
|
||||||
|
new Uri("http://fakeuri.org/"),
|
||||||
|
transportFactory,
|
||||||
|
NullLoggerFactory.Instance,
|
||||||
|
new HttpOptions()
|
||||||
|
{
|
||||||
|
HttpMessageHandler = httpHandler,
|
||||||
|
});
|
||||||
|
|
||||||
|
var closedTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += ex =>
|
||||||
|
{
|
||||||
|
if (ex != null)
|
||||||
|
{
|
||||||
|
closedTcs.SetException(ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
closedTcs.SetResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
closedTask = closedTcs.Task;
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestTransport : ITransport
|
||||||
|
{
|
||||||
|
private Channel<byte[], SendMessage> _application;
|
||||||
|
private readonly Func<Task> _stopHandler;
|
||||||
|
|
||||||
|
public TransferMode? Mode => TransferMode.Text;
|
||||||
|
|
||||||
|
public TestTransport(Func<Task> stopHandler)
|
||||||
|
{
|
||||||
|
_stopHandler = stopHandler ?? new Func<Task>(() => Task.CompletedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(Uri url, Channel<byte[], SendMessage> application, TransferMode requestedTransferMode, string connectionId, IConnection connection)
|
||||||
|
{
|
||||||
|
_application = application;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
await _stopHandler();
|
||||||
|
_application.Writer.TryComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>();
|
||||||
|
|
||||||
|
// Used by the test code to wait and continue
|
||||||
|
public Task WaitForSyncPoint() => _atSyncPoint.Task;
|
||||||
|
public void Continue() => _continueFromSyncPoint.TrySetResult(null);
|
||||||
|
|
||||||
|
// Used by the code under test to wait for the test code to release it.
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -12,14 +13,20 @@ using Microsoft.AspNetCore.Client.Tests;
|
||||||
using Microsoft.AspNetCore.Sockets.Client.Http;
|
using Microsoft.AspNetCore.Sockets.Client.Http;
|
||||||
using Microsoft.AspNetCore.Sockets.Features;
|
using Microsoft.AspNetCore.Sockets.Features;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Moq.Protected;
|
using Moq.Protected;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
{
|
{
|
||||||
public class HttpConnectionTests
|
public partial class HttpConnectionTests : LoggedTest
|
||||||
{
|
{
|
||||||
|
public HttpConnectionTests(ITestOutputHelper output) : base(output)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CannotCreateConnectionWithNullUrl()
|
public void CannotCreateConnectionWithNullUrl()
|
||||||
{
|
{
|
||||||
|
|
@ -53,7 +60,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
|
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -66,7 +73,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
var exception =
|
var exception =
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
async () => await connection.StartAsync());
|
async () => await connection.StartAsync());
|
||||||
Assert.Equal("Cannot start a connection that is not in the Initial state.", exception.Message);
|
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -75,7 +82,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CannotStartStoppedConnection()
|
public async Task CannotStartConnectionDisposedAfterStarting()
|
||||||
{
|
{
|
||||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
mockHttpHandler.Protected()
|
mockHttpHandler.Protected()
|
||||||
|
|
@ -83,7 +90,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -97,7 +104,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
async () => await connection.StartAsync());
|
async () => await connection.StartAsync());
|
||||||
|
|
||||||
Assert.Equal("Cannot start a connection that is not in the Initial state.", exception.Message);
|
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -111,12 +118,12 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
async () => await connection.StartAsync());
|
async () => await connection.StartAsync());
|
||||||
|
|
||||||
Assert.Equal("Cannot start a connection that is not in the Initial state.", exception.Message);
|
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanStopStartingConnection()
|
public async Task CanDisposeStartingConnection()
|
||||||
{
|
{
|
||||||
// Used to make sure StartAsync is not completed before DisposeAsync is called
|
// Used to make sure StartAsync is not completed before DisposeAsync is called
|
||||||
var releaseNegotiateTcs = new TaskCompletionSource<object>();
|
var releaseNegotiateTcs = new TaskCompletionSource<object>();
|
||||||
|
|
@ -134,13 +141,25 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
// allow DisposeAsync to continue once we know we are past the connection state check
|
// allow DisposeAsync to continue once we know we are past the connection state check
|
||||||
allowDisposeTcs.SetResult(null);
|
allowDisposeTcs.SetResult(null);
|
||||||
await releaseNegotiateTcs.Task;
|
await releaseNegotiateTcs.Task;
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
||||||
var transport = new Mock<ITransport>();
|
var transport = new Mock<ITransport>();
|
||||||
transport.Setup(t => t.StopAsync()).Returns(async () => { await releaseDisposeTcs.Task; });
|
Channel<byte[], SendMessage> channel = null;
|
||||||
|
transport.SetupGet(t => t.Mode).Returns(TransferMode.Text);
|
||||||
|
transport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<Channel<byte[], SendMessage>>(), It.IsAny<TransferMode>(), It.IsAny<string>(), It.IsAny<IConnection>()))
|
||||||
|
.Returns<Uri, Channel<byte[], SendMessage>, TransferMode, string, IConnection>((_, c, __, ___, ____) =>
|
||||||
|
{
|
||||||
|
channel = c;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
transport.Setup(t => t.StopAsync()).Returns(async () =>
|
||||||
|
{
|
||||||
|
await releaseDisposeTcs.Task;
|
||||||
|
channel.Writer.TryComplete();
|
||||||
|
});
|
||||||
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), new TestTransportFactory(transport.Object), loggerFactory: null,
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), new TestTransportFactory(transport.Object), loggerFactory: null,
|
||||||
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
|
||||||
|
|
@ -154,8 +173,214 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
await startTask.OrTimeout();
|
await startTask.OrTimeout();
|
||||||
releaseDisposeTcs.SetResult(null);
|
releaseDisposeTcs.SetResult(null);
|
||||||
await disposeTask.OrTimeout();
|
await disposeTask.OrTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
transport.Verify(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<Channel<byte[], SendMessage>>(), It.IsAny<TransferMode>(), It.IsAny<string>(), It.IsAny<IConnection>()), Times.Never);
|
[Fact]
|
||||||
|
public async Task CanStartConnectionThatFailedToStart()
|
||||||
|
{
|
||||||
|
var failNegotiate = true;
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
if (ResponseUtils.IsNegotiateRequest(request))
|
||||||
|
{
|
||||||
|
return failNegotiate
|
||||||
|
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
||||||
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
||||||
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
failNegotiate = false;
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanStartStoppedConnection()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
||||||
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
await connection.StopAsync().OrTimeout();
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanStopStartingConnection()
|
||||||
|
{
|
||||||
|
var allowStopTcs = new TaskCompletionSource<object>();
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (ResponseUtils.IsNegotiateRequest(request))
|
||||||
|
{
|
||||||
|
allowStopTcs.SetResult(null);
|
||||||
|
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var content = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;
|
||||||
|
return (content?.Length == 1 && content[0] == 0x42)
|
||||||
|
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
||||||
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
||||||
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e => closeTcs.TrySetResult(null);
|
||||||
|
|
||||||
|
var startTask = connection.StartAsync();
|
||||||
|
await allowStopTcs.Task.OrTimeout();
|
||||||
|
|
||||||
|
await Task.WhenAll(startTask, connection.StopAsync()).OrTimeout();
|
||||||
|
await closeTcs.Task.OrTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanStartConnectionAfterConnectionStoppedWithError()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (ResponseUtils.IsNegotiateRequest(request))
|
||||||
|
{
|
||||||
|
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;
|
||||||
|
return (content?.Length == 1 && content[0] == 0x42)
|
||||||
|
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
||||||
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
||||||
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e => closeTcs.TrySetResult(null);
|
||||||
|
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.SendAsync(new byte[] { 0x42 }).OrTimeout();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
await closeTcs.Task.OrTimeout();
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanDisposeStoppedConnection()
|
||||||
|
{
|
||||||
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"));
|
||||||
|
await connection.StopAsync().OrTimeout();
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StoppingStoppingConnectionNoOps()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (ResponseUtils.IsNegotiateRequest(request))
|
||||||
|
{
|
||||||
|
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var content = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;
|
||||||
|
return (content?.Length == 1 && content[0] == 0x42)
|
||||||
|
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
||||||
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
||||||
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e => closeTcs.TrySetResult(null);
|
||||||
|
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
await Task.WhenAll(connection.StopAsync().OrTimeout(), connection.StopAsync().OrTimeout());
|
||||||
|
await closeTcs.Task.OrTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DisposedStoppingConnectionDisposesConnection()
|
||||||
|
{
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (ResponseUtils.IsNegotiateRequest(request))
|
||||||
|
{
|
||||||
|
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var content = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;
|
||||||
|
return (content?.Length == 1 && content[0] == 0x42)
|
||||||
|
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
||||||
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
||||||
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e => closeTcs.TrySetResult(null);
|
||||||
|
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
await Task.WhenAll(connection.StopAsync().OrTimeout(), connection.DisposeAsync().OrTimeout());
|
||||||
|
await closeTcs.Task.OrTimeout();
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync());
|
||||||
|
Assert.Equal("Cannot start a connection that is not in the Disconnected state.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -176,7 +401,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -201,7 +426,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -209,11 +434,21 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
||||||
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e =>
|
||||||
|
{
|
||||||
|
if (e != null)
|
||||||
|
{
|
||||||
|
closeTcs.SetException(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
closeTcs.SetResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
await connection.StartAsync().OrTimeout();
|
await connection.StartAsync().OrTimeout();
|
||||||
await connection.DisposeAsync().OrTimeout();
|
await connection.DisposeAsync().OrTimeout();
|
||||||
await connection.Closed.OrTimeout();
|
await closeTcs.Task.OrTimeout();
|
||||||
// in case of clean disconnect error should be null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -228,18 +463,30 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
|
|
||||||
return request.Method == HttpMethod.Get
|
return request.Method == HttpMethod.Get
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
||||||
: IsNegotiateRequest(request)
|
: ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
||||||
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), TransportType.LongPolling, loggerFactory: null,
|
||||||
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e =>
|
||||||
|
{
|
||||||
|
if (e != null)
|
||||||
|
{
|
||||||
|
closeTcs.SetException(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
closeTcs.SetResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await connection.StartAsync().OrTimeout();
|
await connection.StartAsync().OrTimeout();
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(() => connection.Closed.OrTimeout());
|
await Assert.ThrowsAsync<HttpRequestException>(() => closeTcs.Task.OrTimeout());
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -247,54 +494,6 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ReceivedCallbackNotRaisedAfterConnectionIsDisposed()
|
|
||||||
{
|
|
||||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
|
||||||
mockHttpHandler.Protected()
|
|
||||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
await Task.Yield();
|
|
||||||
return IsNegotiateRequest(request)
|
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
|
||||||
});
|
|
||||||
|
|
||||||
var mockTransport = new Mock<ITransport>();
|
|
||||||
Channel<byte[], SendMessage> channel = null;
|
|
||||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<Channel<byte[], SendMessage>>(), It.IsAny<TransferMode>(), It.IsAny<string>(), It.IsAny<IConnection>()))
|
|
||||||
.Returns<Uri, Channel<byte[], SendMessage>, TransferMode, string, IConnection>((url, c, transferMode, connectionId, _) =>
|
|
||||||
{
|
|
||||||
channel = c;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
mockTransport.Setup(t => t.StopAsync())
|
|
||||||
.Returns(() =>
|
|
||||||
{
|
|
||||||
// The connection is now in the Disconnected state so the Received event for
|
|
||||||
// this message should not be raised
|
|
||||||
channel.Writer.TryWrite(Array.Empty<byte>());
|
|
||||||
channel.Writer.TryComplete();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
mockTransport.SetupGet(t => t.Mode).Returns(TransferMode.Text);
|
|
||||||
|
|
||||||
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), new TestTransportFactory(mockTransport.Object), loggerFactory: null,
|
|
||||||
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
|
||||||
|
|
||||||
var onReceivedInvoked = false;
|
|
||||||
connection.OnReceived(_ =>
|
|
||||||
{
|
|
||||||
onReceivedInvoked = true;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
|
|
||||||
await connection.StartAsync();
|
|
||||||
await connection.DisposeAsync();
|
|
||||||
Assert.False(onReceivedInvoked);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EventsAreNotRunningOnMainLoop()
|
public async Task EventsAreNotRunningOnMainLoop()
|
||||||
{
|
{
|
||||||
|
|
@ -304,7 +503,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -355,46 +554,63 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EventQueueTimeout()
|
public async Task EventQueueTimeout()
|
||||||
{
|
{
|
||||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
using (StartLog(out var loggerFactory))
|
||||||
mockHttpHandler.Protected()
|
{
|
||||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
var logger = loggerFactory.CreateLogger<HttpConnectionTests>();
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
|
||||||
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
|
mockHttpHandler.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mockTransport = new Mock<ITransport>();
|
||||||
|
Channel<byte[], SendMessage> channel = null;
|
||||||
|
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<Channel<byte[], SendMessage>>(), It.IsAny<TransferMode>(), It.IsAny<string>(), It.IsAny<IConnection>()))
|
||||||
|
.Returns<Uri, Channel<byte[], SendMessage>, TransferMode, string, IConnection>((url, c, transferMode, connectionId, _) =>
|
||||||
|
{
|
||||||
|
logger.LogInformation("Transport started");
|
||||||
|
channel = c;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
mockTransport.Setup(t => t.StopAsync())
|
||||||
|
.Returns(() =>
|
||||||
|
{
|
||||||
|
logger.LogInformation("Transport stopped");
|
||||||
|
channel.Writer.TryComplete();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
mockTransport.SetupGet(t => t.Mode).Returns(TransferMode.Text);
|
||||||
|
|
||||||
|
var blockReceiveCallbackTcs = new TaskCompletionSource<object>();
|
||||||
|
var onReceivedCalledTcs = new TaskCompletionSource<object>();
|
||||||
|
|
||||||
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), new TestTransportFactory(mockTransport.Object), loggerFactory,
|
||||||
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
|
connection.OnReceived(async _ =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
onReceivedCalledTcs.TrySetResult(null);
|
||||||
return IsNegotiateRequest(request)
|
await blockReceiveCallbackTcs.Task;
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var mockTransport = new Mock<ITransport>();
|
logger.LogInformation("Starting connection");
|
||||||
Channel<byte[], SendMessage> channel = null;
|
await connection.StartAsync().OrTimeout();
|
||||||
mockTransport.Setup(t => t.StartAsync(It.IsAny<Uri>(), It.IsAny<Channel<byte[], SendMessage>>(), It.IsAny<TransferMode>(), It.IsAny<string>(), It.IsAny<IConnection>()))
|
logger.LogInformation("Started connection");
|
||||||
.Returns<Uri, Channel<byte[], SendMessage>, TransferMode, string, IConnection>((url, c, transferMode, connectionId, _) =>
|
channel.Writer.TryWrite(Array.Empty<byte>());
|
||||||
{
|
await onReceivedCalledTcs.Task.OrTimeout();
|
||||||
channel = c;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
mockTransport.Setup(t => t.StopAsync())
|
|
||||||
.Returns(() =>
|
|
||||||
{
|
|
||||||
channel.Writer.TryComplete();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
mockTransport.SetupGet(t => t.Mode).Returns(TransferMode.Text);
|
|
||||||
|
|
||||||
var blockReceiveCallbackTcs = new TaskCompletionSource<object>();
|
// Ensure that SignalR isn't blocked by the receive callback
|
||||||
|
Assert.False(channel.Reader.TryRead(out var message));
|
||||||
|
|
||||||
var connection = new HttpConnection(new Uri("http://fakeuri.org/"), new TestTransportFactory(mockTransport.Object), loggerFactory: null,
|
logger.LogInformation("Disposing connection");
|
||||||
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
await connection.DisposeAsync().OrTimeout(TimeSpan.FromSeconds(10));
|
||||||
connection.OnReceived(_ => blockReceiveCallbackTcs.Task);
|
logger.LogInformation("Disposed connection");
|
||||||
|
}
|
||||||
await connection.StartAsync();
|
|
||||||
channel.Writer.TryWrite(Array.Empty<byte>());
|
|
||||||
|
|
||||||
// Ensure that SignalR isn't blocked by the receive callback
|
|
||||||
Assert.False(channel.Reader.TryRead(out var message));
|
|
||||||
|
|
||||||
await connection.DisposeAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -406,7 +622,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -449,9 +665,10 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
public async Task ClosedEventNotRaisedWhenTheClientIsStoppedButWasNeverStarted()
|
public async Task ClosedEventNotRaisedWhenTheClientIsStoppedButWasNeverStarted()
|
||||||
{
|
{
|
||||||
var connection = new HttpConnection(new Uri("http://fakeuri.org/"));
|
var connection = new HttpConnection(new Uri("http://fakeuri.org/"));
|
||||||
|
var closeInvoked = false;
|
||||||
|
connection.Closed += e => closeInvoked = true;
|
||||||
await connection.DisposeAsync();
|
await connection.DisposeAsync();
|
||||||
Assert.False(connection.Closed.IsCompleted);
|
Assert.False(closeInvoked);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -464,7 +681,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
|
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -502,7 +719,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
if (IsNegotiateRequest(request))
|
if (ResponseUtils.IsNegotiateRequest(request))
|
||||||
{
|
{
|
||||||
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse());
|
return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse());
|
||||||
}
|
}
|
||||||
|
|
@ -557,7 +774,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
content = "T2:T:42;";
|
content = "T2:T:42;";
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
|
||||||
});
|
});
|
||||||
|
|
@ -584,7 +801,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
|
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: request.Method == HttpMethod.Post
|
: request.Method == HttpMethod.Post
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
||||||
|
|
@ -618,7 +835,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
content = "42";
|
content = "42";
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
|
||||||
});
|
});
|
||||||
|
|
@ -635,18 +852,17 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}, receiveTcs);
|
}, receiveTcs);
|
||||||
|
|
||||||
_ = connection.Closed.ContinueWith(task =>
|
connection.Closed += e =>
|
||||||
{
|
{
|
||||||
if (task.Exception != null)
|
if (e != null)
|
||||||
{
|
{
|
||||||
receiveTcs.TrySetException(task.Exception);
|
receiveTcs.TrySetException(e);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
receiveTcs.TrySetCanceled();
|
receiveTcs.TrySetCanceled();
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
};
|
||||||
});
|
|
||||||
|
|
||||||
await connection.StartAsync().OrTimeout();
|
await connection.StartAsync().OrTimeout();
|
||||||
Assert.Equal("42", await receiveTcs.Task.OrTimeout());
|
Assert.Equal("42", await receiveTcs.Task.OrTimeout());
|
||||||
|
|
@ -674,7 +890,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
content = "42";
|
content = "42";
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
|
||||||
});
|
});
|
||||||
|
|
@ -698,18 +914,17 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
_ = connection.Closed.ContinueWith(task =>
|
connection.Closed += e =>
|
||||||
{
|
{
|
||||||
if (task.Exception != null)
|
if (e != null)
|
||||||
{
|
{
|
||||||
receiveTcs.TrySetException(task.Exception);
|
receiveTcs.TrySetException(e);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
receiveTcs.TrySetCanceled();
|
receiveTcs.TrySetCanceled();
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
};
|
||||||
});
|
|
||||||
|
|
||||||
await connection.StartAsync();
|
await connection.StartAsync();
|
||||||
|
|
||||||
|
|
@ -738,7 +953,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
content = "42";
|
content = "42";
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK, content);
|
||||||
});
|
});
|
||||||
|
|
@ -762,18 +977,17 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
_ = connection.Closed.ContinueWith(task =>
|
connection.Closed += e =>
|
||||||
{
|
{
|
||||||
if (task.Exception != null)
|
if (e != null)
|
||||||
{
|
{
|
||||||
receiveTcs.TrySetException(task.Exception);
|
receiveTcs.TrySetException(e);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
receiveTcs.TrySetCanceled();
|
receiveTcs.TrySetCanceled();
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
};
|
||||||
});
|
|
||||||
|
|
||||||
await connection.StartAsync();
|
await connection.StartAsync();
|
||||||
|
|
||||||
|
|
@ -797,7 +1011,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
|
|
||||||
return request.Method == HttpMethod.Get
|
return request.Method == HttpMethod.Get
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
? ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError)
|
||||||
: IsNegotiateRequest(request)
|
: ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -806,12 +1020,21 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
httpOptions: new HttpOptions { HttpMessageHandler = mockHttpHandler.Object });
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e =>
|
||||||
|
{
|
||||||
|
if (e != null)
|
||||||
|
{
|
||||||
|
closeTcs.SetException(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
closeTcs.SetResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
await connection.StartAsync().OrTimeout();
|
await connection.StartAsync().OrTimeout();
|
||||||
|
await Assert.ThrowsAsync<HttpRequestException>(() => closeTcs.Task.OrTimeout());
|
||||||
// Exception in send should shutdown the connection
|
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(() => connection.Closed.OrTimeout());
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync(new byte[0]));
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.SendAsync(new byte[0]));
|
||||||
|
|
||||||
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
Assert.Equal("Cannot send messages when the connection is not in the Connected state.", exception.Message);
|
||||||
|
|
@ -918,7 +1141,7 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
return IsNegotiateRequest(request)
|
return ResponseUtils.IsNegotiateRequest(request)
|
||||||
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
? ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationResponse())
|
||||||
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
: ResponseUtils.CreateResponse(HttpStatusCode.OK);
|
||||||
});
|
});
|
||||||
|
|
@ -977,11 +1200,5 @@ namespace Microsoft.AspNetCore.Sockets.Client.Tests
|
||||||
await connection.StartAsync().OrTimeout();
|
await connection.StartAsync().OrTimeout();
|
||||||
await connection.DisposeAsync().OrTimeout();
|
await connection.DisposeAsync().OrTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsNegotiateRequest(HttpRequestMessage request)
|
|
||||||
{
|
|
||||||
return request.Method == HttpMethod.Post &&
|
|
||||||
new UriBuilder(request.RequestUri).Path.EndsWith("/negotiate");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
{
|
{
|
||||||
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>();
|
var closedEventTcs = new TaskCompletionSource<Exception>();
|
||||||
|
hubConnection.Closed += e => closedEventTcs.SetResult(e);
|
||||||
|
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
await hubConnection.DisposeAsync().OrTimeout();
|
await hubConnection.DisposeAsync().OrTimeout();
|
||||||
await hubConnection.Closed.OrTimeout();
|
Assert.Null(await closedEventTcs.Task);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -182,9 +183,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
|
||||||
await hubConnection.StartAsync();
|
await hubConnection.StartAsync();
|
||||||
var invokeTask = hubConnection.InvokeAsync<int>("testMethod");
|
var invokeTask = hubConnection.InvokeAsync<int>("testMethod");
|
||||||
await hubConnection.DisposeAsync();
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await invokeTask);
|
var exception = new InvalidOperationException();
|
||||||
|
mockConnection.Raise(m => m.Closed += null, exception);
|
||||||
|
|
||||||
|
var actualException = await Assert.ThrowsAsync<InvalidOperationException>(async () => await invokeTask);
|
||||||
|
Assert.Equal(exception, actualException);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -196,8 +200,11 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
hubConnection.ServerTimeout = TimeSpan.FromMilliseconds(100);
|
hubConnection.ServerTimeout = TimeSpan.FromMilliseconds(100);
|
||||||
|
|
||||||
await hubConnection.StartAsync().OrTimeout();
|
await hubConnection.StartAsync().OrTimeout();
|
||||||
var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await hubConnection.Closed.OrTimeout());
|
|
||||||
Assert.Equal("Server timeout (100.00ms) elapsed without receiving a message from the server.", ex.Message);
|
var closeTcs = new TaskCompletionSource<Exception>();
|
||||||
|
hubConnection.Closed += ex => closeTcs.TrySetResult(ex);
|
||||||
|
var exception = Assert.IsType<TimeoutException>(await closeTcs.Task.OrTimeout());
|
||||||
|
Assert.Equal("Server timeout (100.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
|
// Moq really doesn't handle out parameters well, so to make these tests work I added a manual mock -anurse
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\xunit.runner.json" Link="xunit.runner.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Client\Microsoft.AspNetCore.SignalR.Client.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Client\Microsoft.AspNetCore.SignalR.Client.csproj" />
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Tests.Utils\Microsoft.AspNetCore.SignalR.Tests.Utils.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Tests.Utils\Microsoft.AspNetCore.SignalR.Tests.Utils.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// 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.
|
// 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.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -28,6 +29,12 @@ namespace Microsoft.AspNetCore.Client.Tests
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsNegotiateRequest(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
return request.Method == HttpMethod.Post &&
|
||||||
|
new UriBuilder(request.RequestUri).Path.EndsWith("/negotiate");
|
||||||
|
}
|
||||||
|
|
||||||
public static string CreateNegotiationResponse(string connectionId = "00000000-0000-0000-0000-000000000000",
|
public static string CreateNegotiationResponse(string connectionId = "00000000-0000-0000-0000-000000000000",
|
||||||
SocketsTransportType? transportTypes = SocketsTransportType.All)
|
SocketsTransportType? transportTypes = SocketsTransportType.All)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -75,25 +75,23 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
var mockHttpHandler = new Mock<HttpMessageHandler>();
|
||||||
mockHttpHandler.Protected()
|
mockHttpHandler.Protected()
|
||||||
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
.Returns<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
.Returns<HttpRequestMessage, CancellationToken>((request, cancellationToken) =>
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
var mockStream = new Mock<Stream>();
|
var mockStream = new Mock<Stream>();
|
||||||
mockStream
|
mockStream
|
||||||
.Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
.Setup(s => s.CopyToAsync(It.IsAny<Stream>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||||
.Returns<Stream, int, CancellationToken>(async (stream, bufferSize, t) =>
|
.Returns<Stream, int, CancellationToken>(async (stream, bufferSize, t) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var buffer = Encoding.ASCII.GetBytes("data: 3:abc\r\n\r\n");
|
||||||
|
while (!eventStreamCts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await stream.WriteAsync(buffer, 0, buffer.Length).OrTimeout();
|
||||||
var buffer = Encoding.ASCII.GetBytes("data: 3:abc\r\n\r\n");
|
}
|
||||||
while (!eventStreamCts.IsCancellationRequested)
|
});
|
||||||
{
|
|
||||||
await stream.WriteAsync(buffer, 0, buffer.Length);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mockStream.Setup(s => s.CanRead).Returns(true);
|
mockStream.Setup(s => s.CanRead).Returns(true);
|
||||||
|
|
||||||
return new HttpResponseMessage { Content = new StreamContent(mockStream.Object) };
|
return Task.FromResult(new HttpResponseMessage { Content = new StreamContent(mockStream.Object) });
|
||||||
});
|
});
|
||||||
|
|
||||||
Task transportActiveTask;
|
Task transportActiveTask;
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,16 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
private Task _receiveLoop;
|
private Task _receiveLoop;
|
||||||
|
|
||||||
private TransferMode? _transferMode;
|
private TransferMode? _transferMode;
|
||||||
private readonly TaskCompletionSource<object> _closeTcs = new TaskCompletionSource<object>();
|
|
||||||
|
|
||||||
public Task Closed => _closeTcs.Task;
|
public event Action<Exception> Closed;
|
||||||
public Task Started => _started.Task;
|
public Task Started => _started.Task;
|
||||||
public Task Disposed => _disposed.Task;
|
public Task Disposed => _disposed.Task;
|
||||||
public ChannelReader<byte[]> SentMessages => _sentMessages.Reader;
|
public ChannelReader<byte[]> SentMessages => _sentMessages.Reader;
|
||||||
public ChannelWriter<byte[]> ReceivedMessages => _receivedMessages.Writer;
|
public ChannelWriter<byte[]> ReceivedMessages => _receivedMessages.Writer;
|
||||||
|
|
||||||
|
private bool _closed;
|
||||||
|
private object _closedLock = new object();
|
||||||
|
|
||||||
private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>();
|
private readonly List<ReceiveCallback> _callbacks = new List<ReceiveCallback>();
|
||||||
|
|
||||||
public IFeatureCollection Features { get; } = new FeatureCollection();
|
public IFeatureCollection Features { get; } = new FeatureCollection();
|
||||||
|
|
@ -51,19 +53,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
public Task AbortAsync(Exception ex) => DisposeCoreAsync(ex);
|
public Task AbortAsync(Exception ex) => DisposeCoreAsync(ex);
|
||||||
public Task DisposeAsync() => DisposeCoreAsync();
|
public Task DisposeAsync() => DisposeCoreAsync();
|
||||||
|
|
||||||
|
// TestConnection isn't restartable
|
||||||
|
public Task StopAsync() => DisposeAsync();
|
||||||
|
|
||||||
private Task DisposeCoreAsync(Exception ex = null)
|
private Task DisposeCoreAsync(Exception ex = null)
|
||||||
{
|
{
|
||||||
if (ex == null)
|
TriggerClosed(ex);
|
||||||
{
|
|
||||||
_closeTcs.TrySetResult(null);
|
|
||||||
_disposed.TrySetResult(null);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_closeTcs.TrySetException(ex);
|
|
||||||
_disposed.TrySetException(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
_receiveShutdownToken.Cancel();
|
_receiveShutdownToken.Cancel();
|
||||||
return _receiveLoop;
|
return _receiveLoop;
|
||||||
}
|
}
|
||||||
|
|
@ -147,16 +142,28 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_closeTcs.TrySetResult(null);
|
TriggerClosed();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Do nothing, we were just asked to shut down.
|
// Do nothing, we were just asked to shut down.
|
||||||
_closeTcs.TrySetResult(null);
|
TriggerClosed();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_closeTcs.TrySetException(ex);
|
TriggerClosed(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TriggerClosed(Exception ex = null)
|
||||||
|
{
|
||||||
|
lock (_closedLock)
|
||||||
|
{
|
||||||
|
if (!_closed)
|
||||||
|
{
|
||||||
|
_closed = true;
|
||||||
|
Closed?.Invoke(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Client.Tests;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
|
{
|
||||||
|
public class TestHttpMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (ResponseUtils.IsNegotiateRequest(request))
|
||||||
|
{
|
||||||
|
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.OK,
|
||||||
|
ResponseUtils.CreateNegotiationResponse()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Task.FromException<HttpResponseMessage>(new InvalidOperationException($"Http endpoint not implemented: {request.RequestUri}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\xunit.runner.json" Link="xunit.runner.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.csproj" />
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Tests.Utils\Microsoft.AspNetCore.SignalR.Tests.Utils.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Tests.Utils\Microsoft.AspNetCore.SignalR.Tests.Utils.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\xunit.runner.json" Link="xunit.runner.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Redis\Microsoft.AspNetCore.SignalR.Redis.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Redis\Microsoft.AspNetCore.SignalR.Redis.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Sockets\Microsoft.AspNetCore.Sockets.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Sockets\Microsoft.AspNetCore.Sockets.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,19 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
||||||
new TransferModeFeature { TransferMode = requestedTransferMode });
|
new TransferModeFeature { TransferMode = requestedTransferMode });
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e =>
|
||||||
|
{
|
||||||
|
if (e != null)
|
||||||
|
{
|
||||||
|
closeTcs.SetException(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
closeTcs.SetResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var receiveTcs = new TaskCompletionSource<string>();
|
var receiveTcs = new TaskCompletionSource<string>();
|
||||||
connection.OnReceived((data, state) =>
|
connection.OnReceived((data, state) =>
|
||||||
{
|
{
|
||||||
|
|
@ -208,7 +221,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
||||||
logger.LogInformation("Receiving message");
|
logger.LogInformation("Receiving message");
|
||||||
Assert.Equal(message, await receiveTcs.Task.OrTimeout());
|
Assert.Equal(message, await receiveTcs.Task.OrTimeout());
|
||||||
logger.LogInformation("Completed receive");
|
logger.LogInformation("Completed receive");
|
||||||
await connection.Closed.OrTimeout();
|
await closeTcs.Task.OrTimeout();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -325,11 +338,22 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var closeTcs = new TaskCompletionSource<object>();
|
var closeTcs = new TaskCompletionSource<object>();
|
||||||
|
connection.Closed += e =>
|
||||||
|
{
|
||||||
|
if (e != null)
|
||||||
|
{
|
||||||
|
closeTcs.SetException(e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
closeTcs.SetResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
logger.LogInformation("Starting connection to {url}", url);
|
logger.LogInformation("Starting connection to {url}", url);
|
||||||
await connection.StartAsync().OrTimeout();
|
await connection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
await connection.Closed.OrTimeout();
|
await closeTcs.Task.OrTimeout();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,12 @@
|
||||||
<PackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
<PackageReference Include="Microsoft.CSharp" Version="$(MicrosoftCSharpPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\xunit.runner.json" Link="xunit.runner.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR\Microsoft.AspNetCore.SignalR.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Client\Microsoft.AspNetCore.SignalR.Client.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.SignalR.Client\Microsoft.AspNetCore.SignalR.Client.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@
|
||||||
<RuntimeIdentifier Condition="'$(TargetFramework)' == 'net461'">win7-x86</RuntimeIdentifier>
|
<RuntimeIdentifier Condition="'$(TargetFramework)' == 'net461'">win7-x86</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\xunit.runner.json" Link="xunit.runner.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Sockets.Http\Microsoft.AspNetCore.Sockets.Http.csproj" />
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Sockets.Http\Microsoft.AspNetCore.Sockets.Http.csproj" />
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Tests.Utils\Microsoft.AspNetCore.SignalR.Tests.Utils.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.SignalR.Tests.Utils\Microsoft.AspNetCore.SignalR.Tests.Utils.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"longRunningTestSeconds": 5,
|
||||||
|
"diagnosticMessages": false
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue