This commit is contained in:
Muthuveer Somanathan 2021-10-14 21:59:07 -07:00 коммит произвёл GitHub
Родитель 76ccffdd1f
Коммит da8348a7b4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
59 изменённых файлов: 6179 добавлений и 70 удалений

Просмотреть файл

@ -40,6 +40,7 @@
<!--<ModulePath>.*\Microsoft.Bot.Configuration.dll$</ModulePath>-->
<ModulePath>.*\Microsoft.Bot.Connector.dll$</ModulePath>
<ModulePath>.*\Microsoft.Bot.Schema.dll$</ModulePath>
<ModulePath>.*\Microsoft.Bot.Connector.Streaming.dll$</ModulePath>
<ModulePath>.*\Microsoft.Bot.Streaming.dll$</ModulePath>
</Include>
<Exclude>

Просмотреть файл

@ -225,6 +225,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime", "libraries\Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime\Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime.csproj", "{2DB4E5B0-3209-425E-A912-005A330CC66A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Streaming", "libraries\Microsoft.Bot.Connector.Streaming\Microsoft.Bot.Connector.Streaming.csproj", "{80FA0E50-8F81-4C60-B265-1039391C1CEE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Streaming.Tests", "tests\Microsoft.Bot.Connector.Streaming.Tests\Microsoft.Bot.Connector.Streaming.Tests.csproj", "{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Streaming", "Streaming", "{EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Streaming.Tests.Server", "tests\Microsoft.Bot.Connector.Streaming.Tests.Server\Microsoft.Bot.Connector.Streaming.Tests.Server.csproj", "{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Streaming.Tests.Client", "tests\Microsoft.Bot.Connector.Streaming.Tests.Client\Microsoft.Bot.Connector.Streaming.Tests.Client.csproj", "{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Connector.Streaming.Perf", "tests\Microsoft.Bot.Connector.Streaming.Perf\Microsoft.Bot.Connector.Streaming.Perf.csproj", "{B49A3201-5BEE-426C-A082-D92D52172E06}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -881,6 +893,46 @@ Global
{2DB4E5B0-3209-425E-A912-005A330CC66A}.Release|Any CPU.Build.0 = Release|Any CPU
{2DB4E5B0-3209-425E-A912-005A330CC66A}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{2DB4E5B0-3209-425E-A912-005A330CC66A}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{80FA0E50-8F81-4C60-B265-1039391C1CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80FA0E50-8F81-4C60-B265-1039391C1CEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80FA0E50-8F81-4C60-B265-1039391C1CEE}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{80FA0E50-8F81-4C60-B265-1039391C1CEE}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{80FA0E50-8F81-4C60-B265-1039391C1CEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80FA0E50-8F81-4C60-B265-1039391C1CEE}.Release|Any CPU.Build.0 = Release|Any CPU
{80FA0E50-8F81-4C60-B265-1039391C1CEE}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{80FA0E50-8F81-4C60-B265-1039391C1CEE}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}.Release|Any CPU.Build.0 = Release|Any CPU
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}.Release|Any CPU.Build.0 = Release|Any CPU
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}.Release|Any CPU.Build.0 = Release|Any CPU
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release|Any CPU.Build.0 = Release|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -979,6 +1031,12 @@ Global
{0BF5E92D-D034-4D80-8921-07627F55F412} = {C40A300C-8988-4733-A760-A776C6309B57}
{D611AC03-9859-4EB6-BAB9-C26F493DFDB3} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{2DB4E5B0-3209-425E-A912-005A330CC66A} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{80FA0E50-8F81-4C60-B265-1039391C1CEE} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{9EBA6EDB-7D67-4BC5-9F94-E0162A538CC7} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
{EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
{B49A3201-5BEE-426C-A082-D92D52172E06} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}

Просмотреть файл

@ -13,7 +13,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime
public CoreBotAdapter(
BotFrameworkAuthentication botFrameworkAuthentication,
IEnumerable<IMiddleware> middlewares,
ILogger logger = null)
ILogger<CoreBotAdapter> logger = null)
: base(botFrameworkAuthentication, logger)
{
// Pick up feature based middlewares such as telemetry or transcripts

Просмотреть файл

@ -233,6 +233,23 @@ namespace Microsoft.Bot.Builder
}
}
/// <summary>
/// Gets the correct streaming connector factory that is processing the given activity.
/// </summary>
/// <param name="activity">The activity that is being processed.</param>
/// <returns>The Streaming Connector Factory responsible for processing the activity.</returns>
/// <remarks>
/// For HTTP requests, we usually create a new connector factory and reply to the activity over a new HTTP request.
/// However, when processing activities over a streaming connection, we need to reply over the same connection that is talking to a web socket.
/// This method will look up all active streaming connections in cloud adapter and return the connector factory that is processing the activity.
/// Messages between bot and channel go through the StreamingConnection (bot -> channel) and RequestHandler (channel -> bot), both created by the adapter.
/// However, proactive messages don't know which connection to talk to, so this method is designed to aid in the connection resolution for such proactive messages.
/// </remarks>
protected virtual ConnectorFactory GetStreamingConnectorFactory(Activity activity)
{
throw new NotImplementedException();
}
/// <summary>
/// The implementation for continue conversation.
/// </summary>
@ -247,7 +264,9 @@ namespace Microsoft.Bot.Builder
Logger.LogInformation($"ProcessProactiveAsync for Conversation Id: {continuationActivity.Conversation.Id}");
// Create the connector factory.
var connectorFactory = BotFrameworkAuthentication.CreateConnectorFactory(claimsIdentity);
var connectorFactory = continuationActivity.IsFromStreamingConnection()
? GetStreamingConnectorFactory(continuationActivity)
: BotFrameworkAuthentication.CreateConnectorFactory(claimsIdentity);
// Create the connector client to use for outbound requests.
using (var connectorClient = await connectorFactory.CreateAsync(continuationActivity.ServiceUrl, audience, cancellationToken).ConfigureAwait(false))

Просмотреть файл

@ -24,6 +24,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Bot.Streaming" Condition=" '$(ReleasePackageVersion)' == '' " Version="$(LocalPackageVersion)" />
<PackageReference Include="Microsoft.Bot.Streaming" Condition=" '$(ReleasePackageVersion)' != '' " Version="$(ReleasePackageVersion)" />
<PackageReference Include="Microsoft.Bot.Connector.Streaming" Condition=" '$(ReleasePackageVersion)' == '' " Version="$(LocalPackageVersion)" />
<PackageReference Include="Microsoft.Bot.Connector.Streaming" Condition=" '$(ReleasePackageVersion)' != '' " Version="$(ReleasePackageVersion)" />
<PackageReference Include="Microsoft.Bot.Connector" Condition=" '$(ReleasePackageVersion)' == '' " Version="$(LocalPackageVersion)" />
<PackageReference Include="Microsoft.Bot.Connector" Condition=" '$(ReleasePackageVersion)' != '' " Version="$(ReleasePackageVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
@ -33,6 +35,7 @@
<ItemGroup>
<ProjectReference Include="..\Microsoft.Bot.Connector\Microsoft.Bot.Connector.csproj" />
<ProjectReference Include="..\Microsoft.Bot.Streaming\Microsoft.Bot.Streaming.csproj" />
<ProjectReference Include="..\Microsoft.Bot.Connector.Streaming\Microsoft.Bot.Connector.Streaming.csproj" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -22,8 +22,10 @@ namespace Microsoft.Bot.Builder.Streaming
/// <summary>
/// An HTTP adapter base class.
/// </summary>
public class BotFrameworkHttpAdapterBase : BotFrameworkAdapter, IStreamingActivityProcessor
public class BotFrameworkHttpAdapterBase : BotFrameworkAdapter, IStreamingActivityProcessor, IDisposable
{
private bool _disposedValue;
/// <summary>
/// Initializes a new instance of the <see cref="BotFrameworkHttpAdapterBase"/> class.
/// </summary>
@ -220,7 +222,9 @@ namespace Microsoft.Bot.Builder.Streaming
var host = uri[uri.Length - 1];
await connection.ConnectAsync(new Uri(protocol + host + "/api/messages"), cancellationToken).ConfigureAwait(false);
#pragma warning disable CA2000 // Dispose objects before losing scope (We'll dispose this when the adapter gets disposed or when elements are removed)
var handler = new StreamingRequestHandler(ConnectedBot, this, connection, Logger);
#pragma warning restore CA2000 // Dispose objects before losing scope
if (RequestHandlers == null)
{
@ -259,12 +263,49 @@ namespace Microsoft.Bot.Builder.Streaming
RequestHandlers = new List<StreamingRequestHandler>();
}
#pragma warning disable CA2000 // Dispose objects before losing scope (We'll dispose this when the adapter gets disposed or when elements are removed)
var requestHandler = new StreamingRequestHandler(bot, this, pipeName, audience, Logger);
#pragma warning restore CA2000 // Dispose objects before losing scope
RequestHandlers.Add(requestHandler);
await requestHandler.ListenAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes resources of the <see cref="StreamingRequestHandler"/>.
/// </summary>
/// <param name="disposing">Whether we are disposing managed resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
if (RequestHandlers != null)
{
foreach (var handler in RequestHandlers)
{
if (handler is IDisposable disposable)
{
handler.Dispose();
}
}
}
}
RequestHandlers = null;
_disposedValue = true;
}
}
/// <summary>
/// Evaluates if processing an outgoing activity is possible.
/// </summary>

Просмотреть файл

@ -14,11 +14,10 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Connector.Streaming.Application;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Streaming;
using Microsoft.Bot.Streaming.Transport;
using Microsoft.Bot.Streaming.Transport.NamedPipes;
using Microsoft.Bot.Streaming.Transport.WebSockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
@ -29,19 +28,34 @@ namespace Microsoft.Bot.Builder.Streaming
/// A request handler that processes incoming requests sent over an IStreamingTransport
/// and adheres to the Bot Framework Protocol v3 with Streaming Extensions.
/// </summary>
public class StreamingRequestHandler : RequestHandler
public class StreamingRequestHandler : RequestHandler, IDisposable
{
private static ConcurrentDictionary<string, StreamingRequestHandler> _requestHandlers = new ConcurrentDictionary<string, StreamingRequestHandler>();
private readonly string _instanceId = Guid.NewGuid().ToString();
private readonly IBot _bot;
private readonly ILogger _logger;
private readonly IStreamingActivityProcessor _activityProcessor;
private readonly string _userAgent;
private readonly ConcurrentDictionary<string, DateTime> _conversations;
private readonly IStreamingTransportServer _server;
private readonly string _userAgent = GetUserAgent();
private readonly ConcurrentDictionary<string, DateTime> _conversations = new ConcurrentDictionary<string, DateTime>();
private readonly StreamingConnection _innerConnection;
private bool _serverIsConnected;
private bool _disposedValue;
/// <summary>
/// Initializes a new instance of the <see cref="StreamingRequestHandler"/> class.
/// </summary>
/// <param name="bot">The bot for which we handle requests.</param>
/// <param name="activityProcessor">The processor for incoming requests.</param>
/// <param name="connection">Connection used to send requests to the transport.</param>
/// <param name="audience">The specified recipient of all outgoing activities.</param>
/// <param name="logger">Logger implementation for tracing and debugging information.</param>
public StreamingRequestHandler(IBot bot, IStreamingActivityProcessor activityProcessor, StreamingConnection connection, string audience = null, ILogger logger = null)
{
_bot = bot ?? throw new ArgumentNullException(nameof(bot));
_activityProcessor = activityProcessor ?? throw new ArgumentNullException(nameof(activityProcessor));
_innerConnection = connection ?? throw new ArgumentNullException(nameof(connection));
_logger = logger ?? NullLogger.Instance;
Audience = audience;
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamingRequestHandler"/> class and
@ -85,11 +99,7 @@ namespace Microsoft.Bot.Builder.Streaming
Audience = audience;
_logger = logger ?? NullLogger.Instance;
_conversations = new ConcurrentDictionary<string, DateTime>();
_userAgent = GetUserAgent();
_server = new WebSocketServer(socket, this);
_serverIsConnected = true;
_server.Disconnected += ServerDisconnected;
_innerConnection = new LegacyStreamingConnection(socket, _logger, ServerDisconnected);
}
/// <summary>
@ -134,11 +144,7 @@ namespace Microsoft.Bot.Builder.Streaming
}
Audience = audience;
_conversations = new ConcurrentDictionary<string, DateTime>();
_userAgent = GetUserAgent();
_server = new NamedPipeServer(pipeName, this);
_serverIsConnected = true;
_server.Disconnected += ServerDisconnected;
_innerConnection = new LegacyStreamingConnection(pipeName, _logger, ServerDisconnected);
}
/// <summary>
@ -165,11 +171,19 @@ namespace Microsoft.Bot.Builder.Streaming
/// <returns>A task that completes once the server is no longer listening.</returns>
public virtual async Task ListenAsync()
{
await _server.StartAsync().ConfigureAwait(false);
_logger.LogInformation("Streaming request handler started listening");
await ListenAsync(CancellationToken.None).ConfigureAwait(false);
}
// add ourselves to a global collection to ensure a reference is maintained if we are connected
_requestHandlers.TryAdd(_instanceId, this);
/// <summary>
/// Begins listening for incoming requests over this StreamingRequestHandler's server.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that completes once the server is no longer listening.</returns>
public async Task ListenAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Streaming request handler started listening");
await _innerConnection.ListenAsync(this, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Streaming request handler completed listening");
}
/// <summary>
@ -403,15 +417,7 @@ namespace Microsoft.Bot.Builder.Streaming
}
}
if (!_serverIsConnected)
{
throw new InvalidOperationException("Error while attempting to send: Streaming transport is disconnected.");
}
// Attempt to send the request. If send fails, we let the original exception get thrown so that the
// upper layers can handle it and trigger OnError. This is consistent with error handling in http and proactive
// paths, making all 3 paths consistent in terms of error handling.
var serverResponse = await _server.SendAsync(request, cancellationToken).ConfigureAwait(false);
var serverResponse = await _innerConnection.SendStreamingRequestAsync(request, cancellationToken).ConfigureAwait(false);
if (serverResponse.StatusCode == (int)HttpStatusCode.OK)
{
@ -431,12 +437,35 @@ namespace Microsoft.Bot.Builder.Streaming
/// <returns>A task that resolves to a <see cref="ReceiveResponse"/>.</returns>
public Task<ReceiveResponse> SendStreamingRequestAsync(StreamingRequest request, CancellationToken cancellationToken = default)
{
if (!_serverIsConnected)
{
throw new InvalidOperationException("Error while attempting to send: Streaming transport is disconnected.");
}
return _innerConnection.SendStreamingRequestAsync(request, cancellationToken);
}
return _server.SendAsync(request, cancellationToken);
/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes resources of the <see cref="StreamingRequestHandler"/>.
/// </summary>
/// <param name="disposing">Whether we are disposing managed resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
if (_innerConnection is IDisposable disposable)
{
disposable?.Dispose();
}
}
_disposedValue = true;
}
}
/// <summary>
@ -446,10 +475,7 @@ namespace Microsoft.Bot.Builder.Streaming
/// <param name="e">The arguments specified by the disconnection event.</param>
protected virtual void ServerDisconnected(object sender, DisconnectedEventArgs e)
{
_serverIsConnected = false;
// remove ourselves from the global collection
_requestHandlers.TryRemove(_instanceId, out var _);
// Subtypes can override this method to add logging when an underlying transport server is disconnected
}
/// <summary>

Просмотреть файл

@ -0,0 +1,160 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Streaming;
using Microsoft.Bot.Streaming.Transport;
using Microsoft.Bot.Streaming.Transport.NamedPipes;
using Microsoft.Bot.Streaming.Transport.WebSockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Bot.Connector.Streaming.Application
{
/// <summary>
/// The <see cref="StreamingConnection"/> to be used by legacy bots.
/// </summary>
[Obsolete("Use `WebSocketStreamingConnection` instead.", false)]
public class LegacyStreamingConnection : StreamingConnection, IDisposable
{
private readonly WebSocket _socket;
private readonly string _pipeName;
private readonly ILogger _logger;
private readonly DisconnectedEventHandler _onServerDisconnect;
private IStreamingTransportServer _server;
private bool _serverIsConnected;
private bool _disposedValue;
/// <summary>
/// Initializes a new instance of the <see cref="LegacyStreamingConnection"/> class that uses web sockets.
/// </summary>
/// <param name="socket">The <see cref="WebSocket"/> instance to use for legacy streaming connection.</param>
/// <param name="logger">Logger implementation for tracing and debugging information.</param>
/// <param name="onServerDisconnect">Additional handling code to be run when the transport server is disconnected.</param>
public LegacyStreamingConnection(WebSocket socket, ILogger logger, DisconnectedEventHandler onServerDisconnect = null)
{
_socket = socket ?? throw new ArgumentNullException(nameof(socket));
_logger = logger ?? NullLogger.Instance;
_onServerDisconnect = onServerDisconnect;
}
/// <summary>
/// Initializes a new instance of the <see cref="LegacyStreamingConnection"/> class that uses named pipes.
/// </summary>
/// <param name="pipeName">The name of the named pipe.</param>
/// <param name="logger">Logger implementation for tracing and debugging information.</param>
/// <param name="onServerDisconnect">Additional handling code to be run when the transport server is disconnected.</param>
public LegacyStreamingConnection(string pipeName, ILogger logger, DisconnectedEventHandler onServerDisconnect = null)
{
if (string.IsNullOrWhiteSpace(pipeName))
{
throw new ArgumentNullException(nameof(pipeName));
}
_pipeName = pipeName;
_logger = logger ?? NullLogger.Instance;
_onServerDisconnect = onServerDisconnect;
}
/// <inheritdoc />
public override async Task ListenAsync(RequestHandler requestHandler, CancellationToken cancellationToken = default)
{
_server = CreateStreamingTransportServer(requestHandler);
_serverIsConnected = true;
_server.Disconnected += Server_Disconnected;
if (_onServerDisconnect != null)
{
_server.Disconnected += _onServerDisconnect;
}
await _server.StartAsync().ConfigureAwait(false);
}
/// <inheritdoc />
public override async Task<ReceiveResponse> SendStreamingRequestAsync(StreamingRequest request, CancellationToken cancellationToken = default)
{
if (!_serverIsConnected)
{
throw new InvalidOperationException("Error while attempting to send: Streaming transport is disconnected.");
}
return await _server.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
internal virtual IStreamingTransportServer CreateStreamingTransportServer(RequestHandler requestHandler)
{
if (_socket != null)
{
return new WebSocketServer(_socket, requestHandler);
}
if (!string.IsNullOrWhiteSpace(_pipeName))
{
return new NamedPipeServer(_pipeName, requestHandler);
}
throw new ApplicationException("Neither web socket, nor named pipe found to instantiate a streaming transport server!");
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to catch all exceptions while disconnecting.")]
#pragma warning disable CA1063 // Implement IDisposable Correctly
private void Dispose(bool disposing)
#pragma warning restore CA1063 // Implement IDisposable Correctly
{
if (!_disposedValue)
{
if (disposing)
{
try
{
if (_server != null)
{
if (_server is WebSocketServer webSocketServer)
{
webSocketServer.Disconnect();
}
else if (_server is NamedPipeServer namedPipeServer)
{
namedPipeServer.Disconnect();
}
if (_server is IDisposable disposable)
{
disposable.Dispose();
}
_server.Disconnected -= Server_Disconnected;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to gracefully disconnect server while tearing down streaming connection.");
}
}
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
_disposedValue = true;
}
}
private void Server_Disconnected(object sender, DisconnectedEventArgs e)
{
_serverIsConnected = false;
}
}
}

Просмотреть файл

@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Streaming;
namespace Microsoft.Bot.Connector.Streaming.Application
{
/// <summary>
/// A streaming based connection that can listen for incoming requests and send them to a <see cref="RequestHandler"/>,
/// and can also send requests to the other end of the connection.
/// </summary>
public abstract class StreamingConnection
{
/// <summary>
/// Sends a streaming request through the connection.
/// </summary>
/// <param name="request"><see cref="StreamingRequest"/> to be sent.</param>
/// <param name="cancellationToken"><see cref="CancellationToken"/> to cancel the send process.</param>
/// <returns>The <see cref="ReceiveResponse"/> returned from the client.</returns>
public abstract Task<ReceiveResponse> SendStreamingRequestAsync(StreamingRequest request, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Opens the <see cref="StreamingConnection"/> and listens for incoming requests, which will
/// be assembled and sent to the provided <see cref="RequestHandler"/>.
/// </summary>
/// <param name="requestHandler"><see cref="RequestHandler"/> to which incoming requests will be sent.</param>
/// <param name="cancellationToken"><see cref="CancellationToken"/> that signals the need to stop the connection.
/// Once the token is cancelled, the connection will be gracefully shut down, finishing pending sends and receives.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public abstract Task ListenAsync(RequestHandler requestHandler, CancellationToken cancellationToken = default(CancellationToken));
}
}

Просмотреть файл

@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Bot.Connector.Streaming.Application
{
// Reusing internal awaitable timer from https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/common/Shared/TimerAwaitable.cs
internal class TimerAwaitable : IDisposable, INotifyCompletion
{
private static readonly Action _callbackCompleted = () => { };
private Timer _timer;
private Action _callback;
private readonly TimeSpan _period;
private readonly TimeSpan _dueTime;
private readonly object _lockObj = new object();
private bool _disposed;
private bool _running = true;
public TimerAwaitable(TimeSpan dueTime, TimeSpan period)
{
_dueTime = dueTime;
_period = period;
}
public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);
public void Start()
{
if (_timer == null)
{
lock (_lockObj)
{
if (_disposed)
{
return;
}
if (_timer == null)
{
// This fixes the cycle by using a WeakReference to the state object. The object graph now looks like this:
// Timer -> TimerHolder -> TimerQueueTimer -> WeakReference<TimerAwaitable> -> Timer -> ...
// If TimerAwaitable falls out of scope, the timer should be released.
_timer = NonCapturingTimer.Create(
state =>
{
var weakRef = (WeakReference<TimerAwaitable>)state!;
if (weakRef.TryGetTarget(out var thisRef))
{
thisRef.Tick();
}
},
state: new WeakReference<TimerAwaitable>(this),
dueTime: _dueTime,
period: _period);
}
}
}
}
public TimerAwaitable GetAwaiter() => this;
public bool GetResult()
{
_callback = null;
return _running;
}
public void OnCompleted(Action continuation)
{
if (ReferenceEquals(_callback, _callbackCompleted) ||
ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted))
{
_ = Task.Run(continuation);
}
}
public void UnsafeOnCompleted(Action continuation)
{
OnCompleted(continuation);
}
public void Stop()
{
lock (_lockObj)
{
// Stop should be used to trigger the call to end the loop which disposes
if (_disposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
_running = false;
}
// Call tick here to make sure that we yield the callback,
// if it's currently waiting, we don't need to wait for the next period
Tick();
}
void IDisposable.Dispose()
{
lock (_lockObj)
{
_disposed = true;
_timer?.Dispose();
_timer = null;
}
}
private void Tick()
{
var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);
continuation?.Invoke();
}
// A convenience API for interacting with System.Threading.Timer in a way
// that doesn't capture the ExecutionContext. We should be using this (or equivalent)
// everywhere we use timers to avoid rooting any values stored in asynclocals.
private static class NonCapturingTimer
{
public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)
{
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}
// Don't capture the current ExecutionContext and its AsyncLocals onto the timer
bool restoreFlow = false;
try
{
if (!ExecutionContext.IsFlowSuppressed())
{
ExecutionContext.SuppressFlow();
restoreFlow = true;
}
return new Timer(callback, state, dueTime, period);
}
finally
{
// Restore the current ExecutionContext
if (restoreFlow)
{
ExecutionContext.RestoreFlow();
}
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,323 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Session;
using Microsoft.Bot.Connector.Streaming.Transport;
using Microsoft.Bot.Streaming;
using Microsoft.Bot.Streaming.Transport;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using static Microsoft.Bot.Connector.Streaming.Transport.DuplexPipe;
namespace Microsoft.Bot.Connector.Streaming.Application
{
/// <summary>
/// Web socket client.
/// </summary>
public class WebSocketClient : IStreamingTransportClient
{
private readonly string _url;
private readonly RequestHandler _requestHandler;
private readonly ILogger _logger;
private readonly TimeSpan _closeTimeout;
private readonly TimeSpan? _keepAlive;
private CancellationTokenSource _disconnectCts;
private StreamingSession _session;
private TransportHandler _transportHandler;
private DuplexPipePair _duplexPipePair;
private volatile bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketClient"/> class.
/// </summary>
/// <param name="url">Url to connect to.</param>
/// <param name="requestHandler">Request handler that will receive incoming requests to this client instance.</param>
/// <param name="closeTimeOut">Optional time out for closing the client connection.</param>
/// <param name="keepAlive">Optional spacing between keep alives for proactive disconnection detection. If null is provided, no keep alives will be sent.</param>
/// <param name="logger"><see cref="ILogger"/> for the client.</param>
public WebSocketClient(string url, RequestHandler requestHandler, TimeSpan? closeTimeOut = null, TimeSpan? keepAlive = null, ILogger logger = null)
{
if (string.IsNullOrEmpty(url))
{
throw new ArgumentNullException(nameof(url));
}
_url = url;
_requestHandler = requestHandler ?? throw new ArgumentNullException(nameof(requestHandler));
_logger = logger ?? NullLogger.Instance;
_closeTimeout = closeTimeOut ?? TimeSpan.FromSeconds(15);
_keepAlive = keepAlive;
}
internal WebSocketClient(RequestHandler requestHandler, TimeSpan? closeTimeOut = null, TimeSpan? keepAlive = null, ILogger logger = null)
{
_requestHandler = requestHandler ?? throw new ArgumentNullException(nameof(requestHandler));
_logger = logger ?? NullLogger.Instance;
_closeTimeout = closeTimeOut ?? TimeSpan.FromSeconds(15);
_keepAlive = keepAlive;
}
/// <inheritdoc/>
public event DisconnectedEventHandler Disconnected;
/// <inheritdoc/>
public bool IsConnected { get; set; } = false;
/// <inheritdoc/>
public async Task ConnectAsync()
{
await ConnectAsync(new Dictionary<string, string>(), CancellationToken.None).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task ConnectAsync(IDictionary<string, string> requestHeaders)
{
await ConnectAsync(requestHeaders, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// Establishes the connection.
/// </summary>
/// <param name="requestHeaders">Request headers.</param>
/// <param name="cancellationToken"><see cref="CancellationToken"/> for the client connection.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task ConnectAsync(IDictionary<string, string> requestHeaders, CancellationToken cancellationToken)
{
await ConnectInternalAsync(
connectFunc: transport => transport.ConnectAsync(_url, requestHeaders, CancellationToken.None),
cancellationToken: cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<ReceiveResponse> SendAsync(StreamingRequest message, CancellationToken cancellationToken = default)
{
CheckDisposed();
if (_session == null)
{
throw new InvalidOperationException("Session not established. Call ConnectAsync() in order to send requests through this client.");
}
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
return await _session.SendRequestAsync(message, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public void Disconnect()
{
CheckDisposed();
DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
/// <summary>
/// Disconnects.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task DisconnectAsync()
{
CheckDisposed();
await _transportHandler.StopAsync().ConfigureAwait(false);
IsConnected = false;
}
/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
internal async Task ConnectInternalAsync(WebSocket clientSocket, CancellationToken cancellationToken)
{
await ConnectInternalAsync(
connectFunc: transport => transport.ProcessSocketAsync(clientSocket, CancellationToken.None),
cancellationToken: cancellationToken).ConfigureAwait(false);
}
internal async Task ConnectInternalAsync(Func<WebSocketTransport, Task> connectFunc, CancellationToken cancellationToken)
{
CheckDisposed();
TimerAwaitable timer = null;
Task timerTask = null;
try
{
// Pipes
_duplexPipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
// Transport
var transport = new WebSocketTransport(_duplexPipePair.Application, _logger);
// Application
_transportHandler = new TransportHandler(_duplexPipePair.Transport, _logger);
// Session
_session = new StreamingSession(_requestHandler, _transportHandler, _logger);
// Set up cancellation
_disconnectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Start transport and application
var transportTask = connectFunc(transport);
var applicationTask = _transportHandler.ListenAsync(_disconnectCts.Token);
var combinedTask = Task.WhenAll(transportTask, applicationTask);
Log.ClientStarted(_logger, _url ?? string.Empty);
// Periodic task: keep alive
// Disposed with `timer.Stop()` in the finally block below
if (_keepAlive.HasValue)
{
timer = new TimerAwaitable(_keepAlive.Value, _keepAlive.Value);
timerTask = TimerLoopAsync(timer);
}
// We are connected!
IsConnected = true;
// Block until transport or application ends.
await combinedTask.ConfigureAwait(false);
// Signal that we're done
_disconnectCts.Cancel();
Log.ClientTransportApplicationCompleted(_logger, _url);
}
finally
{
timer?.Stop();
if (timerTask != null)
{
await timerTask.ConfigureAwait(false);
}
}
Log.ClientCompleted(_logger, _url ?? string.Empty);
}
internal async Task TimerLoopAsync(TimerAwaitable timer)
{
timer.Start();
using (timer)
{
// await returns True until `timer.Stop()` is called in the `finally` block of `ReceiveLoop`
while (await timer)
{
try
{
// Ping server
var response = await _session.SendRequestAsync(StreamingRequest.CreateGet("/api/version"), _disconnectCts.Token).ConfigureAwait(false);
if (!IsSuccessResponse(response))
{
Log.ClientKeepAliveFail(_logger, _url, response.StatusCode);
IsConnected = false;
Disconnected?.Invoke(this, new DisconnectedEventArgs() { Reason = $"Received failure from server heartbeat: {response.StatusCode}." });
}
else
{
Log.ClientKeepAliveSucceed(_logger, _url);
}
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception e)
#pragma warning restore CA1031 // Do not catch general exception types
{
Log.ClientKeepAliveFail(_logger, _url, 0, e);
IsConnected = false;
Disconnected?.Invoke(this, new DisconnectedEventArgs() { Reason = $"Received failure from server heartbeat: {e}." });
}
}
}
}
/// <summary>
/// Disposes objected used by the class.
/// </summary>
/// <param name="disposing">A Boolean that indicates whether the method call comes from a Dispose method (its value is true) or from a finalizer (its value is false).</param>
/// <remarks>
/// The disposing parameter should be false when called from a finalizer, and true when called from the IDisposable.Dispose method.
/// In other words, it is true when deterministically called and false when non-deterministically called.
/// </remarks>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
try
{
Disconnect();
_disconnectCts.Cancel();
}
finally
{
_transportHandler.Dispose();
_disconnectCts.Dispose();
}
}
_disposed = true;
}
private static bool IsSuccessResponse(ReceiveResponse response)
{
return response != null && response.StatusCode >= 200 && response.StatusCode <= 299;
}
private void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
}
private class Log
{
private static readonly Action<ILogger, string, Exception> _clientStarted =
LoggerMessage.Define<string>(LogLevel.Information, new EventId(1, nameof(ClientStarted)), "WebSocket client connected to {string}.");
private static readonly Action<ILogger, string, Exception> _clientCompleted =
LoggerMessage.Define<string>(LogLevel.Information, new EventId(2, nameof(ClientKeepAliveSucceed)), "WebSocket client connection to {string} closed.");
private static readonly Action<ILogger, string, Exception> _clientKeepAliveSucceed =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(3, nameof(ClientStarted)), "WebSocket client heartbeat to {string} succeeded.");
private static readonly Action<ILogger, string, int, Exception> _clientKeepAliveFail =
LoggerMessage.Define<string, int>(LogLevel.Error, new EventId(4, nameof(ClientKeepAliveFail)), "WebSocket client heartbeat to {string} failed with status code {int}.");
private static readonly Action<ILogger, string, Exception> _clientTransportApplicationCompleted =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(5, nameof(ClientTransportApplicationCompleted)), "WebSocket client heartbeat to {string} completed transport and application tasks.");
public static void ClientStarted(ILogger logger, string url) => _clientStarted(logger, url ?? string.Empty, null);
public static void ClientCompleted(ILogger logger, string url) => _clientCompleted(logger, url ?? string.Empty, null);
public static void ClientKeepAliveSucceed(ILogger logger, string url) => _clientKeepAliveSucceed(logger, url ?? string.Empty, null);
public static void ClientKeepAliveFail(ILogger logger, string url, int statusCode = 0, Exception e = null) => _clientKeepAliveFail(logger, url ?? string.Empty, statusCode, e);
public static void ClientTransportApplicationCompleted(ILogger logger, string url) => _clientTransportApplicationCompleted(logger, url ?? string.Empty, null);
}
}
}

Просмотреть файл

@ -0,0 +1,125 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Bot.Connector.Streaming.Session;
using Microsoft.Bot.Connector.Streaming.Transport;
using Microsoft.Bot.Streaming;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Bot.Connector.Streaming.Application
{
/// <summary>
/// Default implementation of <see cref="StreamingConnection"/> for WebSocket transport.
/// </summary>
public class WebSocketStreamingConnection : StreamingConnection
{
private readonly ILogger _logger;
private readonly HttpContext _httpContext;
private readonly TaskCompletionSource<bool> _sessionInitializedTask = new TaskCompletionSource<bool>();
private StreamingSession _session;
private CancellationToken _cancellationToken;
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketStreamingConnection"/> class.
/// </summary>
/// <param name="httpContext"><see cref="HttpContext"/> instance on which to accept the web socket.</param>
/// <param name="logger"><see cref="ILogger"/> for the connection.</param>
public WebSocketStreamingConnection(HttpContext httpContext, ILogger logger)
: this(logger)
{
_httpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
}
internal WebSocketStreamingConnection(ILogger logger)
{
_logger = logger ?? NullLogger.Instance;
}
/// <inheritdoc/>
public override async Task<ReceiveResponse> SendStreamingRequestAsync(StreamingRequest request, CancellationToken cancellationToken = default(CancellationToken))
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
// This request could come fast while the session, transport and application are still being set up.
// Wait for the session to signal that application and transport started before using the session.
await _sessionInitializedTask.Task.ConfigureAwait(false);
if (_session == null)
{
throw new InvalidOperationException("Cannot send streaming request since the session is not set up.");
}
return await _session.SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc/>
public override async Task ListenAsync(RequestHandler requestHandler, CancellationToken cancellationToken = default(CancellationToken))
{
if (requestHandler == null)
{
throw new ArgumentNullException(nameof(requestHandler));
}
await ListenImplAsync(
socketConnectFunc: t => t.ConnectAsync(_httpContext, CancellationToken.None),
requestHandler: requestHandler,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
internal async Task ListenInternalAsync(WebSocket webSocket, RequestHandler requestHandler, CancellationToken cancellationToken = default(CancellationToken))
{
if (requestHandler == null)
{
throw new ArgumentNullException(nameof(requestHandler));
}
if (requestHandler == null)
{
throw new ArgumentNullException(nameof(requestHandler));
}
await ListenImplAsync(
socketConnectFunc: t => t.ProcessSocketAsync(webSocket, cancellationToken),
requestHandler: requestHandler,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
private async Task ListenImplAsync(Func<WebSocketTransport, Task> socketConnectFunc, RequestHandler requestHandler, CancellationToken cancellationToken = default(CancellationToken))
{
var duplexPipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
_cancellationToken = cancellationToken;
// Create transport and application
var transport = new WebSocketTransport(duplexPipePair.Application, _logger);
var application = new TransportHandler(duplexPipePair.Transport, _logger);
// Create session
_session = new StreamingSession(requestHandler, application, _logger, _cancellationToken);
// Start transport and application
var transportTask = socketConnectFunc(transport);
var applicationTask = application.ListenAsync(cancellationToken);
var tasks = new List<Task>() { transportTask, applicationTask };
// Signal that session is ready to be used
_sessionInitializedTask.SetResult(true);
// Let application and transport run
await Task.WhenAll(tasks).ConfigureAwait(false);
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Runtime.CompilerServices;
#if SIGNASSEMBLY
[assembly: InternalsVisibleTo("Microsoft.Bot.Connector.Streaming.Tests", PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
#else
[assembly: InternalsVisibleTo("Microsoft.Bot.Connector.Streaming.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
#endif

Просмотреть файл

@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version Condition=" '$(ReleasePackageVersion)' == '' ">$(LocalPackageVersion)</Version>
<Version Condition=" '$(ReleasePackageVersion)' != '' ">$(ReleasePackageVersion)</Version>
<PackageVersion Condition=" '$(ReleasePackageVersion)' == '' ">$(LocalPackageVersion)</PackageVersion>
<PackageVersion Condition=" '$(ReleasePackageVersion)' != '' ">$(ReleasePackageVersion)</PackageVersion>
<Configurations>Debug;Release</Configurations>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\Microsoft.Bot.Connector.Streaming.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<DebugType>Full</DebugType>
<PackageId>Microsoft.Bot.Connector.Streaming</PackageId>
<Description>Streaming library for the Bot Framework SDK</Description>
<Summary>Streaming library for the Bot Framework SDK</Summary>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>Full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Connections.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.IO.Pipelines" Version="5.0.1" />
<PackageReference Include="System.Text.Json" Version="4.7.2" />
<!-- Force System.Text.Encodings.Web to a safe version. -->
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Bot.Schema\Microsoft.Bot.Schema.csproj" />
<ProjectReference Include="..\Microsoft.Bot.Streaming\Microsoft.Bot.Streaming.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,523 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Payloads;
using Microsoft.Bot.Connector.Streaming.Transport;
using Microsoft.Bot.Streaming;
using Microsoft.Bot.Streaming.Payloads;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
namespace Microsoft.Bot.Connector.Streaming.Session
{
internal class StreamingSession
{
// Utf byte order mark constant as defined
// Dotnet runtime: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs#L35
// Unicode.org spec: https://www.unicode.org/faq/utf_bom.html#bom5
private static byte[] _utf8Bom = { 0xEF, 0xBB, 0xBF };
private readonly Dictionary<Guid, StreamDefinition> _streamDefinitions = new Dictionary<Guid, StreamDefinition>();
private readonly Dictionary<Guid, ReceiveRequest> _requests = new Dictionary<Guid, ReceiveRequest>();
private readonly Dictionary<Guid, ReceiveResponse> _responses = new Dictionary<Guid, ReceiveResponse>();
private readonly ConcurrentDictionary<Guid, TaskCompletionSource<ReceiveResponse>> _pendingResponses = new ConcurrentDictionary<Guid, TaskCompletionSource<ReceiveResponse>>();
private readonly RequestHandler _receiver;
private readonly TransportHandler _sender;
private readonly ILogger _logger;
private readonly CancellationToken _connectionCancellationToken;
private readonly object _receiveSync = new object();
public StreamingSession(RequestHandler receiver, TransportHandler sender, ILogger logger, CancellationToken connectionCancellationToken = default)
{
_receiver = receiver ?? throw new ArgumentNullException(nameof(receiver));
_sender = sender ?? throw new ArgumentNullException(nameof(sender));
_sender.Subscribe(new ProtocolDispatcher(this));
_logger = logger ?? NullLogger.Instance;
_connectionCancellationToken = connectionCancellationToken;
}
public async Task<ReceiveResponse> SendRequestAsync(StreamingRequest request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
var payload = new RequestPayload()
{
Verb = request.Verb,
Path = request.Path,
};
if (request.Streams != null)
{
payload.Streams = new List<StreamDescription>();
foreach (var contentStream in request.Streams)
{
var description = GetStreamDescription(contentStream);
payload.Streams.Add(description);
}
}
var requestId = Guid.NewGuid();
var responseCompletionSource = new TaskCompletionSource<ReceiveResponse>();
_pendingResponses.TryAdd(requestId, responseCompletionSource);
// Send request
await _sender.SendRequestAsync(requestId, payload, cancellationToken).ConfigureAwait(false);
foreach (var stream in request.Streams)
{
await _sender.SendStreamAsync(stream.Id, await stream.Content.ReadAsStreamAsync().ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
}
// Timeout: We could be waiting for this TaskCompletionSource forever if the connection is broken
// before this response gets back, blocking termination on this thread.
using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
var completedTask = await Task.WhenAny(responseCompletionSource.Task, Task.Delay(TimeSpan.FromSeconds(5), timeoutCancellationTokenSource.Token)).ConfigureAwait(false);
if (completedTask == responseCompletionSource.Task)
{
timeoutCancellationTokenSource.Cancel();
return await responseCompletionSource.Task.ConfigureAwait(false);
}
else
{
throw new TimeoutException($"The operation has timed out");
}
}
}
public async Task SendResponseAsync(Header header, StreamingResponse response, CancellationToken cancellationToken)
{
if (header == null)
{
throw new ArgumentNullException(nameof(header));
}
if (header.Type != PayloadTypes.Response)
{
throw new InvalidOperationException($"StreamingSession SendResponseAsync expected Response payload, but instead received a payload of type {header.Type}");
}
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
var payload = new ResponsePayload()
{
StatusCode = response.StatusCode,
};
if (response.Streams != null)
{
payload.Streams = new List<StreamDescription>();
foreach (var contentStream in response.Streams)
{
var description = GetStreamDescription(contentStream);
payload.Streams.Add(description);
}
}
await _sender.SendResponseAsync(header.Id, payload, cancellationToken).ConfigureAwait(false);
}
public virtual void ReceiveRequest(Header header, ReceiveRequest request)
{
if (header == null)
{
throw new ArgumentNullException(nameof(header));
}
if (header.Type != PayloadTypes.Request)
{
throw new InvalidOperationException($"StreamingSession cannot receive payload of type {header.Type} as request.");
}
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
Log.PayloadReceived(_logger, header);
lock (_receiveSync)
{
_requests.Add(header.Id, request);
if (request.Streams.Any())
{
foreach (var streamDefinition in request.Streams)
{
_streamDefinitions.Add(streamDefinition.Id, streamDefinition as StreamDefinition);
}
}
else
{
ProcessRequest(header.Id, request);
}
}
}
public virtual void ReceiveResponse(Header header, ReceiveResponse response)
{
if (header == null)
{
throw new ArgumentNullException(nameof(header));
}
if (header.Type != PayloadTypes.Response)
{
throw new InvalidOperationException($"StreamingSession cannot receive payload of type {header.Type} as response");
}
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
Log.PayloadReceived(_logger, header);
lock (_receiveSync)
{
if (!response.Streams.Any())
{
if (_pendingResponses.TryGetValue(header.Id, out TaskCompletionSource<ReceiveResponse> responseTask))
{
responseTask.SetResult(response);
_pendingResponses.TryRemove(header.Id, out TaskCompletionSource<ReceiveResponse> removedResponse);
}
}
else
{
_responses.Add(header.Id, response);
foreach (var streamDefinition in response.Streams)
{
_streamDefinitions.Add(streamDefinition.Id, streamDefinition as StreamDefinition);
}
}
}
}
public virtual void ReceiveStream(Header header, ArraySegment<byte> payload)
{
if (header == null)
{
throw new ArgumentNullException(nameof(header));
}
if (header.Type != PayloadTypes.Stream)
{
throw new InvalidOperationException($"StreamingSession cannot receive payload of type {header.Type} as stream");
}
if (payload == null)
{
throw new ArgumentNullException(nameof(payload));
}
Log.PayloadReceived(_logger, header);
// Find request for incoming stream header
if (_streamDefinitions.TryGetValue(header.Id, out StreamDefinition streamDefinition))
{
streamDefinition.Stream.Write(payload.Array, payload.Offset, payload.Count);
// Is this the end of this stream?
if (header.End)
{
// Mark this stream as completed
if (streamDefinition is StreamDefinition streamDef)
{
streamDef.Complete = true;
streamDef.Stream.Seek(0, SeekOrigin.Begin);
List<IContentStream> streams = null;
// Find the request / response
if (streamDef.PayloadType == PayloadTypes.Request)
{
if (_requests.TryGetValue(streamDef.PayloadId, out ReceiveRequest req))
{
streams = req.Streams;
}
}
else if (streamDef.PayloadType == PayloadTypes.Response)
{
if (_responses.TryGetValue(streamDef.PayloadId, out ReceiveResponse res))
{
streams = res.Streams;
}
}
if (streams != null)
{
lock (_receiveSync)
{
// Have we completed all the streams we expect for this request?
bool allStreamsDone = streams.All(s => s is StreamDefinition streamDef && streamDef.Complete);
// If we received all the streams, then it's time to pass this request to the request handler!
// For example, if this request is a send activity, the request handler will deserialize the first stream
// into an activity and pass to the adapter.
if (allStreamsDone)
{
if (streamDef.PayloadType == PayloadTypes.Request)
{
if (_requests.TryGetValue(streamDef.PayloadId, out ReceiveRequest request))
{
ProcessRequest(streamDef.PayloadId, request);
_requests.Remove(streamDef.PayloadId);
}
}
else if (streamDef.PayloadType == PayloadTypes.Response)
{
if (_responses.TryGetValue(streamDef.PayloadId, out ReceiveResponse response))
{
if (_pendingResponses.TryGetValue(streamDef.PayloadId, out TaskCompletionSource<ReceiveResponse> responseTask))
{
responseTask.SetResult(response);
_responses.Remove(streamDef.PayloadId);
_pendingResponses.TryRemove(streamDef.PayloadId, out TaskCompletionSource<ReceiveResponse> removedResponse);
}
}
}
}
}
}
}
}
}
else
{
Log.OrphanedStream(_logger, header);
}
}
private static StreamDescription GetStreamDescription(ResponseMessageStream stream)
{
var description = new StreamDescription()
{
Id = stream.Id.ToString("D"),
};
if (stream.Content.Headers.TryGetValues(HeaderNames.ContentType, out IEnumerable<string> contentType))
{
description.ContentType = contentType?.FirstOrDefault();
}
if (stream.Content.Headers.TryGetValues(HeaderNames.ContentLength, out IEnumerable<string> contentLength))
{
var value = contentLength?.FirstOrDefault();
if (value != null && int.TryParse(value, out int length))
{
description.Length = length;
}
}
else
{
description.Length = (int?)stream.Content.Headers.ContentLength;
}
return description;
}
private static ArraySegment<byte> GetArraySegment(ReadOnlySequence<byte> sequence)
{
if (sequence.IsSingleSegment)
{
if (MemoryMarshal.TryGetArray(sequence.First, out ArraySegment<byte> segment))
{
return segment;
}
}
// Can be optimized by not copying but should be uncommon. If perf data shows that we are hitting this
// code branch, then we can optimize and avoid copies and heap allocations.
return new ArraySegment<byte>(sequence.ToArray());
}
private void ProcessRequest(Guid id, ReceiveRequest request)
{
_ = Task.Run(async () =>
{
var streamingResponse = await _receiver.ProcessRequestAsync(request, null).ConfigureAwait(false);
await SendResponseAsync(new Header() { Id = id, Type = PayloadTypes.Response }, streamingResponse, _connectionCancellationToken).ConfigureAwait(false);
request.Streams.ForEach(s => _streamDefinitions.Remove(s.Id));
});
}
internal class ProtocolDispatcher : IObserver<(Header Header, ReadOnlySequence<byte> Payload)>
{
private readonly StreamingSession _streamingSession;
public ProtocolDispatcher(StreamingSession streamingSession)
{
_streamingSession = streamingSession ?? throw new ArgumentNullException(nameof(streamingSession));
}
public void OnCompleted()
{
throw new NotImplementedException();
}
public void OnError(Exception error)
{
throw new NotImplementedException();
}
public void OnNext((Header Header, ReadOnlySequence<byte> Payload) frame)
{
var header = frame.Header;
var payload = frame.Payload;
switch (header.Type)
{
case PayloadTypes.Stream:
_streamingSession.ReceiveStream(header, GetArraySegment(payload));
break;
case PayloadTypes.Request:
var requestPayload = DeserializeTo<RequestPayload>(payload);
var request = new ReceiveRequest()
{
Verb = requestPayload.Verb,
Path = requestPayload.Path,
Streams = new List<IContentStream>(),
};
CreatePlaceholderStreams(header, request.Streams, requestPayload.Streams);
_streamingSession.ReceiveRequest(header, request);
break;
case PayloadTypes.Response:
var responsePayload = DeserializeTo<ResponsePayload>(payload);
var response = new ReceiveResponse()
{
StatusCode = responsePayload.StatusCode,
Streams = new List<IContentStream>(),
};
CreatePlaceholderStreams(header, response.Streams, responsePayload.Streams);
_streamingSession.ReceiveResponse(header, response);
break;
case PayloadTypes.CancelAll:
break;
case PayloadTypes.CancelStream:
break;
}
}
private static T DeserializeTo<T>(ReadOnlySequence<byte> payload)
{
// The payload here will likely have a UTF-8 byte-order-mark (BOM).
// The JsonSerializer and UtfJsonReader explicitly expect no BOM in this overload that takes a ReadOnlySequence<byte>.
// With that in mind, we check for a UTF-8 BOM and remove it if present. The main reason to call this specific flow instead of
// the stream version or using Json.Net is that the ReadOnlySequence<T> API allows us to do a no-copy deserialization.
// The ReadOnlySequence was allocated from the memory pool by the transport layer and gets sent all the way here without copies.
// Check for UTF-8 BOM and remove if present: https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-use-dom-utf8jsonreader-utf8jsonwriter?pivots=dotnet-5-0#filter-data-using-utf8jsonreader
var potentialBomSequence = payload.Slice(payload.Start, _utf8Bom.Length);
var potentialBomSpan = potentialBomSequence.IsSingleSegment
? potentialBomSequence.First.Span
: potentialBomSequence.ToArray();
ReadOnlySequence<byte> mainPayload = payload;
if (potentialBomSpan.StartsWith(_utf8Bom))
{
mainPayload = payload.Slice(_utf8Bom.Length);
}
var reader = new Utf8JsonReader(mainPayload);
return System.Text.Json.JsonSerializer.Deserialize<T>(
ref reader,
new JsonSerializerOptions() { IgnoreNullValues = true, PropertyNameCaseInsensitive = true });
}
private static void CreatePlaceholderStreams(Header header, List<IContentStream> placeholders, List<StreamDescription> streamInfo)
{
if (streamInfo != null)
{
foreach (var streamDescription in streamInfo)
{
if (!Guid.TryParse(streamDescription.Id, out Guid id))
{
throw new InvalidDataException($"Stream description id '{streamDescription.Id}' is not a Guid");
}
placeholders.Add(new StreamDefinition()
{
ContentType = streamDescription.ContentType,
Length = streamDescription.Length,
Id = Guid.Parse(streamDescription.Id),
Stream = new MemoryStream(),
PayloadType = header.Type,
PayloadId = header.Id
});
}
}
}
}
internal class StreamDefinition : IContentStream
{
public Guid Id { get; set; }
public string ContentType { get; set; }
public int? Length { get; set; }
public Stream Stream { get; set; }
public bool Complete { get; set; }
public char PayloadType { get; set; }
public Guid PayloadId { get; set; }
}
private class Log
{
private static readonly Action<ILogger, Guid, char, int, bool, Exception> _orphanedStream =
LoggerMessage.Define<Guid, char, int, bool>(LogLevel.Error, new EventId(1, nameof(OrphanedStream)), "Stream has no associated payload. Header: ID {Guid} Type {char} Payload length:{int}. End :{bool}.");
private static readonly Action<ILogger, Guid, char, int, bool, Exception> _payloadReceived =
LoggerMessage.Define<Guid, char, int, bool>(LogLevel.Debug, new EventId(2, nameof(PayloadReceived)), "Payload received in session. Header: ID {Guid} Type {char} Payload length:{int}. End :{bool}..");
public static void OrphanedStream(ILogger logger, Header header) => _orphanedStream(logger, header.Id, header.Type, header.PayloadLength, header.End, null);
public static void PayloadReceived(ILogger logger, Header header) => _payloadReceived(logger, header.Id, header.Type, header.PayloadLength, header.End, null);
}
}
}

Просмотреть файл

@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.IO.Pipelines;
namespace Microsoft.Bot.Connector.Streaming.Transport
{
internal class DuplexPipe : IDuplexPipe
{
public DuplexPipe(PipeReader reader, PipeWriter writer)
{
Input = reader;
Output = writer;
}
public PipeReader Input { get; }
public PipeWriter Output { get; }
public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions)
{
var input = new Pipe(inputOptions);
var output = new Pipe(outputOptions);
var transport = new DuplexPipe(output.Reader, input.Writer);
var application = new DuplexPipe(input.Reader, output.Writer);
return new DuplexPipePair(transport, application);
}
internal readonly struct DuplexPipePair
{
public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application)
{
Transport = transport;
Application = application;
}
public IDuplexPipe Transport { get; }
public IDuplexPipe Application { get; }
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
using Microsoft.Bot.Streaming.Payloads;
using Newtonsoft.Json;
namespace Microsoft.Bot.Connector.Streaming.Payloads
{
internal class RequestPayload
{
#pragma warning disable SA1609
/// <summary>
/// Gets or sets request verb, null on responses.
/// </summary>
[JsonProperty("verb")]
public string Verb { get; set; }
/// <summary>
/// Gets or sets request path; null on responses.
/// </summary>
[JsonProperty("path")]
public string Path { get; set; }
/// <summary>
/// Gets or sets assoicated stream descriptions.
/// </summary>
[JsonProperty("streams")]
public List<StreamDescription> Streams { get; set; }
#pragma warning restore SA1609
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Collections.Generic;
using Microsoft.Bot.Streaming.Payloads;
using Newtonsoft.Json;
namespace Microsoft.Bot.Connector.Streaming.Payloads
{
internal class ResponsePayload
{
#pragma warning disable SA1609
/// <summary>
/// Gets or sets status - The Response Status.
/// </summary>
[JsonProperty("statusCode")]
public int StatusCode { get; set; }
/// <summary>
/// Gets or sets assoicated stream descriptions.
/// </summary>
[JsonProperty("streams")]
public List<StreamDescription> Streams { get; set; }
#pragma warning restore SA1609
}
}

Просмотреть файл

@ -0,0 +1,323 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.IO;
using System.IO.Pipelines;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Payloads;
using Microsoft.Bot.Streaming.Payloads;
using Microsoft.Bot.Streaming.Transport;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Microsoft.Bot.Connector.Streaming.Transport
{
internal class TransportHandler : IObservable<(Header Header, ReadOnlySequence<byte> Payload)>, IDisposable
{
private readonly IDuplexPipe _transport;
private readonly ILogger _logger;
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1);
private readonly TimeSpan _semaphoreTimeout = TimeSpan.FromSeconds(10);
private readonly byte[] _sendHeaderBuffer = new byte[TransportConstants.MaxHeaderLength];
private IObserver<(Header, ReadOnlySequence<byte>)> _observer;
private bool _disposedValue;
public TransportHandler(IDuplexPipe transport, ILogger logger)
{
_transport = transport;
_logger = logger;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to catch all exceptions in the message loop.")]
public async Task ListenAsync(CancellationToken cancellationToken)
{
var input = _transport.Input;
bool aborted = false;
while (!cancellationToken.IsCancellationRequested)
{
ReadResult result;
result = await input.ReadAsync().ConfigureAwait(false);
var buffer = result.Buffer;
try
{
if (result.IsCanceled)
{
break;
}
if (!buffer.IsEmpty)
{
while (TryParseHeader(ref buffer, out Header header))
{
Log.PayloadReceived(_logger, header);
ReadOnlySequence<byte> payload = ReadOnlySequence<byte>.Empty;
if (header.PayloadLength > 0)
{
if (buffer.Length < header.PayloadLength)
{
input.AdvanceTo(buffer.Start, buffer.End);
result = await input.ReadAsync().ConfigureAwait(false);
if (result.IsCanceled)
{
break;
}
buffer = result.Buffer;
}
if (buffer.Length >= header.PayloadLength)
{
payload = buffer.Slice(buffer.Start, header.PayloadLength);
buffer = buffer.Slice(header.PayloadLength);
}
else
{
break;
}
}
_observer.OnNext((header, payload));
}
}
if (result.IsCompleted)
{
if (buffer.IsEmpty)
{
break;
}
}
}
catch (OperationCanceledException)
{
// Don't treat OperationCanceledException as an error, it's basically a "control flow"
// exception to stop things from running.
}
catch (Exception ex)
{
Log.ReadFrameFailed(_logger, ex);
// This failure means we are tearing down the connection, so return and let the cancellation
// and draining take place.
await input.CompleteAsync(ex).ConfigureAwait(false);
aborted = true;
Log.ListenError(_logger, ex);
return;
}
finally
{
if (!aborted)
{
input.AdvanceTo(buffer.Start, buffer.End);
}
}
}
await input.CompleteAsync().ConfigureAwait(false);
await _transport.Output.CompleteAsync().ConfigureAwait(false);
Log.ListenCompleted(_logger);
}
public Task StopAsync()
{
_transport.Input.CancelPendingRead();
return Task.CompletedTask;
}
public virtual async Task SendResponseAsync(Guid id, ResponsePayload response, CancellationToken cancellationToken = default)
{
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
var responseBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(response));
var responseHeader = new Header()
{
Type = PayloadTypes.Response,
Id = id,
PayloadLength = (int)responseBytes.Length,
End = true,
};
await WriteAsync(
header: responseHeader,
writeFunc: async pipeWriter => await pipeWriter.WriteAsync(responseBytes).ConfigureAwait(false)).ConfigureAwait(false);
}
public virtual async Task SendRequestAsync(Guid id, RequestPayload request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
var requestBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(request));
var requestHeader = new Header()
{
Type = PayloadTypes.Request,
Id = id,
PayloadLength = (int)requestBytes.Length,
End = true,
};
await WriteAsync(
header: requestHeader,
writeFunc: async pipeWriter => await pipeWriter.WriteAsync(requestBytes).ConfigureAwait(false)).ConfigureAwait(false);
}
public virtual async Task SendStreamAsync(Guid id, Stream stream, CancellationToken cancellationToken)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
var streamHeader = new Header()
{
Type = PayloadTypes.Stream,
Id = id,
PayloadLength = (int)stream.Length,
End = true,
};
await WriteAsync(streamHeader, pipeWriter => stream.CopyToAsync(pipeWriter)).ConfigureAwait(false);
}
public IDisposable Subscribe(IObserver<(Header, ReadOnlySequence<byte>)> observer)
{
if (_observer != null)
{
throw new InvalidOperationException("The protocol expects only a single observer.");
}
_observer = observer ?? throw new ArgumentNullException(nameof(observer));
return null;
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_writeLock?.Dispose();
}
_disposedValue = true;
}
}
private static bool TryParseHeader(ref ReadOnlySequence<byte> buffer, out Header header)
{
if (buffer.IsEmpty)
{
header = null;
return false;
}
var length = Math.Min(TransportConstants.MaxHeaderLength, buffer.Length);
var headerBuffer = buffer.Slice(0, length);
if (headerBuffer.Length != TransportConstants.MaxHeaderLength)
{
header = null;
return false;
}
// Optimization opportunity: instead of headerBuffer.ToArray() which does a 48 byte heap allocation,
// do a best effort attempt to use MemoryMashal.TryGetArray. Since it has a lot of corner cases,
// keeping it simple for now and we can optimize further if data says we required it.
// Alternatively we can have a 48 byte buffer that we reuse, considering that we always
// have a single thread running a given transportHandler instance.
header = HeaderSerializer.Deserialize(headerBuffer.ToArray(), 0, TransportConstants.MaxHeaderLength);
buffer = buffer.Slice(TransportConstants.MaxHeaderLength);
return true;
}
private async Task WriteAsync(Header header, Func<PipeWriter, Task> writeFunc, CancellationToken cancellationToken = default)
{
var output = _transport.Output;
Log.SendingPayload(_logger, header);
if (await _writeLock.WaitAsync(_semaphoreTimeout, cancellationToken).ConfigureAwait(false))
{
try
{
HeaderSerializer.Serialize(header, _sendHeaderBuffer, 0);
await output.WriteAsync(_sendHeaderBuffer).ConfigureAwait(false);
await writeFunc(output).ConfigureAwait(false);
}
finally
{
_writeLock.Release();
}
}
else
{
Log.SemaphoreTimeOut(_logger, header);
}
}
private static class Log
{
private static readonly Action<ILogger, Guid, char, int, bool, Exception> _payloadReceived =
LoggerMessage.Define<Guid, char, int, bool>(LogLevel.Debug, new EventId(1, nameof(PayloadReceived)), "Payload received. Header: ID {Guid} Type {char} Payload length:{int}. End :{bool}.");
private static readonly Action<ILogger, Exception> _readFrameFailed =
LoggerMessage.Define(LogLevel.Error, new EventId(2, nameof(ReadFrameFailed)), "Failed to read frame from transport.");
private static readonly Action<ILogger, Guid, char, int, bool, Exception> _payloadSending =
LoggerMessage.Define<Guid, char, int, bool>(LogLevel.Debug, new EventId(3, nameof(SendingPayload)), "Sending Payload. Header: ID {Guid} Type {char} Payload length:{int}. End :{bool}.");
private static readonly Action<ILogger, Guid, char, int, bool, Exception> _semaphoreTimeOut =
LoggerMessage.Define<Guid, char, int, bool>(LogLevel.Error, new EventId(4, nameof(SemaphoreTimeOut)), "Timed out trying to acquire write semaphore. Header: ID {Guid} Type {char} Payload length:{int}. End :{bool}.");
private static readonly Action<ILogger, Exception> _listenError =
LoggerMessage.Define(LogLevel.Error, new EventId(5, nameof(ListenError)), "TransportHandler encountered an error and will stop listening.");
private static readonly Action<ILogger, Exception> _listenCompleted =
LoggerMessage.Define(LogLevel.Information, new EventId(6, nameof(ListenCompleted)), "TransportHandler listen task completed.");
public static void PayloadReceived(ILogger logger, Header header) => _payloadReceived(logger, header.Id, header.Type, header.PayloadLength, header.End, null);
public static void ReadFrameFailed(ILogger logger, Exception ex) => _readFrameFailed(logger, ex);
public static void SendingPayload(ILogger logger, Header header) => _payloadSending(logger, header.Id, header.Type, header.PayloadLength, header.End, null);
public static void SemaphoreTimeOut(ILogger logger, Header header) => _semaphoreTimeOut(logger, header.Id, header.Type, header.PayloadLength, header.End, null);
public static void ListenError(ILogger logger, Exception ex) => _listenError(logger, ex);
public static void ListenCompleted(ILogger logger) => _listenCompleted(logger, null);
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Buffers;
using System.Net.WebSockets;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Bot.Connector.Streaming.Transport
{
internal static class WebSocketExtensions
{
public static ValueTask SendAsync(this WebSocket webSocket, ReadOnlySequence<byte> buffer, WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default)
{
if (buffer.IsSingleSegment)
{
var isArray = MemoryMarshal.TryGetArray(buffer.First, out var segment);
return new ValueTask(webSocket.SendAsync(segment, webSocketMessageType, endOfMessage: true, cancellationToken));
}
else
{
return SendMultiSegmentAsync(webSocket, buffer, webSocketMessageType, cancellationToken);
}
}
private static async ValueTask SendMultiSegmentAsync(WebSocket webSocket, ReadOnlySequence<byte> buffer, WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default)
{
var position = buffer.Start;
// Get a segment before the loop so we can be one segment behind while writing
// This allows us to do a non-zero byte write for the endOfMessage = true send
buffer.TryGet(ref position, out var prevSegment);
while (buffer.TryGet(ref position, out var segment))
{
var isArray = MemoryMarshal.TryGetArray(prevSegment, out var arraySegment);
await webSocket.SendAsync(arraySegment, webSocketMessageType, endOfMessage: false, cancellationToken).ConfigureAwait(false);
prevSegment = segment;
}
// End of message frame
if (MemoryMarshal.TryGetArray(prevSegment, out var arraySegmentEnd))
{
await webSocket.SendAsync(arraySegmentEnd, webSocketMessageType, endOfMessage: true, cancellationToken).ConfigureAwait(false);
}
}
}
}

Просмотреть файл

@ -0,0 +1,382 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Net.WebSockets;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Bot.Connector.Streaming.Transport
{
internal class WebSocketTransport
{
private readonly IDuplexPipe _application;
private readonly ILogger _logger;
private volatile bool _aborted;
public WebSocketTransport(IDuplexPipe application, ILogger logger)
{
_application = application ?? throw new ArgumentNullException(nameof(application));
_logger = logger ?? NullLogger.Instance;
}
public async Task ConnectAsync(HttpContext context, CancellationToken cancellationToken)
{
using (var ws = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false))
{
Log.SocketOpened(_logger);
try
{
await ProcessSocketAsync(ws, cancellationToken).ConfigureAwait(false);
}
finally
{
Log.SocketClosed(_logger);
}
}
}
public async Task ConnectAsync(string url, IDictionary<string, string> requestHeaders = null, CancellationToken cancellationToken = default)
{
using (var ws = new ClientWebSocket())
{
Log.SocketOpened(_logger);
try
{
if (requestHeaders != null)
{
foreach (var key in requestHeaders.Keys)
{
ws.Options.SetRequestHeader(key, requestHeaders[key]);
}
}
await ws.ConnectAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
await ProcessSocketAsync(ws, cancellationToken).ConfigureAwait(false);
}
finally
{
Log.SocketClosed(_logger);
}
}
}
internal async Task ProcessSocketAsync(WebSocket socket, CancellationToken cancellationToken)
{
// Begin sending and receiving. Receiving must be started first because ExecuteAsync enables SendAsync.
var receiving = StartReceivingAsync(socket, cancellationToken);
var sending = StartSendingAsync(socket);
// Wait for send or receive to complete
var trigger = await Task.WhenAny(receiving, sending).ConfigureAwait(false);
if (trigger == receiving)
{
Log.WaitingForSend(_logger);
// We're waiting for the application to finish and there are 2 things it could be doing
// 1. Waiting for application data
// 2. Waiting for a websocket send to complete
// Cancel the application so that ReadAsync yields
_application.Input.CancelPendingRead();
using (var delayCts = new CancellationTokenSource())
{
// TODO: flow this timeout to allow draining
var resultTask = await Task.WhenAny(sending, Task.Delay(TimeSpan.FromSeconds(1), delayCts.Token)).ConfigureAwait(false);
if (resultTask != sending)
{
// We timed out so now we're in ungraceful shutdown mode
Log.CloseTimedOut(_logger);
// Abort the websocket if we're stuck in a pending send to the client
_aborted = true;
socket.Abort();
}
else
{
delayCts.Cancel();
}
}
}
else
{
Log.WaitingForClose(_logger);
// We're waiting on the websocket to close and there are 2 things it could be doing
// 1. Waiting for websocket data
// 2. Waiting on a flush to complete (backpressure being applied)
using (var delayCts = new CancellationTokenSource())
{
var resultTask = await Task.WhenAny(receiving, Task.Delay(TimeSpan.FromSeconds(1), delayCts.Token)).ConfigureAwait(false);
if (resultTask != receiving)
{
// Abort the websocket if we're stuck in a pending receive from the client
_aborted = true;
socket.Abort();
// Cancel any pending flush so that we can quit
_application.Output.CancelPendingFlush();
}
else
{
delayCts.Cancel();
}
}
}
}
private static ArraySegment<byte> GetArraySegment(ReadOnlyMemory<byte> memory)
{
if (!MemoryMarshal.TryGetArray(memory, out var result))
{
throw new InvalidOperationException("Buffer backed by array was expected");
}
return result;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to catch all exceptions in the message loop.")]
private async Task StartReceivingAsync(WebSocket socket, CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
// Do a 0 byte read so that idle connections don't allocate a buffer when waiting for a read
var result = await socket.ReceiveAsync(GetArraySegment(Memory<byte>.Empty), cancellationToken).ConfigureAwait(false);
if (result.MessageType == WebSocketMessageType.Close)
{
return;
}
var memory = _application.Output.GetMemory();
var arraySegment = GetArraySegment(memory);
var receiveResult = await socket.ReceiveAsync(arraySegment, cancellationToken).ConfigureAwait(false);
// Need to check again for netcoreapp3.0 and later because a close can happen between a 0-byte read and the actual read
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
return;
}
Log.MessageReceived(_logger, receiveResult.MessageType, receiveResult.Count, receiveResult.EndOfMessage);
_application.Output.Advance(receiveResult.Count);
var flushResult = await _application.Output.FlushAsync().ConfigureAwait(false);
// We canceled in the middle of applying back pressure
// or if the consumer is done
if (flushResult.IsCanceled || flushResult.IsCompleted)
{
break;
}
}
}
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
// Client has closed the WebSocket connection without completing the close handshake
Log.ClosedPrematurely(_logger, ex);
}
catch (OperationCanceledException)
{
// Ignore aborts, don't treat them like transport errors
}
catch (Exception ex)
{
if (!_aborted && !cancellationToken.IsCancellationRequested)
{
await _application.Output.CompleteAsync(ex).ConfigureAwait(false);
Log.TransportError(_logger, ex);
}
}
finally
{
// We're done writing.
await _application.Output.CompleteAsync().ConfigureAwait(false);
Log.ReceivingCompleted(_logger);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We want to catch all exceptions in the message loop.")]
private async Task StartSendingAsync(WebSocket socket)
{
Exception error = null;
try
{
while (true)
{
var result = await _application.Input.ReadAsync().ConfigureAwait(false);
var buffer = result.Buffer;
// Get a frame from the application
try
{
if (result.IsCanceled)
{
break;
}
if (!buffer.IsEmpty)
{
try
{
Log.SendPayload(_logger, buffer.Length);
if (WebSocketCanSend(socket))
{
await socket.SendAsync(buffer, WebSocketMessageType.Binary, CancellationToken.None).ConfigureAwait(false);
}
else
{
break;
}
}
catch (Exception ex)
{
if (!_aborted)
{
Log.ErrorWritingFrame(_logger, ex);
}
break;
}
}
else if (result.IsCompleted)
{
break;
}
}
finally
{
_application.Input.AdvanceTo(buffer.End);
}
}
}
catch (Exception ex)
{
error = ex;
}
finally
{
// Send the close frame before calling into user code
if (WebSocketCanSend(socket))
{
try
{
// We're done sending, send the close frame to the client if the websocket is still open
await socket.CloseOutputAsync(error != null ? WebSocketCloseStatus.InternalServerError : WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.ClosingWebSocketFailed(_logger, ex);
}
}
Log.SendingCompleted(_logger);
await _application.Input.CompleteAsync().ConfigureAwait(false);
}
}
private bool WebSocketCanSend(WebSocket ws)
{
return !(ws.State == WebSocketState.Aborted ||
ws.State == WebSocketState.Closed ||
ws.State == WebSocketState.CloseSent);
}
/// <summary>
/// Log messages for <see cref="WebSocketTransport"/>.
/// </summary>
/// <remarks>
/// Messages implementred using <see cref="LoggerMessage.Define(LogLevel, EventId, string)"/> to maximize performance.
/// For more information, see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/loggermessage?view=aspnetcore-5.0.
/// </remarks>
private static class Log
{
private static readonly Action<ILogger, Exception> _socketOpened =
LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(SocketOpened)), "Socket transport connection opened.");
private static readonly Action<ILogger, Exception> _socketClosed =
LoggerMessage.Define(LogLevel.Information, new EventId(2, nameof(SocketClosed)), "Socket transport connection closed.");
private static readonly Action<ILogger, Exception> _waitingForSend =
LoggerMessage.Define(LogLevel.Debug, new EventId(3, nameof(WaitingForSend)), "Waiting for the application to finish sending data.");
private static readonly Action<ILogger, Exception> _waitingForClose =
LoggerMessage.Define(LogLevel.Debug, new EventId(4, nameof(WaitingForClose)), "Waiting for the client to close the socket.");
private static readonly Action<ILogger, Exception> _closeTimedOut =
LoggerMessage.Define(LogLevel.Debug, new EventId(5, nameof(CloseTimedOut)), "Timed out waiting for client to send the close frame, aborting the connection.");
private static readonly Action<ILogger, WebSocketMessageType, int, bool, Exception> _messageReceived =
LoggerMessage.Define<WebSocketMessageType, int, bool>(LogLevel.Trace, new EventId(6, nameof(MessageReceived)), "Message received. Type: {MessageType}, size: {Size}, EndOfMessage: {EndOfMessage}.");
private static readonly Action<ILogger, long, Exception> _sendPayload =
LoggerMessage.Define<long>(LogLevel.Trace, new EventId(7, nameof(SendPayload)), "Sending payload: {Size} bytes.");
private static readonly Action<ILogger, Exception> _errorWritingFrame =
LoggerMessage.Define(LogLevel.Debug, new EventId(8, nameof(ErrorWritingFrame)), "Error writing frame.");
private static readonly Action<ILogger, Exception> _closedPrematurely =
LoggerMessage.Define(LogLevel.Debug, new EventId(9, nameof(ClosedPrematurely)), "Socket connection closed prematurely.");
private static readonly Action<ILogger, Exception> _closingWebSocketFailed =
LoggerMessage.Define(LogLevel.Debug, new EventId(10, nameof(ClosingWebSocketFailed)), "Closing webSocket failed.");
private static readonly Action<ILogger, Exception> _sendingCompleted =
LoggerMessage.Define(LogLevel.Information, new EventId(11, nameof(SendingCompleted)), "Socket transport sending task completed.");
private static readonly Action<ILogger, Exception> _receivingCompleted =
LoggerMessage.Define(LogLevel.Information, new EventId(12, nameof(ReceivingCompleted)), "Socket transport receiving task completed.");
private static readonly Action<ILogger, Exception> _transportError =
LoggerMessage.Define(LogLevel.Error, new EventId(13, nameof(TransportError)), "Transport error deteted.");
public static void SocketOpened(ILogger logger) => _socketOpened(logger, null);
public static void SocketClosed(ILogger logger) => _socketClosed(logger, null);
public static void WaitingForSend(ILogger logger) => _waitingForSend(logger, null);
public static void WaitingForClose(ILogger logger) => _waitingForClose(logger, null);
public static void CloseTimedOut(ILogger logger) => _closeTimedOut(logger, null);
public static void MessageReceived(ILogger logger, WebSocketMessageType type, int size, bool endOfMessage) => _messageReceived(logger, type, size, endOfMessage, null);
public static void SendPayload(ILogger logger, long size) => _sendPayload(logger, size, null);
public static void ErrorWritingFrame(ILogger logger, Exception ex) => _errorWritingFrame(logger, ex);
public static void ClosedPrematurely(ILogger logger, Exception ex) => _closedPrematurely(logger, ex);
public static void ClosingWebSocketFailed(ILogger logger, Exception ex) => _closingWebSocketFailed(logger, ex);
public static void SendingCompleted(ILogger logger) => _sendingCompleted(logger, null);
public static void ReceivingCompleted(ILogger logger) => _receivingCompleted(logger, null);
public static void TransportError(ILogger logger, Exception ex) => _transportError(logger, ex);
}
}
}

Просмотреть файл

@ -90,7 +90,7 @@ namespace Microsoft.Bot.Connector.Authentication
if (appId != AppId)
{
throw new InvalidOperationException("Invalid appId");
throw new InvalidOperationException($"Invalid appId {appId} does not match expected {AppId}");
}
if (loginEndpoint.StartsWith(AuthenticationConstants.ToChannelFromBotLoginUrlTemplate, StringComparison.OrdinalIgnoreCase))

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Runtime.CompilerServices;
#if SIGNASSEMBLY
[assembly: InternalsVisibleTo("Microsoft.Bot.Connector.Streaming.Tests", PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
#else
[assembly: InternalsVisibleTo("Microsoft.Bot.Connector.Streaming.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
#endif

Просмотреть файл

@ -58,7 +58,7 @@ namespace Microsoft.Bot.Streaming
/// A <see cref="List{T}"/> of <see cref="ResponseMessageStream"/> items associated with this request.
/// </value>
#pragma warning disable CA2227 // Collection properties should be read only (we can't change this without breaking binary compat)
public List<ResponseMessageStream> Streams { get; set; }
public List<ResponseMessageStream> Streams { get; set; } = new List<ResponseMessageStream>();
#pragma warning restore CA2227 // Collection properties should be read only
/// <summary>

Просмотреть файл

@ -187,6 +187,36 @@ namespace Microsoft.Bot.Streaming.Transport.WebSockets
GC.SuppressFinalize(this);
}
/// <summary>
/// Establish a connection with injected web socket for more control in tests.
/// </summary>
/// <param name="socket">A <see cref="WebSocket"/> for the client which msut already be .</param>
/// <returns>A <see cref="Task"/> that will not resolve until the client stops listening for incoming messages.</returns>
internal Task ConnectInternalAsync(WebSocket socket)
{
if (IsConnected)
{
return Task.CompletedTask;
}
// We don't dispose the websocket, since WebSocketTransport is now
// the owner of the web socket.
#pragma warning disable CA2000 // Dispose objects before losing scope
var socketTransport = new WebSocketTransport(socket);
#pragma warning restore CA2000 // Dispose objects before losing scope
// Listen for disconnected events.
_sender.Disconnected += OnConnectionDisconnected;
_receiver.Disconnected += OnConnectionDisconnected;
_sender.Connect(socketTransport);
_receiver.Connect(socketTransport);
IsConnected = true;
return Task.CompletedTask;
}
/// <summary>
/// Disposes objected used by the class.
/// </summary>

Просмотреть файл

@ -207,6 +207,30 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
return new StreamingRequestHandler(bot, this, socket, audience, Logger);
}
/// <summary>
/// Create the <see cref="StreamingRequestHandler"/> for processing for a new Web Socket connection request.
/// </summary>
/// <param name="bot">The <see cref="IBot"/> implementation which will process the request.</param>
/// <param name="context">The <see cref="HttpContext"/> instance on which to accept the web socket.</param>
/// <param name="audience">The authorized audience of the incoming connection request.</param>
/// <returns>Returns a new <see cref="StreamingRequestHandler"/> implementation.</returns>
protected virtual async Task<StreamingRequestHandler> CreateStreamingRequestHandlerAsync(IBot bot, HttpContext context, string audience)
{
if (bot == null)
{
throw new ArgumentNullException(nameof(bot));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var socket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
return CreateStreamingRequestHandler(bot, socket, audience);
}
private static async Task WriteUnauthorizedResponseAsync(string headerName, HttpRequest httpRequest)
{
httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
@ -253,12 +277,10 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
try
{
var socket = await httpRequest.HttpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
// Set ClaimsIdentity on Adapter to enable Skills and User OAuth in WebSocket-based streaming scenarios.
var audience = GetAudience(claimsIdentity);
var requestHandler = CreateStreamingRequestHandler(bot, socket, audience);
var requestHandler = await CreateStreamingRequestHandlerAsync(bot, httpRequest.HttpContext, audience).ConfigureAwait(false);
if (RequestHandlers == null)
{
@ -267,7 +289,9 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
RequestHandlers.Add(requestHandler);
Log.WebSocketConnectionStarted(Logger);
await requestHandler.ListenAsync().ConfigureAwait(false);
Log.WebSocketConnectionCompleted(Logger);
}
catch (Exception ex)
{
@ -355,5 +379,18 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
return null;
}
private class Log
{
private static readonly Action<ILogger, Exception> _webSocketConnectionStarted =
LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(WebSocketConnectionStarted)), "WebSocket connection started.");
private static readonly Action<ILogger, Exception> _webSocketConnectionCompleted =
LoggerMessage.Define(LogLevel.Information, new EventId(2, nameof(WebSocketConnectionCompleted)), "WebSocket connection completed.");
public static void WebSocketConnectionStarted(ILogger logger) => _webSocketConnectionStarted(logger, null);
public static void WebSocketConnectionCompleted(ILogger logger) => _webSocketConnectionCompleted(logger, null);
}
}
}

Просмотреть файл

@ -2,9 +2,9 @@
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Net.WebSockets;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Bot.Builder.Streaming;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Connector.Streaming.Application;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Streaming;
using Microsoft.Extensions.Configuration;
@ -24,6 +25,8 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
/// </summary>
public class CloudAdapter : CloudAdapterBase, IBotFrameworkHttpAdapter
{
private readonly ConcurrentDictionary<Guid, StreamingActivityProcessor> _streamingConnections = new ConcurrentDictionary<Guid, StreamingActivityProcessor>();
/// <summary>
/// Initializes a new instance of the <see cref="CloudAdapter"/> class. (Public cloud. No auth. For testing.)
/// </summary>
@ -146,10 +149,42 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
};
// Tie the authentication results, the named pipe, the adapter and the bot together to be ready to handle any inbound activities
var streamingActivityProcessor = new StreamingActivityProcessor(authenticationRequestResult, pipeName, this, bot);
using (var streamingActivityProcessor = new StreamingActivityProcessor(authenticationRequestResult, pipeName, this, bot))
{
// Start receiving activities on the named pipe
// TODO /*_applicationLifetime?.ApplicationStopped ?? */
await streamingActivityProcessor.ListenAsync(CancellationToken.None).ConfigureAwait(false);
}
}
// Start receiving activities on the named pipe
await streamingActivityProcessor.ListenAsync().ConfigureAwait(false);
/// <inheritdoc/>
protected override ConnectorFactory GetStreamingConnectorFactory(Activity activity)
{
foreach (var connection in _streamingConnections.Values)
{
if (connection.HandlesActivity(activity))
{
return connection.GetConnectorFactory();
}
}
throw new ApplicationException($"No streaming connection found for activity: {activity}");
}
/// <summary>
/// Creates a <see cref="StreamingConnection"/> that uses web sockets.
/// </summary>
/// <param name="httpContext"><see cref="HttpContext"/> instance on which to accept the web socket.</param>
/// <param name="logger">Logger implementation for tracing and debugging information.</param>
/// <returns><see cref="StreamingConnection"/> that uses web socket.</returns>
protected virtual StreamingConnection CreateWebSocketConnection(HttpContext httpContext, ILogger logger)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
return new WebSocketStreamingConnection(httpContext, logger);
}
private async Task ConnectAsync(HttpRequest httpRequest, IBot bot, CancellationToken cancellationToken)
@ -164,29 +199,37 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
var authenticationRequestResult = await BotFrameworkAuthentication.AuthenticateStreamingRequestAsync(authHeader, channelIdHeader, cancellationToken).ConfigureAwait(false);
// Transition the request to a WebSocket connection
var socket = await httpRequest.HttpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
var connectionId = Guid.NewGuid();
using (var scope = Logger.BeginScope(connectionId))
{
var connection = CreateWebSocketConnection(httpRequest.HttpContext, Logger);
// Tie the authentication results, the socket, the adapter and the bot together to be ready to handle any inbound activities
var streamingActivityProcessor = new StreamingActivityProcessor(authenticationRequestResult, socket, this, bot);
// Start receiving activities on the socket
await streamingActivityProcessor.ListenAsync().ConfigureAwait(false);
using (var streamingActivityProcessor = new StreamingActivityProcessor(authenticationRequestResult, connection, this, bot))
{
// Start receiving activities on the socket
// TODO: pass asp.net core lifetime for cancellation here.
_streamingConnections.TryAdd(connectionId, streamingActivityProcessor);
Log.WebSocketConnectionStarted(Logger);
await streamingActivityProcessor.ListenAsync(CancellationToken.None).ConfigureAwait(false);
_streamingConnections.TryRemove(connectionId, out _);
Log.WebSocketConnectionCompleted(Logger);
}
}
}
private class StreamingActivityProcessor : IStreamingActivityProcessor
private class StreamingActivityProcessor : IStreamingActivityProcessor, IDisposable
{
private readonly AuthenticateRequestResult _authenticateRequestResult;
private readonly CloudAdapter _adapter;
private readonly StreamingRequestHandler _requestHandler;
public StreamingActivityProcessor(AuthenticateRequestResult authenticateRequestResult, WebSocket socket, CloudAdapter adapter, IBot bot)
public StreamingActivityProcessor(AuthenticateRequestResult authenticateRequestResult, StreamingConnection connection, CloudAdapter adapter, IBot bot)
{
_authenticateRequestResult = authenticateRequestResult;
_adapter = adapter;
// Internal reuse of the existing StreamingRequestHandler class
_requestHandler = new StreamingRequestHandler(bot, this, socket, _authenticateRequestResult.Audience, adapter.Logger);
_requestHandler = new StreamingRequestHandler(bot, this, connection, authenticateRequestResult.Audience, logger: adapter.Logger);
// Fix up the connector factory so connector create from it will send over this connection
_authenticateRequestResult.ConnectorFactory = new StreamingConnectorFactory(_requestHandler);
@ -204,7 +247,23 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
_authenticateRequestResult.ConnectorFactory = new StreamingConnectorFactory(_requestHandler);
}
public Task ListenAsync() => _requestHandler.ListenAsync();
public bool HandlesActivity(Activity activity)
{
return _requestHandler.ServiceUrl.Equals(activity.ServiceUrl, StringComparison.OrdinalIgnoreCase) &&
_requestHandler.HasConversation(activity.Conversation.Id);
}
public ConnectorFactory GetConnectorFactory()
{
return _authenticateRequestResult.ConnectorFactory;
}
public void Dispose()
{
((IDisposable)_requestHandler)?.Dispose();
}
public Task ListenAsync(CancellationToken cancellationToken) => _requestHandler.ListenAsync(cancellationToken);
Task<InvokeResponse> IStreamingActivityProcessor.ProcessStreamingActivityAsync(Activity activity, BotCallbackHandler callback, CancellationToken cancellationToken)
=> _adapter.ProcessActivityAsync(_authenticateRequestResult, activity, callback, cancellationToken);
@ -274,5 +333,18 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core
}
}
}
private class Log
{
private static readonly Action<ILogger, Exception> _webSocketConnectionStarted =
LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(WebSocketConnectionStarted)), "WebSocket connection started.");
private static readonly Action<ILogger, Exception> _webSocketConnectionCompleted =
LoggerMessage.Define(LogLevel.Information, new EventId(2, nameof(WebSocketConnectionCompleted)), "WebSocket connection completed.");
public static void WebSocketConnectionStarted(ILogger logger) => _webSocketConnectionStarted(logger, null);
public static void WebSocketConnectionCompleted(ILogger logger) => _webSocketConnectionCompleted(logger, null);
}
}
}

Просмотреть файл

@ -34,6 +34,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bot.Connector.Streaming" Condition=" '$(ReleasePackageVersion)' == '' " Version="$(LocalPackageVersion)" />
<PackageReference Include="Microsoft.Bot.Connector.Streaming" Condition=" '$(ReleasePackageVersion)' != '' " Version="$(ReleasePackageVersion)" />
<PackageReference Include="Microsoft.Bot.Streaming" Condition=" '$(ReleasePackageVersion)' == '' " Version="$(LocalPackageVersion)" />
<PackageReference Include="Microsoft.Bot.Streaming" Condition=" '$(ReleasePackageVersion)' != '' " Version="$(ReleasePackageVersion)" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(ReleasePackageVersion)' == '' " Version="$(LocalPackageVersion)" />
<PackageReference Include="Microsoft.Bot.Builder" Condition=" '$(ReleasePackageVersion)' != '' " Version="$(ReleasePackageVersion)" />
<PackageReference Include="Microsoft.Bot.Configuration" Condition=" '$(ReleasePackageVersion)' == '' " Version="$(LocalPackageVersion)" />
@ -44,5 +48,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.Bot.Builder\Microsoft.Bot.Builder.csproj" />
<ProjectReference Include="..\..\Microsoft.Bot.Configuration\Microsoft.Bot.Configuration.csproj" />
<ProjectReference Include="..\..\Microsoft.Bot.Connector.Streaming\Microsoft.Bot.Connector.Streaming.csproj" />
<ProjectReference Include="..\..\Microsoft.Bot.Streaming\Microsoft.Bot.Streaming.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Connector.Streaming\Microsoft.Bot.Connector.Streaming.csproj" />
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Connector\Microsoft.Bot.Connector.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using BenchmarkDotNet.Running;
namespace Microsoft.Bot.Connector.Streaming.Perf
{
public class Program
{
public static void Main(string[] args)
=> BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}

Просмотреть файл

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Connector.Streaming\Microsoft.Bot.Connector.Streaming.csproj" />
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Connector\Microsoft.Bot.Connector.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,241 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Connector.Streaming.Application;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Streaming;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Microsoft.Bot.Connector.Streaming.Tests.Client
{
public class Program
{
private static WebSocketClient _client;
private static Task _clientTask;
private static CancellationTokenSource _cancellationSource;
private static string _conversationId;
public static void Main(string[] args)
{
Menu();
do
{
try
{
DispatchAsync(Console.ReadLine()).GetAwaiter().GetResult();
}
catch (Exception ex)
{
var originalForegroundColor = Console.ForegroundColor;
WriteLine($"Error: {ex}", ConsoleColor.Red);
}
}
while (true);
}
private static async Task DispatchAsync(string command)
{
switch (command)
{
case "c":
await ConnectAsync();
break;
case "car":
await ConnectAsync(true);
break;
case "m":
await MessageAsync();
break;
case "msplit":
await MessageSplitAsync();
break;
case "sd":
await ForceServerDisconnectAsync();
break;
case "d":
await DisconnectClientAsync();
break;
case "h":
Menu();
break;
}
}
private static string AskUser(string message)
{
Console.WriteLine(message);
return Console.ReadLine();
}
private static async Task ConnectAsync(bool automaticallyReconnect = false)
{
var url = AskUser("Bot url:");
var appId = AskUser("Bot app id:");
var appPassword = AskUser("Bot app password:");
var headers = new Dictionary<string, string>() { { "channelId", "Test" } };
if (!string.IsNullOrEmpty(appId) && !string.IsNullOrEmpty(appPassword))
{
var credentials = new MicrosoftAppCredentials(appId, appPassword);
var token = await credentials.GetTokenAsync();
headers.Add("Authorization", $"Bearer {token}");
}
var configureNamedOptions = new ConfigureNamedOptions<ConsoleLoggerOptions>(string.Empty, null);
var optionsFactory = new OptionsFactory<ConsoleLoggerOptions>(new[] { configureNamedOptions }, Enumerable.Empty<IPostConfigureOptions<ConsoleLoggerOptions>>());
var optionsMonitor = new OptionsMonitor<ConsoleLoggerOptions>(optionsFactory, Enumerable.Empty<IOptionsChangeTokenSource<ConsoleLoggerOptions>>(), new OptionsCache<ConsoleLoggerOptions>());
// Improvement opportunity: expose command / argument to control log level.
var loggerFactory = new LoggerFactory(new[] { new ConsoleLoggerProvider(optionsMonitor) }, new LoggerFilterOptions { MinLevel = LogLevel.Debug });
_cancellationSource = new CancellationTokenSource();
_client = new WebSocketClient(url, new ConsoleRequestHandler(), logger: loggerFactory.CreateLogger("WebSocketClient"));
_client.Disconnected += Client_Disconnected;
_clientTask = _client.ConnectAsync(headers, _cancellationSource.Token);
}
private static void Client_Disconnected(object sender, Bot.Streaming.Transport.DisconnectedEventArgs e)
{
WriteLine($"[Program] Client disconnected. Reason: {e?.Reason}.", foregroundColor: ConsoleColor.Yellow);
var response = AskUser("Attempt to reconnect the existing connection? y / n");
// Let the client gracefully finish
WriteLine("[Program] Waiting for graceful completion...", foregroundColor: ConsoleColor.Yellow);
_clientTask.GetAwaiter().GetResult();
if (response == "y")
{
WriteLine("[Program] Reconnecting...");
ConnectAsync().GetAwaiter().GetResult();
}
else
{
WriteLine("[Program] Client shut down completed gracefully");
}
}
private static async Task MessageAsync()
{
if (_client == null || !_client.IsConnected)
{
WriteLine("[Program] Client is not connected, connect before sending messages.");
}
var text = AskUser("[Program] Enter text:");
WriteLine($"[User]: {text}", ConsoleColor.Cyan);
if (string.IsNullOrEmpty(_conversationId))
{
_conversationId = Guid.NewGuid().ToString();
}
var activity = new Schema.Activity()
{
Id = Guid.NewGuid().ToString(),
Type = ActivityTypes.Message,
From = new ChannelAccount { Id = "testUser" },
Conversation = new ConversationAccount { Id = _conversationId },
Recipient = new ChannelAccount { Id = "testBot" },
ServiceUrl = "wss://InvalidServiceUrl/api/messages",
ChannelId = "Test",
Text = text,
};
var request = StreamingRequest.CreatePost("/api/messages", new StringContent(JsonConvert.SerializeObject(activity), Encoding.UTF8, "application/json"));
var stopwatch = Stopwatch.StartNew();
var response = await _client.SendAsync(request, CancellationToken.None);
}
private static Task MessageSplitAsync()
{
throw new NotImplementedException();
}
private static Task ForceServerDisconnectAsync()
{
throw new NotImplementedException();
}
private static async Task DisconnectClientAsync()
{
await _client.DisconnectAsync();
if (_cancellationSource != null)
{
_cancellationSource.Cancel();
_cancellationSource.Dispose();
_cancellationSource = null;
}
await _clientTask;
}
private static void Menu()
{
Console.WriteLine("Welcome to the streaming client.");
Console.WriteLine("Commands:");
Console.WriteLine("c - Connect client");
Console.WriteLine("car - Connect client with automatic reconnect");
Console.WriteLine("m - Send message activity to bot");
Console.WriteLine("msplit - Send message activity to bot, split between request and stream, allowing commands in between.");
Console.WriteLine("sd - Force server disconnect");
Console.WriteLine("d - Disconnect client");
Console.WriteLine("h - Help");
}
private static void WriteLine(string message, ConsoleColor foregroundColor = ConsoleColor.White, ConsoleColor backgroundColor = ConsoleColor.Black)
{
// Save original color.
//var originalForegroundColor = Console.ForegroundColor;
//var originalBackgroundColor = Console.BackgroundColor;
// Set requested color.
Console.ForegroundColor = foregroundColor;
Console.BackgroundColor = backgroundColor;
// Write message.
Console.WriteLine(message);
// Restore original colors.
Console.ResetColor();
//var Console.ForegroundColor = originalForegroundColor;
//var Console.BackgroundColor = originalBackgroundColor;
}
private class ConsoleRequestHandler : RequestHandler
{
public override async Task<StreamingResponse> ProcessRequestAsync(ReceiveRequest request, ILogger<RequestHandler> logger, object context = null, CancellationToken cancellationToken = default)
{
var response = await request.ReadBodyAsJsonAsync<Schema.Activity>().ConfigureAwait(false);
System.Console.WriteLine($"[Bot]: {response?.Text}");
return await Task.FromResult(StreamingResponse.OK()).ConfigureAwait(false);
}
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
namespace Microsoft.Bot.Connector.Streaming.Tests.Server
{
// This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot
// implementation at runtime. Multiple different IBot implementations running at different endpoints can be
// achieved by specifying a more specific type for the bot constructor argument.
[Route("api/messages")]
[ApiController]
public class BotController : ControllerBase
{
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly IBot _bot;
public BotController(IBotFrameworkHttpAdapter adapter, IBot bot)
{
_adapter = adapter;
_bot = bot;
}
[HttpPost]
[HttpGet]
public async Task PostAsync()
{
// Delegate the processing of the HTTP POST to the adapter.
// The adapter will invoke the bot.
await _adapter.ProcessAsync(Request, Response, _bot);
}
}
}

Просмотреть файл

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime\Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="snapshot.dialog">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Microsoft.Bot.Connector.Streaming.Tests.Server
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureLogging((logging) =>
{
logging.AddDebug();
logging.AddConsole();
});
webBuilder.UseStartup<Startup>();
});
}
}

Просмотреть файл

@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Bot.Builder.Dialogs.Adaptive.Runtime.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.Bot.Connector.Streaming.Tests.Server
{
public class Startup
{
private readonly IConfiguration _configuration;
//1private readonly IHostApplicationLifetime _hostAppLifetime;
public Startup(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
//_hostAppLifetime = appLifetime ?? throw new ArgumentNullException(nameof(appLifetime));
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson();
services.AddBotRuntime(_configuration);
services.Configure<HostOptions>(
opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(30));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles()
.UseStaticFiles()
.UseWebSockets()
.UseRouting()
.UseAuthorization()
.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
},
"AllowedHosts": "*",
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"ConnectionName": "",
"defaultRootDialog": "snapshot.dialog"
}

Просмотреть файл

@ -0,0 +1,19 @@
{
"$kind": "Microsoft.AdaptiveDialog",
"triggers": [
{
"$kind": "Microsoft.OnMessageActivity",
"actions": [
{
"$kind": "Microsoft.SendActivity",
"activity": "Hello world"
},
{
"$kind": "Microsoft.SetProperty",
"property": "turn.x",
"value": "y"
}
]
}
]
}

Просмотреть файл

@ -0,0 +1,216 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.WebSockets;
using System.Runtime.InteropServices.ComTypes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Application;
using Microsoft.Bot.Streaming;
using Microsoft.Bot.Streaming.Payloads;
using Microsoft.Bot.Streaming.Transport;
using Microsoft.Bot.Streaming.Transport.NamedPipes;
using Microsoft.Bot.Streaming.Transport.WebSockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.Bot.Connector.Streaming.Tests.Application
{
public class LegacyStreamingConnectionTests
{
[Fact]
public void ConstructorTests()
{
var webSocketConnection = new LegacyStreamingConnection(new TestWebSocket(), null);
var namedPipeConnection = new LegacyStreamingConnection("test", null);
}
[Fact]
public void CannotCreateWithoutValidWebSocket()
{
Assert.Throws<ArgumentNullException>(() =>
{
WebSocket socket = null;
_ = new LegacyStreamingConnection(socket, NullLogger.Instance);
});
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void CannotCreateWithoutValidPipeName(string pipeName)
{
Assert.Throws<ArgumentNullException>(() =>
{
_ = new LegacyStreamingConnection(pipeName, NullLogger.Instance);
});
}
[Fact]
public void CanCreateWebSocketServer()
{
var socket = new TestWebSocket();
var requestHandler = new TestRequestHandler();
var sut = new LegacyStreamingConnection(socket, NullLogger.Instance);
var server = sut.CreateStreamingTransportServer(requestHandler);
Assert.True(server is WebSocketServer);
}
[Fact]
public void CanCreateNamedPipeServer()
{
var requestHandler = new TestRequestHandler();
var sut = new LegacyStreamingConnection("test", NullLogger.Instance);
var server = sut.CreateStreamingTransportServer(requestHandler);
Assert.True(server is NamedPipeServer);
}
[Fact]
public void CanSendStreamingRequest()
{
var socket = new TestWebSocket();
var requestHandler = new TestRequestHandler();
using (var sut = new TestLegacyStreamingConnection(socket, NullLogger.Instance))
{
sut.ListenAsync(requestHandler).Wait();
var request = new StreamingRequest
{
Verb = "POST",
Path = "/api/messages",
Streams = new List<ResponseMessageStream>
{
new ResponseMessageStream { Content = new StringContent("foo") }
}
};
var response = sut.SendStreamingRequestAsync(request).Result;
Assert.Equal(request.Streams.Count, response.Streams.Count);
Assert.Equal(request.Streams[0].Id, response.Streams[0].Id);
}
}
private class TestLegacyStreamingConnection : LegacyStreamingConnection
{
public TestLegacyStreamingConnection(WebSocket socket, ILogger logger, DisconnectedEventHandler onServerDisconnect = null)
: base(socket, logger, onServerDisconnect)
{
}
public TestLegacyStreamingConnection(string pipeName, ILogger logger, DisconnectedEventHandler onServerDisconnect = null)
: base(pipeName, logger, onServerDisconnect)
{
}
internal override IStreamingTransportServer CreateStreamingTransportServer(RequestHandler requestHandler)
{
return new TestStreamingTransportServer();
}
}
private class TestStreamingTransportServer : IStreamingTransportServer, IDisposable
{
public event DisconnectedEventHandler Disconnected;
public Task StartAsync()
{
return Task.CompletedTask;
}
public Task<ReceiveResponse> SendAsync(StreamingRequest request, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(new ReceiveResponse
{
StatusCode = 200,
Streams = new List<IContentStream>(request.Streams.Select(s => new TestContentStream(s.Id)))
});
}
public void Dispose()
{
if (Disconnected != null)
{
Disconnected(this, DisconnectedEventArgs.Empty);
}
}
}
private class TestWebSocket : WebSocket
{
public override WebSocketCloseStatus? CloseStatus { get; }
public override string CloseStatusDescription { get; }
public override WebSocketState State { get; }
public override string SubProtocol { get; }
public override void Abort()
{
throw new NotImplementedException();
}
public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public override void Dispose()
{
throw new NotImplementedException();
}
public override Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
private class TestRequestHandler : RequestHandler
{
public override Task<StreamingResponse> ProcessRequestAsync(ReceiveRequest request, ILogger<RequestHandler> logger, object context = null, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(new StreamingResponse { StatusCode = 200 });
}
}
private class TestContentStream : IContentStream
{
public TestContentStream(Guid id)
{
Id = id;
}
public Guid Id { get; }
public string ContentType { get; set; }
public int? Length { get; set; }
public Stream Stream { get; }
}
}
}

Просмотреть файл

@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Application;
namespace Microsoft.Bot.Connector.Streaming.Tests.Application
{
// This object holds onto a TimerAwaitable referencing the callback (the async continuation is the callback)
// it also has a finalizer that triggers a tcs so callers can be notified when this object is being cleaned up.
public class ObjectWithTimerAwaitable
{
private readonly TimerAwaitable _timer;
private readonly TaskCompletionSource<bool> _tcs;
public ObjectWithTimerAwaitable(TaskCompletionSource<bool> tcs)
{
_tcs = tcs;
_timer = new TimerAwaitable(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(1));
_timer.Start();
}
~ObjectWithTimerAwaitable()
{
_tcs.TrySetResult(true);
}
public async Task Start()
{
using (_timer)
{
while (await _timer)
{
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Application;
using Microsoft.Bot.Connector.Streaming.Tests.Tools;
using Xunit;
namespace Microsoft.Bot.Connector.Streaming.Tests.Application
{
public class TimerAwaitableTests
{
[Fact]
public async Task FinalizerRunsIfTimerAwaitableReferencesObject()
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
UseTimerAwaitableAndUnref(tcs);
GC.Collect();
GC.WaitForPendingFinalizers();
// Make sure the finalizer runs
await tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(30));
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void UseTimerAwaitableAndUnref(TaskCompletionSource<bool> tcs)
{
_ = new ObjectWithTimerAwaitable(tcs).Start();
}
}
}

Просмотреть файл

@ -0,0 +1,140 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Application;
using Microsoft.Bot.Connector.Streaming.Tests.Features;
using Microsoft.Bot.Connector.Streaming.Tests.Tools;
using Microsoft.Bot.Streaming;
using Moq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
public class ApplicationToApplicationIntegrationTests
{
private readonly ITestOutputHelper _outputHelper;
public ApplicationToApplicationIntegrationTests(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
}
[Fact]
public async Task Integration_ListenSendShutDownServer()
{
// TODO: Transform this test into a theory and do multi-message, multi-thread, multi-client, etc.
var logger = XUnitLogger.CreateLogger(_outputHelper);
using (var webSocketFeature = new TestWebSocketConnectionFeature())
{
// Bot / server setup
var botRequestHandler = new Mock<RequestHandler>();
botRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
var connection = new WebSocketStreamingConnection(logger);
var socket = await webSocketFeature.AcceptAsync().ConfigureAwait(false);
var serverTask = Task.Run(() => connection.ListenInternalAsync(socket, botRequestHandler.Object));
// Client / channel setup
var clientRequestHandler = new Mock<RequestHandler>();
clientRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
var client = new WebSocketClient(clientRequestHandler.Object, logger: logger);
var clientTask = Task.Run(() => client.ConnectInternalAsync(webSocketFeature.Client, CancellationToken.None));
// Send request bot (server) -> channel (client)
const string path = "api/version";
const string botToClientPayload = "Hello human, I'm Bender!";
var request = StreamingRequest.CreatePost(path, new StringContent(botToClientPayload));
var responseFromClient = await connection.SendStreamingRequestAsync(request).ConfigureAwait(false);
Assert.Equal(200, responseFromClient.StatusCode);
const string clientToBotPayload = "Hello bot, I'm Calculon!";
var clientRequest = StreamingRequest.CreatePost(path, new StringContent(clientToBotPayload));
// Send request bot channel (client) -> (server)
var clientToBotResult = await client.SendAsync(clientRequest).ConfigureAwait(false);
Assert.Equal(200, clientToBotResult.StatusCode);
await client.DisconnectAsync().ConfigureAwait(false);
await clientTask.ConfigureAwait(false);
await serverTask.ConfigureAwait(false);
}
}
[Fact]
public async Task Integration_KeepAlive()
{
// TODO: Transform this test into a theory and do multi-message, multi-thread, multi-client, etc.
var logger = XUnitLogger.CreateLogger(_outputHelper);
var cts = new CancellationTokenSource();
using (var webSocketFeature = new TestWebSocketConnectionFeature())
{
// Bot / server setup
var botRequestHandler = new Mock<RequestHandler>();
botRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
var connection = new WebSocketStreamingConnection(logger);
var socket = await webSocketFeature.AcceptAsync().ConfigureAwait(false);
var serverTask = Task.Run(() => connection.ListenInternalAsync(socket, botRequestHandler.Object, cts.Token));
// Client / channel setup
var clientRequestHandler = new Mock<RequestHandler>();
clientRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
var client = new WebSocketClient(clientRequestHandler.Object, logger: logger, closeTimeOut: TimeSpan.FromSeconds(10), keepAlive: TimeSpan.FromMilliseconds(200));
var clientTask = Task.Run(() => client.ConnectInternalAsync(webSocketFeature.Client, CancellationToken.None));
// Send request bot (server) -> channel (client)
const string path = "api/version";
const string botToClientPayload = "Hello human, I'm Bender!";
var request = StreamingRequest.CreatePost(path, new StringContent(botToClientPayload));
var responseFromClient = await connection.SendStreamingRequestAsync(request).ConfigureAwait(false);
Assert.Equal(200, responseFromClient.StatusCode);
const string clientToBotPayload = "Hello bot, I'm Calculon!";
var clientRequest = StreamingRequest.CreatePost(path, new StringContent(clientToBotPayload));
// Send request bot channel (client) -> (server)
var clientToBotResult = await client.SendAsync(clientRequest).ConfigureAwait(false);
Assert.Equal(200, clientToBotResult.StatusCode);
await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);
cts.Cancel();
await clientTask.ConfigureAwait(false);
await serverTask.ConfigureAwait(false);
}
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
public class EndToEndMiniLoadTests
{
}
}

Просмотреть файл

@ -0,0 +1,317 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Application;
using Microsoft.Bot.Connector.Streaming.Tests.Features;
using Microsoft.Bot.Connector.Streaming.Tests.Tools;
using Microsoft.Bot.Streaming;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
public class InteropApplicationIntegrationTests
{
private readonly ITestOutputHelper _outputHelper;
public InteropApplicationIntegrationTests(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
}
[Fact]
public async Task Integration_Interop_LegacyClient()
{
// TODO: Transform this test into a theory and do multi-message, multi-thread, multi-client, etc.
var logger = XUnitLogger.CreateLogger(_outputHelper);
using (var webSocketFeature = new TestWebSocketConnectionFeature())
{
// Bot / server setup
var botRequestHandler = new Mock<RequestHandler>();
botRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
var connection = new WebSocketStreamingConnection(logger);
var socket = await webSocketFeature.AcceptAsync().ConfigureAwait(false);
var serverTask = Task.Run(() => connection.ListenInternalAsync(socket, botRequestHandler.Object));
// Client / channel setup
var clientRequestHandler = new Mock<RequestHandler>();
clientRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
using (var client = new Microsoft.Bot.Streaming.Transport.WebSockets.WebSocketClient("wss://test", clientRequestHandler.Object))
{
await client.ConnectInternalAsync(webSocketFeature.Client).ConfigureAwait(false);
// Send request bot (server) -> channel (client)
const string path = "api/version";
const string botToClientPayload = "Hello human, I'm Bender!";
var request = StreamingRequest.CreatePost(path, new StringContent(botToClientPayload));
var responseFromClient = await connection.SendStreamingRequestAsync(request).ConfigureAwait(false);
Assert.Equal(200, responseFromClient.StatusCode);
const string clientToBotPayload = "Hello bot, I'm Calculon!";
var clientRequest = StreamingRequest.CreatePost(path, new StringContent(clientToBotPayload));
// Send request bot channel (client) -> (server)
var clientToBotResult = await client.SendAsync(clientRequest).ConfigureAwait(false);
Assert.Equal(200, clientToBotResult.StatusCode);
client.Disconnect();
}
await serverTask.ConfigureAwait(false);
}
}
[Theory]
[InlineData(32, 1024)]
[InlineData(4, 1000)]
[InlineData(4, 100)]
[InlineData(8, 100)]
[InlineData(16, 100)]
[InlineData(32, 100)]
public async Task Integration_Interop_LegacyClient_MiniLoad(int threadCount, int messageCount)
{
var logger = XUnitLogger.CreateLogger(_outputHelper);
using (var webSocketFeature = new TestWebSocketConnectionFeature())
{
var botRequestHandler = new Mock<RequestHandler>();
botRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
var connection = new WebSocketStreamingConnection(logger);
var socket = await webSocketFeature.AcceptAsync().ConfigureAwait(false);
var serverTask = Task.Run(() => connection.ListenInternalAsync(socket, botRequestHandler.Object));
await Task.Delay(TimeSpan.FromSeconds(1));
var clients = new List<Microsoft.Bot.Streaming.Transport.WebSockets.WebSocketClient>();
var clientRequestHandler = new Mock<RequestHandler>();
clientRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
using (var client = new Microsoft.Bot.Streaming.Transport.WebSockets.WebSocketClient(
"wss://test",
clientRequestHandler.Object))
{
await client.ConnectInternalAsync(webSocketFeature.Client).ConfigureAwait(false);
clients.Add(client);
// Send request bot (server) -> channel (client)
const string path = "api/version";
const string botToClientPayload = "Hello human, I'm Bender!";
Func<int, Task> testFlow = async (i) =>
{
var request = StreamingRequest.CreatePost(path, new StringContent(botToClientPayload));
var stopwatch = Stopwatch.StartNew();
var responseFromClient =
await connection.SendStreamingRequestAsync(request).ConfigureAwait(false);
stopwatch.Stop();
Assert.Equal(200, responseFromClient.StatusCode);
logger.LogInformation(
$"Server->Client {i} latency: {stopwatch.ElapsedMilliseconds}. Status code: {responseFromClient.StatusCode}");
const string clientToBotPayload = "Hello bot, I'm Calculon!";
var clientRequest = StreamingRequest.CreatePost(path, new StringContent(clientToBotPayload));
stopwatch = Stopwatch.StartNew();
// Send request bot channel (client) -> (server)
var clientToBotResult = await client.SendAsync(clientRequest).ConfigureAwait(false);
stopwatch.Stop();
Assert.Equal(200, clientToBotResult.StatusCode);
logger.LogInformation(
$"Client->Server {i} latency: {stopwatch.ElapsedMilliseconds}. Status code: {responseFromClient.StatusCode}");
};
await testFlow(-1).ConfigureAwait(false);
var tasks = new List<Task>();
using (var throttler = new SemaphoreSlim(threadCount))
{
for (int j = 0; j < messageCount; j++)
{
await throttler.WaitAsync().ConfigureAwait(false);
// using Task.Run(...) to run the lambda in its own parallel
// flow on the threadpool
tasks.Add(
Task.Run(async () =>
{
try
{
await testFlow(j).ConfigureAwait(false);
}
finally
{
throttler.Release();
}
}));
}
await Task.WhenAll(tasks).ConfigureAwait(false);
}
client.Disconnect();
}
await serverTask.ConfigureAwait(false);
}
}
[Theory]
[InlineData(32, 1000)]
public async Task Integration_NewClient_MiniLoad(int threadCount, int messageCount)
{
// TODO: Transform this test into a theory and do multi-message, multi-thread, multi-client, etc.
var logger = XUnitLogger.CreateLogger(_outputHelper);
using (var webSocketFeature = new TestWebSocketConnectionFeature())
{
// Bot / server setup
var botRequestHandler = new Mock<RequestHandler>();
botRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
var connection = new WebSocketStreamingConnection(logger);
var socket = await webSocketFeature.AcceptAsync().ConfigureAwait(false);
var serverTask = Task.Run(() => connection.ListenInternalAsync(socket, botRequestHandler.Object));
await Task.Delay(TimeSpan.FromSeconds(1));
//Parallel.For(0, clientCount, async i =>
{
// Client / channel setup
var clientRequestHandler = new Mock<RequestHandler>();
clientRequestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 });
using (var client = new WebSocketClient($"wss://test", clientRequestHandler.Object, logger: logger))
{
var clientTask = client.ConnectInternalAsync(webSocketFeature.Client, CancellationToken.None);
// Send request bot (server) -> channel (client)
const string path = "api/version";
const string botToClientPayload = "Hello human, I'm Bender!";
Func<int, Task> testFlow = async (i) =>
{
var request = StreamingRequest.CreatePost(path, new StringContent(botToClientPayload));
var stopwatch = Stopwatch.StartNew();
var responseFromClient = await connection.SendStreamingRequestAsync(request).ConfigureAwait(false);
stopwatch.Stop();
Assert.Equal(200, responseFromClient.StatusCode);
logger.LogInformation($"Server->Client {i} latency: {stopwatch.ElapsedMilliseconds}. Status code: {responseFromClient.StatusCode}");
const string clientToBotPayload = "Hello bot, I'm Calculon!";
var clientRequest = StreamingRequest.CreatePost(path, new StringContent(clientToBotPayload));
stopwatch = Stopwatch.StartNew();
// Send request bot channel (client) -> (server)
var clientToBotResult = await client.SendAsync(clientRequest).ConfigureAwait(false);
stopwatch.Stop();
Assert.Equal(200, clientToBotResult.StatusCode);
logger.LogInformation($"Client->Server {i} latency: {stopwatch.ElapsedMilliseconds}. Status code: {responseFromClient.StatusCode}");
};
await testFlow(-1).ConfigureAwait(false);
var tasks = new List<Task>();
using (var throttler = new SemaphoreSlim(threadCount))
{
for (int j = 0; j < messageCount; j++)
{
await throttler.WaitAsync().ConfigureAwait(false);
// using Task.Run(...) to run the lambda in its own parallel
// flow on the threadpool
tasks.Add(
Task.Run(async () =>
{
try
{
await testFlow(j).ConfigureAwait(false);
}
finally
{
throttler.Release();
}
}));
}
await Task.WhenAll(tasks).ConfigureAwait(false);
}
await client.DisconnectAsync().ConfigureAwait(false);
await clientTask.ConfigureAwait(false);
}
await serverTask.ConfigureAwait(false);
}
}
}
private static void RunWithLimitedParalelism(List<Task> tasks, int maxTasksToRunInParallel, int timeoutInMilliseconds, CancellationToken cancellationToken = new CancellationToken())
{
// Convert to a list of tasks so that we don&#39;t enumerate over it multiple times needlessly.
using (var throttler = new SemaphoreSlim(maxTasksToRunInParallel))
{
var postTaskTasks = new List<Task>();
// Have each task notify the throttler when it completes so that it decrements the number of tasks currently running.
tasks.ForEach(t => postTaskTasks.Add(t.ContinueWith(tsk => throttler.Release())));
// Start running each task.
foreach (var task in tasks)
{
// Increment the number of tasks currently running and wait if too many are running.
throttler.Wait(timeoutInMilliseconds, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
task.Start();
}
// Wait for all of the provided tasks to complete.
// We wait on the list of "post" tasks instead of the original tasks, otherwise there is a potential race condition where the throttler&#39;s using block is exited before some Tasks have had their "post" action completed, which references the throttler, resulting in an exception due to accessing a disposed object.
Task.WaitAll(postTaskTasks.ToArray(), cancellationToken);
}
}
}
}

Просмотреть файл

@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Tests.Features;
using Microsoft.Bot.Connector.Streaming.Transport;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
public class WebSocketTransportClientServerTests
{
[Fact]
public async Task WebSocketTransport_ClientServer_WhatIsSentIsReceived()
{
var serverPipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var clientPipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
using (var webSocketFeature = new TestWebSocketConnectionFeature())
{
// Build server transport
var serverTransport = new WebSocketTransport(serverPipePair.Application, NullLogger.Instance);
// Accept server web socket, start receiving / sending at the transport level
var serverTask = serverTransport.ProcessSocketAsync(await webSocketFeature.AcceptAsync(), CancellationToken.None);
var clientTransport = new WebSocketTransport(clientPipePair.Application, NullLogger.Instance);
var clientTask = clientTransport.ProcessSocketAsync(webSocketFeature.Client, CancellationToken.None);
// Send a frame client -> server
await clientPipePair.Transport.Output.WriteAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello")));
await clientPipePair.Transport.Output.FlushAsync();
var result = await serverPipePair.Transport.Input.ReadAsync();
var buffer = result.Buffer;
Assert.Equal("Hello", Encoding.UTF8.GetString(buffer.ToArray()));
serverPipePair.Transport.Input.AdvanceTo(buffer.End);
// Send a frame server -> client
await serverPipePair.Transport.Output.WriteAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("World")));
await serverPipePair.Transport.Output.FlushAsync();
var clientResult = await clientPipePair.Transport.Input.ReadAsync();
buffer = clientResult.Buffer;
Assert.Equal("World", Encoding.UTF8.GetString(buffer.ToArray()));
clientPipePair.Transport.Input.AdvanceTo(buffer.End);
clientPipePair.Transport.Output.Complete();
serverPipePair.Transport.Output.Complete();
// The transport should finish now
await serverTask;
await clientTask;
}
}
}
}

Просмотреть файл

@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework Condition="'$(BuildTarget)' == 'netcoreapp21'">netcoreapp2.1</TargetFramework>
<TargetFramework Condition="'$(BuildTarget)' == 'netcoreapp31'">netcoreapp3.1</TargetFramework>
<TargetFrameworks Condition="'$(BuildTarget)' == ''">netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netcoreapp3.1'">
<PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="2.7.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<DotNetCliToolReference Include="dotnet-reportgenerator-cli" Version="4.3.0-rc2" />
<PackageReference Include="Microsoft.Bot.Connector.DirectLine" Version="3.0.2" />
<PackageReference Include="Microsoft.CodeCoverage" Version="16.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.1.1" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="NunitXml.TestLogger" Version="2.1.41" />
<PackageReference Include="ReportGenerator" Version="4.3.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="XunitXml.TestLogger" Version="2.1.26" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\libraries\integration\Microsoft.Bot.Builder.Integration.AspNet.Core\Microsoft.Bot.Builder.Integration.AspNet.Core.csproj" />
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Builder\Microsoft.Bot.Builder.csproj" />
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Schema\Microsoft.Bot.Schema.csproj" />
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Connector.Streaming\Microsoft.Bot.Connector.Streaming.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,194 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.Bot.Connector.Streaming.Session;
using Microsoft.Bot.Connector.Streaming.Transport;
using Microsoft.Bot.Streaming;
using Microsoft.Bot.Streaming.Payloads;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Newtonsoft.Json;
using Xunit;
using static Microsoft.Bot.Connector.Streaming.Session.StreamingSession;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
public class ProtocolDispatcherTests
{
[Fact]
public void ProtocolDispatcher_NullSession_Throws()
{
Assert.Throws<ArgumentNullException>(() => new ProtocolDispatcher(null));
}
[Fact]
public void ProtocolDispatcher_DispatchRequest()
{
// Arrange
var request = new RequestPayload()
{
Verb = "GET",
Path = "api/version",
Streams = new List<StreamDescription>()
{
new StreamDescription() { ContentType = "json", Id = Guid.NewGuid().ToString(), Length = 18 },
new StreamDescription() { ContentType = "text", Id = Guid.NewGuid().ToString(), Length = 24 }
}
};
var requestJson = JsonConvert.SerializeObject(request);
var requestBytes = Encoding.UTF8.GetBytes(requestJson);
var header = new Header()
{
End = true,
Id = Guid.NewGuid(),
PayloadLength = requestBytes.Length,
Type = PayloadTypes.Request
};
var callCount = 0;
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var requestHandler = new Mock<RequestHandler>();
var session = new Mock<StreamingSession>(requestHandler.Object, transportHandler.Object, NullLogger.Instance, CancellationToken.None);
session.Setup(
s => s.ReceiveRequest(It.IsAny<Header>(), It.IsAny<ReceiveRequest>()))
.Callback((Header h, ReceiveRequest r) =>
{
callCount++;
// Assert
Assert.Equal(h.Id, header.Id);
Assert.Equal(request.Verb, r.Verb);
Assert.Equal(request.Path, r.Path);
Assert.Equal(request.Streams.Count, r.Streams.Count);
var firstStream = r.Streams.First() as StreamDefinition;
Assert.Equal(request.Streams.First().Id, firstStream.Id.ToString());
Assert.Equal(request.Streams.First().Length, firstStream.Length);
Assert.IsType<MemoryStream>(firstStream.Stream);
Assert.Equal(h.Id, firstStream.PayloadId);
});
var dispatcher = new ProtocolDispatcher(session.Object);
// Act
dispatcher.OnNext((header, new ReadOnlySequence<byte>(requestBytes)));
// Assert
Assert.Equal(1, callCount);
}
[Fact]
public void ProtocolDispatcher_DispatchResponse()
{
// Arrange
var request = new ResponsePayload()
{
StatusCode = 200,
Streams = new List<StreamDescription>()
{
new StreamDescription() { ContentType = "json", Id = Guid.NewGuid().ToString(), Length = 18 },
new StreamDescription() { ContentType = "text", Id = Guid.NewGuid().ToString(), Length = 24 }
}
};
var requestJson = JsonConvert.SerializeObject(request);
var requestBytes = Encoding.UTF8.GetBytes(requestJson);
var header = new Header()
{
End = true,
Id = Guid.NewGuid(),
PayloadLength = requestBytes.Length,
Type = PayloadTypes.Response
};
var callCount = 0;
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var requestHandler = new Mock<RequestHandler>();
var session = new Mock<StreamingSession>(requestHandler.Object, transportHandler.Object, NullLogger.Instance, CancellationToken.None);
session.Setup(
s => s.ReceiveResponse(It.IsAny<Header>(), It.IsAny<ReceiveResponse>()))
.Callback((Header h, ReceiveResponse r) =>
{
callCount++;
// Assert
Assert.Equal(h.Id, header.Id);
Assert.Equal(request.StatusCode, r.StatusCode);
Assert.Equal(request.Streams.Count, r.Streams.Count);
var firstStream = r.Streams.First() as StreamDefinition;
Assert.Equal(request.Streams.First().Id, firstStream.Id.ToString());
Assert.Equal(request.Streams.First().Length, firstStream.Length);
Assert.IsType<MemoryStream>(firstStream.Stream);
Assert.Equal(0, firstStream.Stream.Length);
});
var dispatcher = new ProtocolDispatcher(session.Object);
// Act
dispatcher.OnNext((header, new ReadOnlySequence<byte>(requestBytes)));
// Assert
Assert.Equal(1, callCount);
}
[Fact]
public void ProtocolDispatcher_DispatchStream()
{
// Arrange
var buffer = new byte[256];
new Random().NextBytes(buffer);
var header = new Header()
{
End = true,
Id = Guid.NewGuid(),
PayloadLength = buffer.Length,
Type = PayloadTypes.Stream
};
var callCount = 0;
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var requestHandler = new Mock<RequestHandler>();
var session = new Mock<StreamingSession>(requestHandler.Object, transportHandler.Object, NullLogger.Instance, CancellationToken.None);
session
.Setup(s => s.ReceiveStream(It.IsAny<Header>(), It.IsAny<ArraySegment<byte>>()))
.Callback((Header h, ArraySegment<byte> s) =>
{
callCount++;
// Assert
Assert.Equal(h.Id, header.Id);
Assert.True(s.Array.SequenceEqual(buffer));
});
var dispatcher = new ProtocolDispatcher(session.Object);
// Act
dispatcher.OnNext((header, new ReadOnlySequence<byte>(buffer)));
// Assert
Assert.Equal(1, callCount);
}
}
}

Просмотреть файл

@ -0,0 +1,369 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Session;
using Microsoft.Bot.Connector.Streaming.Transport;
using Microsoft.Bot.Streaming;
using Microsoft.Bot.Streaming.Payloads;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
using static Microsoft.Bot.Connector.Streaming.Session.StreamingSession;
using RequestModel = Microsoft.Bot.Connector.Streaming.Payloads.RequestPayload;
using ResponseModel = Microsoft.Bot.Connector.Streaming.Payloads.ResponsePayload;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
public class StreamingSessionTests
{
public static IEnumerable<object[]> ReceiveRequestParameterValidationData =>
new List<object[]>
{
new object[] { new Header() { Type = PayloadTypes.Request }, null, typeof(ArgumentNullException) },
new object[] { null, new ReceiveRequest(), typeof(ArgumentNullException) },
new object[] { new Header() { Type = PayloadTypes.Response }, new ReceiveRequest(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.Stream }, new ReceiveRequest(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.CancelStream }, new ReceiveRequest(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.CancelAll }, new ReceiveRequest(), typeof(InvalidOperationException) },
};
public static IEnumerable<object[]> ReceiveResponseParameterValidationData =>
new List<object[]>
{
new object[] { new Header() { Type = PayloadTypes.Response }, null, typeof(ArgumentNullException) },
new object[] { null, new ReceiveResponse(), typeof(ArgumentNullException) },
new object[] { new Header() { Type = PayloadTypes.Request }, new ReceiveResponse(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.Stream }, new ReceiveResponse(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.CancelStream }, new ReceiveResponse(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.CancelAll }, new ReceiveResponse(), typeof(InvalidOperationException) },
};
public static IEnumerable<object[]> ReceiveStreamParameterValidationData =>
new List<object[]>
{
new object[] { new Header() { Type = PayloadTypes.Response }, null, typeof(ArgumentNullException) },
new object[] { null, Array.Empty<byte>(), typeof(ArgumentNullException) },
new object[] { new Header() { Type = PayloadTypes.Request }, Array.Empty<byte>(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.Response }, Array.Empty<byte>(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.CancelStream }, Array.Empty<byte>(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.CancelAll }, Array.Empty<byte>(), typeof(InvalidOperationException) },
};
public static IEnumerable<object[]> SendResponseParameterValidationData =>
new List<object[]>
{
new object[] { new Header() { Type = PayloadTypes.Response }, null, typeof(ArgumentNullException) },
new object[] { null, new StreamingResponse(), typeof(ArgumentNullException) },
new object[] { new Header() { Type = PayloadTypes.Request }, new StreamingResponse(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.Stream }, new StreamingResponse(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.CancelAll }, new StreamingResponse(), typeof(InvalidOperationException) },
new object[] { new Header() { Type = PayloadTypes.CancelStream }, new StreamingResponse(), typeof(InvalidOperationException) },
};
[Fact]
public void StreamingSession_Constructor_NullRequestHandler_Throws()
{
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
Assert.Throws<ArgumentNullException>(
() => new StreamingSession(null, transportHandler.Object, NullLogger.Instance));
}
[Fact]
public void StreamingSession_Constructor_NullTransportHandler_Throws()
{
Assert.Throws<ArgumentNullException>(
() => new StreamingSession(new Mock<RequestHandler>().Object, null, NullLogger.Instance));
}
[Fact]
public async Task StreamingSession_SendRequest_ParameterValidation()
{
// Arrange
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var session = new StreamingSession(new Mock<RequestHandler>().Object, transportHandler.Object, NullLogger.Instance);
// Act + Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => session.SendRequestAsync(null, CancellationToken.None));
}
[Theory]
[MemberData(nameof(SendResponseParameterValidationData))]
public async Task StreamingSession_SendResponse_ParameterValidation(Header header, StreamingResponse response, Type exceptionType)
{
// Arrange
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var session = new StreamingSession(new Mock<RequestHandler>().Object, transportHandler.Object, NullLogger.Instance);
// Act + Assert
await Assert.ThrowsAsync(exceptionType, () => session.SendResponseAsync(header, response, CancellationToken.None));
}
[Theory]
[MemberData(nameof(ReceiveRequestParameterValidationData))]
public void StreamingSession_ReceiveRequest_ParameterValidation(Header header, ReceiveRequest request, Type exceptionType)
{
// Arrange
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var session = new StreamingSession(new Mock<RequestHandler>().Object, transportHandler.Object, NullLogger.Instance);
// Act + Assert
Assert.Throws(exceptionType, () => session.ReceiveRequest(header, request));
}
[Theory]
[MemberData(nameof(ReceiveResponseParameterValidationData))]
public void StreamingSession_ReceiveResponse_ParameterValidation(Header header, ReceiveResponse response, Type exceptionType)
{
// Arrange
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var session = new StreamingSession(new Mock<RequestHandler>().Object, transportHandler.Object, NullLogger.Instance);
// Act + Assert
Assert.Throws(exceptionType, () => session.ReceiveResponse(header, response));
}
[Theory]
[MemberData(nameof(ReceiveStreamParameterValidationData))]
public void StreamingSession_Receivestream_ParameterValidation(Header header, byte[] payload, Type exceptionType)
{
// Arrange
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var session = new StreamingSession(new Mock<RequestHandler>().Object, transportHandler.Object, NullLogger.Instance);
// Act + Assert
Assert.Throws(exceptionType, () => session.ReceiveStream(header, new ArraySegment<byte>(payload)));
}
[Theory]
[InlineData(10, 1, 1)]
[InlineData(100, 1, 1)]
[InlineData(1000, 1, 1)]
[InlineData(10000, 1, 1)]
[InlineData(1000, 2, 1)]
[InlineData(1000, 1, 2)]
[InlineData(1000, 1, 10)]
[InlineData(1000, 10, 10)]
[InlineData(1000, 100, 10)]
public async Task StreamingSession_RequestWithStreams_SentToHandler(int streamLength, int streamCount, int chunkCount)
{
// Arrange
var requestId = Guid.NewGuid();
var request = new ReceiveRequest()
{
Verb = "GET",
Path = "api/version",
Streams = new List<IContentStream>()
};
request.Streams = StreamingDataGenerator.CreateStreams(requestId, streamLength, streamCount, chunkCount);
var requestHandler = new Mock<RequestHandler>();
var requestCompletionSource = new TaskCompletionSource<bool>();
requestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 })
.Callback(() => requestCompletionSource.SetResult(true));
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var responseCompletionSource = new TaskCompletionSource<bool>();
transportHandler
.Setup(t => t.SendResponseAsync(It.IsAny<Guid>(), It.Is<ResponseModel>(r => r.StatusCode == 200), CancellationToken.None))
.Callback(() => responseCompletionSource.SetResult(true));
// Act
var session = new StreamingSession(requestHandler.Object, transportHandler.Object, NullLogger.Instance);
session.ReceiveRequest(new Header() { Id = requestId, Type = PayloadTypes.Request }, request);
foreach (AugmentedStreamDefinition definition in request.Streams)
{
var chunkList = definition.Chunks;
for (int i = 0; i < chunkList.Count; i++)
{
bool isLast = i == chunkList.Count - 1;
session.ReceiveStream(
new Header() { End = isLast, Id = definition.Id, PayloadLength = chunkList[i].Length, Type = PayloadTypes.Stream },
chunkList[i]);
}
}
var roundtripTask = Task.WhenAll(requestCompletionSource.Task, responseCompletionSource.Task);
var result = await Task.WhenAny(roundtripTask, Task.Delay(TimeSpan.FromSeconds(5)));
// Assert
Assert.Equal(result, roundtripTask);
}
[Theory]
[InlineData(10, 1, 1)]
[InlineData(100, 1, 1)]
[InlineData(1000, 1, 1)]
[InlineData(10000, 1, 1)]
[InlineData(1000, 2, 1)]
[InlineData(1000, 1, 2)]
[InlineData(1000, 1, 10)]
[InlineData(1000, 10, 10)]
[InlineData(1000, 100, 10)]
public async Task StreamingSession_SendRequest_ReceiveResponse(int streamLength, int streamCount, int chunkCount)
{
// Arrange
var request = new StreamingRequest()
{
Verb = "GET",
Path = "api/version",
Streams = new List<ResponseMessageStream>()
};
request.AddStream(new StringContent("Hello human, I'm Bender!"));
var requestHandler = new Mock<RequestHandler>();
var requestCompletionSource = new TaskCompletionSource<bool>();
requestHandler
.Setup(r => r.ProcessRequestAsync(It.IsAny<ReceiveRequest>(), null, null, CancellationToken.None))
.ReturnsAsync(() => new StreamingResponse() { StatusCode = 200 })
.Callback(() => requestCompletionSource.SetResult(true));
var transportHandler = new Mock<TransportHandler>(new Mock<IDuplexPipe>().Object, NullLogger.Instance);
var responseCompletionSource = new TaskCompletionSource<bool>();
var transportHandlerSetup = transportHandler.Setup(t => t.SendRequestAsync(It.IsAny<Guid>(), It.IsAny<RequestModel>(), CancellationToken.None));
var session = new StreamingSession(requestHandler.Object, transportHandler.Object, NullLogger.Instance);
Header responseHeader = null;
ReceiveResponse response = null;
transportHandlerSetup.Callback(
(Guid requestId, RequestModel requestPayload, CancellationToken cancellationToken) =>
{
responseHeader = new Header() { Id = requestId, Type = PayloadTypes.Response };
response = new ReceiveResponse() { StatusCode = 200, Streams = StreamingDataGenerator.CreateStreams(requestId, streamLength, streamCount, chunkCount, PayloadTypes.Response) };
session.ReceiveResponse(responseHeader, response);
foreach (AugmentedStreamDefinition definition in response.Streams)
{
var chunkList = definition.Chunks;
for (int i = 0; i < chunkCount; i++)
{
bool isLast = i == chunkCount - 1;
session.ReceiveStream(
new Header() { End = isLast, Id = definition.Id, PayloadLength = chunkList[i].Length, Type = PayloadTypes.Stream },
chunkList[i]);
}
}
});
// Act
var responseTask = session.SendRequestAsync(request, CancellationToken.None);
var responseWithTimeout = await Task.WhenAny(responseTask, Task.Delay(TimeSpan.FromSeconds(5)));
// Assert
Assert.Equal(responseTask, responseWithTimeout);
var receivedResponse = await responseTask;
Assert.Equal(response.StatusCode, receivedResponse.StatusCode);
Assert.Equal(response.Streams.Count, receivedResponse.Streams.Count);
Assert.True(response.Streams.SequenceEqual(receivedResponse.Streams));
}
internal static class StreamingDataGenerator
{
public static List<IContentStream> CreateStreams(Guid requestId, int streamLength, int streamCount = 1, int chunkCount = 1, char type = PayloadTypes.Request)
{
var result = new List<IContentStream>();
for (int i = 0; i < streamCount; i++)
{
// To keep code simple, asking that stream length can be equally divided in chunks. Feel
// free to adapt code to support it if needed.
Assert.Equal(0, streamLength % chunkCount);
var definition = new AugmentedStreamDefinition()
{
Complete = false,
Id = Guid.NewGuid(),
PayloadId = requestId,
Length = streamLength,
PayloadType = type,
Stream = new MemoryStream()
};
int chunkSize = streamLength / chunkCount;
int current = 0;
while (current < streamLength)
{
var data = new byte[chunkSize];
new Random().NextBytes(data);
definition.Chunks.Add(data);
current += chunkSize;
}
result.Add(definition);
}
return result;
}
}
private class AugmentedStreamDefinition : StreamDefinition
{
public List<byte[]> Chunks { get; set; } = new List<byte[]>();
}
//[Fact]
//public async Task StreamingSession_RequestWithNoStreams_SentToHandler()
//{
//}
//[Fact]
//public async Task StreamingSession_ResponseWithNoStreams_SentToHandler()
//{
//}
//[Fact]
//public async Task StreamingSession_ResponseWithStreams_SentToHandler()
//{
//}
//[Fact]
//public async Task StreamingSession_SendRequest_ResponseReceivedAsynchronously()
//{
//}
//[Fact]
//public async Task StreamingSession_SendResponse_Succeeds()
//{
//}
}
}

Просмотреть файл

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
namespace Microsoft.Bot.Connector.Streaming.Tests.Tools
{
internal class MemorySegment<T> : ReadOnlySequenceSegment<T>
{
public MemorySegment(ReadOnlyMemory<T> memory)
{
Memory = memory;
}
public MemorySegment<T> Append(ReadOnlyMemory<T> memory)
{
var segment = new MemorySegment<T>(memory)
{
RunningIndex = RunningIndex + Memory.Length
};
Next = segment;
return segment;
}
}
}

Просмотреть файл

@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Threading.Tasks;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
internal class SyncPoint
{
private readonly TaskCompletionSource<object> _atSyncPoint = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource<object> _continueFromSyncPoint = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
/// <summary>
/// Cretes a sync point and returns the associated handler.
/// </summary>
/// <param name="syncPoint">The created <see cref="SyncPoint"/>.</param>
/// <returns>A <see cref="Func{Task}"/> representing the sync point.</returns>
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>A <see cref="Func{Task}"/> representing the sync point next step.</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();
}
};
}
/// <summary>
/// Waits for the code-under-test to reach <see cref="WaitToContinue"/>.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public Task WaitForSyncPoint() => _atSyncPoint.Task;
/// <summary>
/// Releases the code-under-test to continue past where it waited for <see cref="WaitToContinue"/>.
/// </summary>
/// <param name="obj">The result of the sync point continuation.</param>
public void Continue(object obj = null) => _continueFromSyncPoint.TrySetResult(obj);
/// <summary>
/// Used by the code-under-test to wait for the test code to sync up.
/// </summary>
/// <remarks>
/// This code will unblock <see cref="WaitForSyncPoint"/> and then block waiting for <see cref="Continue"/> to be called.
/// </remarks>
/// <param name="obj">The underlying task result.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public Task WaitToContinue(object obj = null)
{
_atSyncPoint.TrySetResult(obj);
return _continueFromSyncPoint.Task;
}
}
}

Просмотреть файл

@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.Bot.Connector.Streaming.Tests.Tools
{
internal static class TaskExtensions
{
public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
{
// Don't create a timer if the task is already completed
// or the debugger is attached
if (task.IsCompleted || Debugger.IsAttached)
{
return await task;
}
using (var cts = new CancellationTokenSource())
{
if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)))
{
cts.Cancel();
return await task;
}
else
{
throw new TimeoutException();
}
}
}
public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
{
// Don't create a timer if the task is already completed
// or the debugger is attached
if (task.IsCompleted || Debugger.IsAttached)
{
await task;
return;
}
using (var cts = new CancellationTokenSource())
{
if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)))
{
cts.Cancel();
await task;
}
else
{
throw new TimeoutException();
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.Collections.Generic;
using Microsoft.Bot.Streaming.Payloads;
namespace Microsoft.Bot.Connector.Streaming.Tests.Features
{
internal class TestTransportObserver : IObserver<(Header Header, ReadOnlySequence<byte> Payload)>
{
public List<(Header Header, byte[] Payload)> Received { get; private set; } = new List<(Header Header, byte[] Payload)>();
public void OnCompleted()
{
throw new NotImplementedException();
}
public void OnError(Exception error)
{
throw new NotImplementedException();
}
public void OnNext((Header Header, ReadOnlySequence<byte> Payload) value)
{
Received.Add((value.Header, value.Payload.ToArray()));
}
}
}

Просмотреть файл

@ -0,0 +1,283 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.Bot.Connector.Streaming.Tests.Features
{
internal class TestWebSocketConnectionFeature : IHttpWebSocketFeature, IDisposable
{
private readonly SyncPoint _sync;
private readonly TaskCompletionSource<object> _accepted = new TaskCompletionSource<object>();
public TestWebSocketConnectionFeature()
{
}
public TestWebSocketConnectionFeature(SyncPoint sync)
{
_sync = sync;
}
public bool IsWebSocketRequest => true;
public WebSocketChannel Client { get; private set; }
public string SubProtocol { get; private set; }
public Task Accepted => _accepted.Task;
public Task<WebSocket> AcceptAsync() => AcceptAsync(new WebSocketAcceptContext());
public Task<WebSocket> AcceptAsync(WebSocketAcceptContext context)
{
var clientToServer = Channel.CreateUnbounded<WebSocketMessage>();
var serverToClient = Channel.CreateUnbounded<WebSocketMessage>();
var clientSocket = new WebSocketChannel(serverToClient.Reader, clientToServer.Writer, _sync);
var serverSocket = new WebSocketChannel(clientToServer.Reader, serverToClient.Writer, _sync);
Client = clientSocket;
SubProtocol = context.SubProtocol;
_accepted.TrySetResult(new object());
return Task.FromResult<WebSocket>(serverSocket);
}
public void Dispose()
{
}
public class WebSocketChannel : WebSocket
{
private readonly ChannelReader<WebSocketMessage> _input;
private readonly ChannelWriter<WebSocketMessage> _output;
private readonly SyncPoint _sync;
private WebSocketCloseStatus? _closeStatus;
private string _closeStatusDescription;
private WebSocketState _state;
private WebSocketMessage _internalBuffer = new WebSocketMessage();
public WebSocketChannel(ChannelReader<WebSocketMessage> input, ChannelWriter<WebSocketMessage> output, SyncPoint sync = null)
{
_input = input;
_output = output;
_sync = sync;
_state = WebSocketState.Open;
}
public override WebSocketCloseStatus? CloseStatus => _closeStatus;
public override string CloseStatusDescription => _closeStatusDescription;
public override WebSocketState State => _state;
public override string SubProtocol => null;
public override void Abort()
{
_output.TryComplete(new OperationCanceledException());
_state = WebSocketState.Aborted;
}
public void SendAbort()
{
_output.TryComplete(new WebSocketException(WebSocketError.ConnectionClosedPrematurely));
}
public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
{
await SendMessageAsync(
new WebSocketMessage
{
CloseStatus = closeStatus,
CloseStatusDescription = statusDescription,
MessageType = WebSocketMessageType.Close,
},
cancellationToken).ConfigureAwait(false);
_state = WebSocketState.CloseSent;
_output.TryComplete();
}
public override async Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
{
await SendMessageAsync(
new WebSocketMessage
{
CloseStatus = closeStatus,
CloseStatusDescription = statusDescription,
MessageType = WebSocketMessageType.Close,
},
cancellationToken).ConfigureAwait(false);
_state = WebSocketState.CloseSent;
_output.TryComplete();
}
public override void Dispose()
{
_state = WebSocketState.Closed;
_output.TryComplete();
}
public override async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
{
try
{
while (_internalBuffer.Buffer == null || _internalBuffer.Buffer.Length == 0)
{
await _input.WaitToReadAsync(cancellationToken).ConfigureAwait(false);
if (_input.TryRead(out var message))
{
if (message.MessageType == WebSocketMessageType.Close)
{
_state = WebSocketState.CloseReceived;
_closeStatus = message.CloseStatus;
_closeStatusDescription = message.CloseStatusDescription;
return new WebSocketReceiveResult(0, WebSocketMessageType.Close, true, message.CloseStatus, message.CloseStatusDescription);
}
_internalBuffer = message;
}
else
{
await Task.Delay(100).ConfigureAwait(false);
}
}
var length = _internalBuffer.Buffer.Length;
if (buffer.Count < _internalBuffer.Buffer.Length)
{
length = Math.Min(buffer.Count, _internalBuffer.Buffer.Length);
Buffer.BlockCopy(_internalBuffer.Buffer, 0, buffer.Array, buffer.Offset, length);
}
else
{
Buffer.BlockCopy(_internalBuffer.Buffer, 0, buffer.Array, buffer.Offset, length);
}
var endOfMessage = _internalBuffer.EndOfMessage;
if (length > 0)
{
// Remove the sent bytes from the remaining buffer
_internalBuffer.Buffer = _internalBuffer.Buffer.AsMemory().Slice(length).ToArray();
endOfMessage = _internalBuffer.Buffer.Length == 0 && endOfMessage;
}
return new WebSocketReceiveResult(length, _internalBuffer.MessageType, endOfMessage);
}
catch (WebSocketException ex)
{
switch (ex.WebSocketErrorCode)
{
case WebSocketError.ConnectionClosedPrematurely:
_state = WebSocketState.Aborted;
break;
}
// Complete the client side if there's an error
_output.TryComplete();
throw;
}
throw new InvalidOperationException("Unexpected close");
}
public override async Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
{
if (_sync != null)
{
await _sync.WaitToContinue().ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
var copy = new byte[buffer.Count];
Buffer.BlockCopy(buffer.Array, buffer.Offset, copy, 0, buffer.Count);
await SendMessageAsync(
new WebSocketMessage
{
Buffer = copy,
MessageType = messageType,
EndOfMessage = endOfMessage
},
cancellationToken).ConfigureAwait(false);
}
public async Task<WebSocketConnectionSummary> ExecuteAndCaptureFramesAsync()
{
var frames = new List<WebSocketMessage>();
while (await _input.WaitToReadAsync().ConfigureAwait(false))
{
while (_input.TryRead(out var message))
{
if (message.MessageType == WebSocketMessageType.Close)
{
_state = WebSocketState.CloseReceived;
_closeStatus = message.CloseStatus;
_closeStatusDescription = message.CloseStatusDescription;
return new WebSocketConnectionSummary(frames, new WebSocketReceiveResult(0, message.MessageType, message.EndOfMessage, message.CloseStatus, message.CloseStatusDescription));
}
frames.Add(message);
}
}
_state = WebSocketState.Closed;
_closeStatus = WebSocketCloseStatus.InternalServerError;
return new WebSocketConnectionSummary(frames, new WebSocketReceiveResult(0, WebSocketMessageType.Close, endOfMessage: true, closeStatus: WebSocketCloseStatus.InternalServerError, closeStatusDescription: string.Empty));
}
private async Task SendMessageAsync(WebSocketMessage webSocketMessage, CancellationToken cancellationToken)
{
while (await _output.WaitToWriteAsync(cancellationToken).ConfigureAwait(false))
{
if (_output.TryWrite(webSocketMessage))
{
break;
}
}
}
}
internal class WebSocketConnectionSummary
{
public WebSocketConnectionSummary(IList<WebSocketMessage> received, WebSocketReceiveResult closeResult)
{
Received = received;
CloseResult = closeResult;
}
public IList<WebSocketMessage> Received { get; }
public WebSocketReceiveResult CloseResult { get; }
}
internal class WebSocketMessage
{
public byte[] Buffer { get; set; }
public WebSocketMessageType MessageType { get; set; }
public bool EndOfMessage { get; set; }
public WebSocketCloseStatus? CloseStatus { get; set; }
public string CloseStatusDescription { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.Bot.Connector.Streaming.Tests.Tools
{
internal class XUnitLogger : ILogger
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly string _categoryName;
private readonly LoggerExternalScopeProvider _scopeProvider;
public XUnitLogger(ITestOutputHelper testOutputHelper, LoggerExternalScopeProvider scopeProvider, string categoryName)
{
_testOutputHelper = testOutputHelper;
_scopeProvider = scopeProvider;
_categoryName = categoryName;
}
public static ILogger CreateLogger(ITestOutputHelper testOutputHelper) => new XUnitLogger(testOutputHelper, new LoggerExternalScopeProvider(), string.Empty);
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
public IDisposable BeginScope<TState>(TState state) => _scopeProvider.Push(state);
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
var sb = new StringBuilder();
sb.Append(Enum.GetName(typeof(LogLevel), logLevel))
.Append(" [").Append(_categoryName).Append("] ")
.Append(formatter(state, exception));
if (exception != null)
{
sb.Append('\n').Append(exception);
}
// Append scopes
_scopeProvider.ForEachScope(
(scope, state) =>
{
state.Append("\n => ");
state.Append(scope);
}, sb);
Debug.WriteLine(sb.ToString());
_testOutputHelper.WriteLine(sb.ToString());
}
}
}

Просмотреть файл

@ -0,0 +1,424 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Streaming.Tests.Features;
using Microsoft.Bot.Connector.Streaming.Transport;
using Microsoft.Bot.Streaming.Payloads;
using Microsoft.Bot.Streaming.Transport;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json;
using Xunit;
using RequestModel = Microsoft.Bot.Connector.Streaming.Payloads.RequestPayload;
using ResponseModel = Microsoft.Bot.Connector.Streaming.Payloads.ResponsePayload;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
public class TransportHandlerTests
{
public static IEnumerable<object[]> PipeToObserverData =>
new List<object[]>()
{
new object[] { new List<(Header Header, byte[] Payload)>() },
new object[] { GenerateHeaderPayloadData(1, 1) },
new object[] { GenerateHeaderPayloadData(1, 1) },
new object[] { GenerateHeaderPayloadData(10, 1) },
new object[] { GenerateHeaderPayloadData(100, 1) },
new object[] { GenerateHeaderPayloadData(1000, 1) },
new object[] { GenerateHeaderPayloadData(10000, 1) },
new object[] { GenerateHeaderPayloadData(100000, 1) },
new object[] { GenerateHeaderPayloadData(100000, 2) },
new object[] { GenerateHeaderPayloadData(1000, 20) },
new object[] { GenerateHeaderPayloadData(1000, 200) },
};
public static IEnumerable<object[]> ErrorScenarioData =>
new List<object[]>()
{
new object[] { GenerateHeaderPayloadData(1000, 2), true, false, false },
new object[] { GenerateHeaderPayloadData(1000, 2), false, true, false },
new object[] { GenerateHeaderPayloadData(1000, 2), false, false, true },
};
[Theory]
[MemberData(nameof(PipeToObserverData))]
public async Task TransportHandler_ReceiveFromPipe_IsSentToObserver(
List<(Header Header, byte[] Payload)> transportData)
{
await RunTransportHandlerReceiveTestAsync(transportData, false, false, false);
}
[Theory]
[MemberData(nameof(ErrorScenarioData))]
public async Task TransportHandler_ReceiveFromPipe_ErrorScenarios(
List<(Header Header, byte[] Payload)> transportData,
bool cancelAfterFirst,
bool cancelWithoutPayload,
bool completeWithException)
{
await RunTransportHandlerReceiveTestAsync(transportData, cancelAfterFirst, cancelWithoutPayload, completeWithException);
}
[Fact]
public void TransportHandler_NullObserver_Throws()
{
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
Assert.Throws<ArgumentNullException>(() => transportHandler.Subscribe(null));
}
[Fact]
public void TransportHandler_DoubleObserverRegistration_Throws()
{
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
transportHandler.Subscribe(new TestTransportObserver());
Assert.Throws<InvalidOperationException>(() => transportHandler.Subscribe(new TestTransportObserver()));
}
[Fact]
public async Task TransportHandler_SendRequest_ThrowsOnNull()
{
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
await Assert.ThrowsAsync<ArgumentNullException>(
async () => await transportHandler.SendRequestAsync(Guid.NewGuid(), null, CancellationToken.None));
}
[Fact]
public async Task TransportHandler_SendRequest_TransportReceivesHeaderAndPayload()
{
var request = new RequestModel()
{
Verb = "GET",
Path = "api/version",
Streams = new List<StreamDescription>()
{
new StreamDescription() { ContentType = "json", Id = Guid.NewGuid().ToString(), Length = 18 },
new StreamDescription() { ContentType = "text", Id = Guid.NewGuid().ToString(), Length = 24 }
}
};
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
var transport = pipePair.Application.Input;
await transportHandler.SendRequestAsync(Guid.NewGuid(), request, CancellationToken.None);
var result = await transport.ReadAsync();
var buffer = result.Buffer;
var headerBuffer = buffer.Slice(0, Math.Min(TransportConstants.MaxHeaderLength, buffer.Length));
var header = HeaderSerializer.Deserialize(headerBuffer.ToArray(), 0, TransportConstants.MaxHeaderLength);
buffer = buffer.Slice(TransportConstants.MaxHeaderLength);
if (buffer.Length < header.PayloadLength)
{
transport.AdvanceTo(buffer.Start, buffer.End);
result = await transport.ReadAsync().ConfigureAwait(false);
Assert.False(result.IsCanceled);
buffer = result.Buffer;
}
var payload = buffer.Slice(buffer.Start, header.PayloadLength).ToArray();
var payloadJson = Encoding.UTF8.GetString(payload);
var receivedPayload = JsonConvert.DeserializeObject<RequestPayload>(payloadJson);
Assert.NotNull(receivedPayload);
Assert.Equal(request.Path, receivedPayload.Path);
Assert.Equal(request.Verb, receivedPayload.Verb);
Assert.Equal(request.Streams.Count, receivedPayload.Streams.Count);
for (int i = 0; i < request.Streams.Count; i++)
{
Assert.Equal(request.Streams[i].ContentType, receivedPayload.Streams[i].ContentType);
Assert.Equal(request.Streams[i].Id, receivedPayload.Streams[i].Id);
Assert.Equal(request.Streams[i].Length, receivedPayload.Streams[i].Length);
}
}
[Fact]
public async Task TransportHandler_SendResponse_TransportReceivesHeaderAndPayload()
{
var response = new ResponseModel()
{
StatusCode = 200,
Streams = new List<StreamDescription>()
{
new StreamDescription() { ContentType = "json", Id = Guid.NewGuid().ToString(), Length = 18 },
new StreamDescription() { ContentType = "text", Id = Guid.NewGuid().ToString(), Length = 24 }
}
};
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
var transport = pipePair.Application.Input;
await transportHandler.SendResponseAsync(Guid.NewGuid(), response, CancellationToken.None);
var result = await transport.ReadAsync();
var buffer = result.Buffer;
var headerBuffer = buffer.Slice(0, Math.Min(TransportConstants.MaxHeaderLength, buffer.Length));
var header = HeaderSerializer.Deserialize(headerBuffer.ToArray(), 0, TransportConstants.MaxHeaderLength);
buffer = buffer.Slice(TransportConstants.MaxHeaderLength);
if (buffer.Length < header.PayloadLength)
{
transport.AdvanceTo(buffer.Start, buffer.End);
result = await transport.ReadAsync().ConfigureAwait(false);
Assert.False(result.IsCanceled);
buffer = result.Buffer;
}
var payload = buffer.Slice(buffer.Start, header.PayloadLength).ToArray();
var payloadJson = Encoding.UTF8.GetString(payload);
var receivedPayload = JsonConvert.DeserializeObject<ResponsePayload>(payloadJson);
Assert.NotNull(receivedPayload);
Assert.Equal(response.StatusCode, receivedPayload.StatusCode);
Assert.Equal(response.Streams.Count, receivedPayload.Streams.Count);
for (int i = 0; i < response.Streams.Count; i++)
{
Assert.Equal(response.Streams[i].ContentType, receivedPayload.Streams[i].ContentType);
Assert.Equal(response.Streams[i].Id, receivedPayload.Streams[i].Id);
Assert.Equal(response.Streams[i].Length, receivedPayload.Streams[i].Length);
}
}
[Fact]
public async Task TransportHandler_SendStream_TransportReceivesHeaderAndPayload()
{
var text = "Hello human, I'm Bender";
// TODO: make this a theory with increasing byte count. Implement chunking in the transport handler
// to ensure once byte size increases we still send manageable packet size, and test the chunking here.
var stream = new MemoryStream(Encoding.UTF8.GetBytes(text));
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
var transport = pipePair.Application.Input;
await transportHandler.SendStreamAsync(Guid.NewGuid(), stream, CancellationToken.None);
var result = await transport.ReadAsync();
var buffer = result.Buffer;
var headerBuffer = buffer.Slice(0, Math.Min(TransportConstants.MaxHeaderLength, buffer.Length));
var header = HeaderSerializer.Deserialize(headerBuffer.ToArray(), 0, TransportConstants.MaxHeaderLength);
buffer = buffer.Slice(TransportConstants.MaxHeaderLength);
if (buffer.Length < header.PayloadLength)
{
transport.AdvanceTo(buffer.Start, buffer.End);
result = await transport.ReadAsync().ConfigureAwait(false);
Assert.False(result.IsCanceled);
buffer = result.Buffer;
}
var payload = buffer.Slice(buffer.Start, header.PayloadLength).ToArray();
var payloadString = Encoding.UTF8.GetString(payload);
Assert.Equal(text, payloadString);
}
[Fact]
public async Task TransportHandler_SendResponse_ThrowsOnNull()
{
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
await Assert.ThrowsAsync<ArgumentNullException>(
async () => await transportHandler.SendResponseAsync(Guid.NewGuid(), null, CancellationToken.None));
}
[Fact]
public async Task TransportHandler_SendStream_ThrowsOnNull()
{
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
await Assert.ThrowsAsync<ArgumentNullException>(
async () => await transportHandler.SendStreamAsync(Guid.NewGuid(), null, CancellationToken.None));
}
private static async Task RunTransportHandlerReceiveTestAsync(
List<(Header Header, byte[] Payload)> transportData,
bool cancelAfterFirst,
bool cancelWithoutPayload,
bool completeWithException)
{
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
var applicationDuplexPipe = pipePair.Application;
var transportHandler = new TransportHandler(pipePair.Transport, NullLogger.Instance);
var transportObserver = new TestTransportObserver();
transportHandler.Subscribe(transportObserver);
var transportTask = transportHandler.ListenAsync(CancellationToken.None);
var output = applicationDuplexPipe.Output;
bool first = true;
foreach (var entry in transportData)
{
var headerBuffer = new byte[48];
HeaderSerializer.Serialize(entry.Header, headerBuffer, 0);
Assert.Equal(entry.Header.PayloadLength, entry.Payload.Length);
await output.WriteAsync(headerBuffer, CancellationToken.None).ConfigureAwait(false);
if (cancelWithoutPayload)
{
output.CancelPendingFlush();
break;
}
if (entry.Header.PayloadLength > 0)
{
await output.WriteAsync(entry.Payload, CancellationToken.None).ConfigureAwait(false);
}
if (first && cancelAfterFirst)
{
output.CancelPendingFlush();
break;
}
}
if (completeWithException)
{
await Task.Delay(TimeSpan.FromSeconds(1));
await output.CompleteAsync(new Exception()).ConfigureAwait(false);
await Assert.ThrowsAsync<Exception>(async () => await transportTask);
}
else
{
await output.CompleteAsync().ConfigureAwait(false);
if (Debugger.IsAttached)
{
await transportTask;
}
else
{
var result = await Task.WhenAny(transportTask, Task.Delay(TimeSpan.FromSeconds(5))).ConfigureAwait(false);
Assert.Equal(result, transportTask);
}
}
var receivedData = transportObserver.Received;
if (cancelAfterFirst)
{
Assert.Single(receivedData);
}
else if (cancelWithoutPayload)
{
Assert.Empty(receivedData);
}
else if (!completeWithException)
{
Assert.Equal(transportData.Count, receivedData.Count);
}
if (!cancelAfterFirst && !cancelWithoutPayload && !completeWithException)
{
for (int i = 0; i < transportData.Count; i++)
{
Assert.Equal(transportData[i].Header.End, receivedData[i].Header.End);
Assert.Equal(transportData[i].Header.Id, receivedData[i].Header.Id);
Assert.Equal(transportData[i].Header.PayloadLength, receivedData[i].Header.PayloadLength);
Assert.Equal(transportData[i].Header.Type, receivedData[i].Header.Type);
Assert.True(transportData[i].Payload.SequenceEqual(receivedData[i].Payload));
}
}
}
private static List<(Header Header, byte[] Payload)> GenerateHeaderPayloadData(int totalLength, int packageCount)
{
var result = new List<(Header Header, byte[] Payload)>();
if (totalLength == 0)
{
var header = new Header()
{
Id = Guid.NewGuid(),
End = true,
PayloadLength = 0,
Type = PayloadTypes.Stream
};
result.Add((header, null));
return result;
}
byte[] buffer = new byte[totalLength];
var random = new Random();
random.NextBytes(buffer);
var chunkSize = totalLength / packageCount;
var current = 0;
while (current < totalLength)
{
var currentSize = Math.Min(chunkSize, totalLength - current);
var header = new Header()
{
Id = Guid.NewGuid(),
End = true,
PayloadLength = currentSize,
Type = PayloadTypes.Stream
};
var payload = new byte[currentSize];
result.Add((header, payload));
current += currentSize;
}
return result;
}
}
}

Просмотреть файл

@ -0,0 +1,515 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Bot.Connector.Streaming.Tests.Features;
using Microsoft.Bot.Connector.Streaming.Tests.Tools;
using Microsoft.Bot.Connector.Streaming.Transport;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
using Xunit.Abstractions;
using static Microsoft.Bot.Connector.Streaming.Tests.Features.TestWebSocketConnectionFeature;
namespace Microsoft.Bot.Connector.Streaming.Tests
{
public class WebSocketTransportTests
{
private readonly ITestOutputHelper _testOutput;
public WebSocketTransportTests(ITestOutputHelper testOutput)
{
_testOutput = testOutput;
}
[Fact]
public async Task WebSocketTransport_WhatIsReceivedIsWritten()
{
var pipePair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
using (var webSocketFeature = new TestWebSocketConnectionFeature())
{
// Build transport
var transport = new WebSocketTransport(pipePair.Application, NullLogger.Instance);
// Accept web socket, start receiving / sending at the transport level
var processTask = transport.ProcessSocketAsync(await webSocketFeature.AcceptAsync(), CancellationToken.None);
// Start a socket client that will capture traffic for posterior analysis
var clientTask = webSocketFeature.Client.ExecuteAndCaptureFramesAsync();
// Send a frame, then close
await webSocketFeature.Client.SendAsync(
buffer: new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello")),
messageType: WebSocketMessageType.Binary,
endOfMessage: true,
cancellationToken: CancellationToken.None);
await webSocketFeature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
var result = await pipePair.Transport.Input.ReadAsync();
var buffer = result.Buffer;
Assert.Equal("Hello", Encoding.UTF8.GetString(buffer.ToArray()));
pipePair.Transport.Input.AdvanceTo(buffer.End);
pipePair.Transport.Output.Complete();
// The transport should finish now
await processTask;
// The connection should close after this, which means the client will get a close frame.
var clientSummary = await clientTask;
Assert.Equal(WebSocketCloseStatus.NormalClosure, clientSummary.CloseResult.CloseStatus);
}
}
[Fact]
public async Task TransportCommunicatesErrorToApplicationWhenClientDisconnectsAbnormally()
{
//using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
using (var feature = new TestWebSocketConnectionFeature())
{
async Task CompleteApplicationAfterTransportCompletes()
{
try
{
// Wait until the transport completes so that we can end the application
var result = await pair.Transport.Input.ReadAsync();
pair.Transport.Input.AdvanceTo(result.Buffer.End);
}
catch (Exception ex)
{
Assert.IsType<WebSocketError>(ex);
}
finally
{
// Complete the application so that the connection unwinds without aborting
pair.Transport.Output.Complete();
}
}
var transport = new WebSocketTransport(pair.Application, NullLogger.Instance);
// Accept web socket, start receiving / sending at the transport level
var processTask = transport.ProcessSocketAsync(await feature.AcceptAsync(), CancellationToken.None);
// Start a socket client that will capture traffic for posterior analysis
var clientTask = feature.Client.ExecuteAndCaptureFramesAsync();
// When the close frame is received, we complete the application so the send
// loop unwinds
_ = CompleteApplicationAfterTransportCompletes();
// Terminate the client to server channel with an exception
feature.Client.SendAbort();
// Wait for the transport
await processTask.TimeoutAfter(TimeSpan.FromSeconds(5));
await clientTask.TimeoutAfter(TimeSpan.FromSeconds(5));
}
}
}
[Fact]
public async Task ClientReceivesInternalServerErrorWhenTheApplicationFails()
{
//using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
using (var feature = new TestWebSocketConnectionFeature())
{
var transport = new WebSocketTransport(pair.Application, NullLogger.Instance);
// Accept web socket, start receiving / sending at the transport level
var processTask = transport.ProcessSocketAsync(await feature.AcceptAsync(), CancellationToken.None);
// Start a socket client that will capture traffic for posterior analysis
var clientTask = feature.Client.ExecuteAndCaptureFramesAsync();
// Fail in the app
pair.Transport.Output.Complete(new InvalidOperationException("Catastrophic failure."));
var clientSummary = await clientTask.TimeoutAfter<WebSocketConnectionSummary>(TimeSpan.FromSeconds(5));
Assert.Equal(WebSocketCloseStatus.InternalServerError, clientSummary.CloseResult.CloseStatus);
// Close from the client
await feature.Client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
await processTask.TimeoutAfter(TimeSpan.FromSeconds(5));
}
}
}
[Fact]
public async Task TransportClosesOnCloseTimeoutIfClientDoesNotSendCloseFrame()
{
//using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
using (var feature = new TestWebSocketConnectionFeature())
{
var transport = new WebSocketTransport(pair.Application, NullLogger.Instance);
var serverSocket = await feature.AcceptAsync();
// Accept web socket, start receiving / sending at the transport level
var processTask = transport.ProcessSocketAsync(serverSocket, CancellationToken.None);
// Start a socket client that will capture traffic for posterior analysis
var clientTask = feature.Client.ExecuteAndCaptureFramesAsync();
// End the app
pair.Transport.Output.Complete();
await processTask.TimeoutAfter(TimeSpan.FromSeconds(10));
// Now we're closed
Assert.Equal(WebSocketState.Aborted, serverSocket.State);
serverSocket.Dispose();
}
}
}
[Fact]
public async Task TransportFailsOnTimeoutWithErrorWhenApplicationFailsAndClientDoesNotSendCloseFrame()
{
//using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
using (var feature = new TestWebSocketConnectionFeature())
{
var transport = new WebSocketTransport(pair.Application, NullLogger.Instance);
var serverSocket = await feature.AcceptAsync();
// Accept web socket, start receiving / sending at the transport level
var processTask = transport.ProcessSocketAsync(serverSocket, CancellationToken.None);
// Start a socket client that will capture traffic for posterior analysis
var clientTask = feature.Client.ExecuteAndCaptureFramesAsync();
// fail the client to server channel
pair.Transport.Output.Complete(new Exception());
await processTask.TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Equal(WebSocketState.Aborted, serverSocket.State);
}
}
}
[Fact]
public async Task ServerGracefullyClosesWhenApplicationEndsThenClientSendsCloseFrame()
{
//using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
using (var feature = new TestWebSocketConnectionFeature())
{
var transport = new WebSocketTransport(pair.Application, NullLogger.Instance);
var serverSocket = await feature.AcceptAsync();
// Accept web socket, start receiving / sending at the transport level
var processTask = transport.ProcessSocketAsync(serverSocket, CancellationToken.None);
// Start a socket client that will capture traffic for posterior analysis
var clientTask = feature.Client.ExecuteAndCaptureFramesAsync();
// close the client to server channel
pair.Transport.Output.Complete();
_ = await clientTask.TimeoutAfter(TimeSpan.FromSeconds(5));
await feature.Client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None).TimeoutAfter(TimeSpan.FromSeconds(5));
await processTask.TimeoutAfter(TimeSpan.FromSeconds(5));
Assert.Equal(WebSocketCloseStatus.NormalClosure, serverSocket.CloseStatus);
}
}
}
[Fact]
public async Task ServerGracefullyClosesWhenClientSendsCloseFrameThenApplicationEnds()
{
//using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
using (var feature = new TestWebSocketConnectionFeature())
{
var transport = new WebSocketTransport(pair.Application, NullLogger.Instance);
var serverSocket = await feature.AcceptAsync();
// Accept web socket, start receiving / sending at the transport level
var processTask = transport.ProcessSocketAsync(serverSocket, CancellationToken.None);
// Start a socket client that will capture traffic for posterior analysis
var clientTask = feature.Client.ExecuteAndCaptureFramesAsync();
await feature.Client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None).TimeoutAfter(TimeSpan.FromSeconds(5));
// close the client to server channel
pair.Transport.Output.Complete();
_ = await clientTask.TimeoutAfter(TimeSpan.FromSeconds(5));
await processTask.TimeoutAfter(TimeSpan.FromSeconds(5));
Assert.Equal(WebSocketCloseStatus.NormalClosure, serverSocket.CloseStatus);
}
}
}
[Fact]
public async Task MultiSegmentSendWillNotSendEmptyEndOfMessageFrame()
{
using (var feature = new TestWebSocketConnectionFeature())
{
var serverSocket = await feature.AcceptAsync();
var firstSegment = new byte[] { 1 };
var secondSegment = new byte[] { 15 };
var first = new MemorySegment<byte>(firstSegment);
var last = first.Append(secondSegment);
var sequence = new ReadOnlySequence<byte>(first, 0, last, last.Memory.Length);
Assert.False(sequence.IsSingleSegment);
await serverSocket.SendAsync(sequence, WebSocketMessageType.Text);
// Run the client socket
var client = feature.Client.ExecuteAndCaptureFramesAsync();
await serverSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, default);
var messages = await client.TimeoutAfter(TimeSpan.FromSeconds(5));
Assert.Equal(2, messages.Received.Count);
// First message: 1 byte, endOfMessage false
Assert.Single(messages.Received[0].Buffer);
Assert.Equal(1, messages.Received[0].Buffer[0]);
Assert.False(messages.Received[0].EndOfMessage);
// Second message: 1 byte, endOfMessage true
Assert.Single(messages.Received[1].Buffer);
Assert.Equal(15, messages.Received[1].Buffer[0]);
Assert.True(messages.Received[1].EndOfMessage);
}
}
[Fact]
public async Task ServerTransportCanReceiveMessages()
{
var logger = XUnitLogger.CreateLogger(_testOutput);
using (var connection = new TestWebSocketConnectionFeature())
{
var server = connection.AcceptAsync();
var client = connection.Client;
var fromTransport = new Pipe(PipeOptions.Default);
var toTransport = new Pipe(PipeOptions.Default);
var listenerRunning = ListenAsync(fromTransport.Reader);
var webSocketManager = new Mock<WebSocketManager>();
webSocketManager.Setup(m => m.AcceptWebSocketAsync()).Returns(server);
var httpContext = new Mock<HttpContext>();
httpContext.Setup(c => c.WebSockets).Returns(webSocketManager.Object);
var sut = new WebSocketTransport(new DuplexPipe(toTransport.Reader, fromTransport.Writer), logger);
var serverTransportRunning = sut.ConnectAsync(httpContext.Object, CancellationToken.None);
var messages = new List<byte[]> { Encoding.UTF8.GetBytes("foo"), Encoding.UTF8.GetBytes("bar") };
SendBinaryAsync(client, messages).Wait();
client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Done sending.", CancellationToken.None).Wait();
serverTransportRunning.Wait();
var output = await listenerRunning;
Assert.Equal("foo", Encoding.UTF8.GetString(output[0]));
Assert.Equal("bar", Encoding.UTF8.GetString(output[1]));
}
}
[Fact]
public void ServerTransportCanSendMessages()
{
var logger = XUnitLogger.CreateLogger(_testOutput);
using (var connection = new TestWebSocketConnectionFeature())
{
var server = connection.AcceptAsync().GetAwaiter().GetResult();
var client = connection.Client;
var receiverRunning = ReceiveAsync(client);
var fromTransport = new Pipe(PipeOptions.Default);
var toTransport = new Pipe(PipeOptions.Default);
var webSocketManager = new Mock<WebSocketManager>();
webSocketManager.Setup(m => m.AcceptWebSocketAsync()).Returns(Task.FromResult(server));
var httpContext = new Mock<HttpContext>();
httpContext.Setup(c => c.WebSockets).Returns(webSocketManager.Object);
var sut = new WebSocketTransport(new DuplexPipe(toTransport.Reader, fromTransport.Writer), logger);
var serverTransportRunning = sut.ConnectAsync(httpContext.Object, CancellationToken.None);
var messages = new List<byte[]> { Encoding.UTF8.GetBytes("foo") };
WriteAsync(toTransport.Writer, messages).Wait();
toTransport.Writer.CompleteAsync().GetAwaiter().GetResult();
serverTransportRunning.Wait();
var output = receiverRunning.GetAwaiter().GetResult();
Assert.Equal("foo", Encoding.UTF8.GetString(output[0]));
}
}
[Fact]
public void ClientTransportCanReceiveMessages()
{
var logger = XUnitLogger.CreateLogger(_testOutput);
using (var connection = new TestWebSocketConnectionFeature())
{
var server = connection.AcceptAsync().GetAwaiter().GetResult();
var client = connection.Client;
var fromTransport = new Pipe(PipeOptions.Default);
var toTransport = new Pipe(PipeOptions.Default);
var listenerRunning = ListenAsync(fromTransport.Reader);
var sut = new WebSocketTransport(new DuplexPipe(toTransport.Reader, fromTransport.Writer), logger);
var clientTransportRunning = sut.ProcessSocketAsync(client, CancellationToken.None);
var messages = new List<byte[]> { Encoding.UTF8.GetBytes("foo") };
SendBinaryAsync(server, messages).Wait();
server.CloseAsync(WebSocketCloseStatus.NormalClosure, "Done sending.", CancellationToken.None).Wait();
clientTransportRunning.Wait();
var output = listenerRunning.GetAwaiter().GetResult();
Assert.Equal("foo", Encoding.UTF8.GetString(output[0]));
}
}
[Fact]
public void ClientTransportCanSendMessages()
{
var logger = XUnitLogger.CreateLogger(_testOutput);
using (var connection = new TestWebSocketConnectionFeature())
{
var server = connection.AcceptAsync().GetAwaiter().GetResult();
var client = connection.Client;
var receiverRunning = ReceiveAsync(server);
var fromTransport = new Pipe(PipeOptions.Default);
var toTransport = new Pipe(PipeOptions.Default);
var sut = new WebSocketTransport(new DuplexPipe(toTransport.Reader, fromTransport.Writer), logger);
var clientTransportRunning = sut.ProcessSocketAsync(client, CancellationToken.None);
var messages = new List<byte[]> { Encoding.UTF8.GetBytes("foo") };
WriteAsync(toTransport.Writer, messages).Wait();
toTransport.Writer.CompleteAsync().GetAwaiter().GetResult();
clientTransportRunning.Wait();
var output = receiverRunning.GetAwaiter().GetResult();
Assert.Equal("foo", Encoding.UTF8.GetString(output[0]));
}
}
private static async Task<List<byte[]>> ListenAsync(PipeReader input)
{
var messages = new List<byte[]>();
const int messageLength = 3;
while (true)
{
var result = await input.ReadAsync();
var buffer = result.Buffer;
while (!buffer.IsEmpty)
{
var payload = buffer.Slice(0, messageLength);
messages.Add(payload.ToArray());
buffer = buffer.Slice(messageLength);
}
input.AdvanceTo(buffer.Start, buffer.End);
if (result.IsCompleted)
{
break;
}
}
await input.CompleteAsync();
return messages;
}
private static async Task SendBinaryAsync(WebSocket socket, List<byte[]> messages)
{
foreach (var message in messages)
{
var buffer = new ArraySegment<byte>(message);
await socket.SendAsync(buffer, WebSocketMessageType.Binary, endOfMessage: true, CancellationToken.None);
}
}
private static async Task<List<byte[]>> ReceiveAsync(WebSocket socket)
{
var messages = new List<byte[]>();
while (true)
{
var buffer = new byte[1024 * 4];
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
break;
}
messages.Add(buffer.Take(result.Count).ToArray());
}
return messages;
}
private static async Task WriteAsync(PipeWriter output, List<byte[]> messages)
{
foreach (var message in messages)
{
var buffer = new ReadOnlyMemory<byte>(message);
await output.WriteAsync(buffer);
}
}
}
}

Просмотреть файл

@ -129,7 +129,7 @@ namespace Microsoft.Bot.Streaming.UnitTests
Assert.Equal(StreamingRequest.GET, r.Verb);
Assert.Null(r.Path);
Assert.Null(r.Streams);
Assert.Empty(r.Streams);
}
[Fact]
@ -139,7 +139,7 @@ namespace Microsoft.Bot.Streaming.UnitTests
Assert.Equal(StreamingRequest.POST, r.Verb);
Assert.Null(r.Path);
Assert.Null(r.Streams);
Assert.Empty(r.Streams);
}
[Fact]
@ -149,7 +149,7 @@ namespace Microsoft.Bot.Streaming.UnitTests
Assert.Equal(StreamingRequest.DELETE, r.Verb);
Assert.Null(r.Path);
Assert.Null(r.Streams);
Assert.Empty(r.Streams);
}
[Fact]
@ -159,7 +159,7 @@ namespace Microsoft.Bot.Streaming.UnitTests
Assert.Equal(StreamingRequest.PUT, r.Verb);
Assert.Null(r.Path);
Assert.Null(r.Streams);
Assert.Empty(r.Streams);
}
[Fact]

Просмотреть файл

@ -17,8 +17,14 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Connector.Streaming.Application;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Streaming;
using Microsoft.Bot.Streaming.Payloads;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Primitives;
using Microsoft.Rest;
using Microsoft.Rest.Serialization;
using Moq;
@ -186,6 +192,143 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core.Tests
botFrameworkAuthenticationMock.Verify(x => x.AuthenticateStreamingRequestAsync(It.Is<string>(v => true), It.Is<string>(v => true), It.Is<CancellationToken>(ct => true)), Times.Once());
}
[Fact]
public void CanContinueConversationOverWebSocket()
{
// Arrange
var continueConversationWaiter = new AutoResetEvent(false);
var verifiedValidContinuation = false;
var appId = "testAppId";
var tenantId = "testTenantId";
var token = "Bearer testjwt";
var channelId = "testChannel";
var audience = "testAudience";
var callerId = "testCallerId";
var authResult = new AuthenticateRequestResult
{
Audience = audience,
CallerId = callerId,
ClaimsIdentity = new ClaimsIdentity(new List<Claim>
{
new Claim("aud", audience),
new Claim("iss", $"https://login.microsoftonline.com/{tenantId}/"),
new Claim("azp", appId),
new Claim("tid", tenantId),
new Claim("ver", "2.0")
})
};
var userTokenClient = new TestUserTokenClient(appId);
var validActivity = new Activity
{
Id = Guid.NewGuid().ToString("N"),
Type = ActivityTypes.Message,
From = new ChannelAccount { Id = "testUser" },
Conversation = new ConversationAccount { Id = Guid.NewGuid().ToString("N") },
Recipient = new ChannelAccount { Id = "testBot" },
ServiceUrl = "wss://InvalidServiceUrl/api/messages",
ChannelId = channelId,
Text = "hi",
};
var validContent = new StringContent(JsonConvert.SerializeObject(validActivity), Encoding.UTF8, "application/json");
var invalidActivity = new Activity
{
Id = Guid.NewGuid().ToString("N"),
Type = ActivityTypes.Message,
From = new ChannelAccount { Id = "testUser" },
Conversation = new ConversationAccount { Id = Guid.NewGuid().ToString("N") },
Recipient = new ChannelAccount { Id = "testBot" },
ServiceUrl = "wss://InvalidServiceUrl/api/messages",
ChannelId = channelId,
Text = "hi",
};
var streamingConnection = new Mock<StreamingConnection>();
streamingConnection
.Setup(c => c.ListenAsync(It.IsAny<RequestHandler>(), It.IsAny<CancellationToken>()))
.Returns<RequestHandler, CancellationToken>((handler, cancellationToken) => handler.ProcessRequestAsync(
new ReceiveRequest
{
Verb = "POST",
Path = "/api/messages",
Streams = new List<IContentStream>
{
new TestContentStream
{
Id = Guid.NewGuid(),
ContentType = "application/json",
Length = (int?)validContent.Headers.ContentLength,
Stream = validContent.ReadAsStreamAsync().GetAwaiter().GetResult()
}
}
},
null,
cancellationToken: cancellationToken));
var auth = new Mock<BotFrameworkAuthentication>();
auth.Setup(a => a.AuthenticateStreamingRequestAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(authResult));
auth.Setup(a => a.CreateUserTokenClientAsync(It.IsAny<ClaimsIdentity>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<UserTokenClient>(userTokenClient));
var webSocketManager = new Mock<WebSocketManager>();
webSocketManager.Setup(m => m.IsWebSocketRequest).Returns(true);
var httpContext = new Mock<HttpContext>();
httpContext.Setup(c => c.WebSockets).Returns(webSocketManager.Object);
var httpRequest = new Mock<HttpRequest>();
httpRequest.Setup(r => r.Method).Returns("GET");
httpRequest.Setup(r => r.HttpContext).Returns(httpContext.Object);
httpRequest.Setup(r => r.Headers).Returns(new HeaderDictionary
{
{ "Authorization", new StringValues(token) },
{ "channelid", new StringValues(channelId) }
});
var httpResponse = new Mock<HttpResponse>();
var bot = new Mock<IBot>();
bot.Setup(b => b.OnTurnAsync(It.IsAny<ITurnContext>(), It.IsAny<CancellationToken>()))
.Returns(Task.Factory.StartNew(() => { continueConversationWaiter.WaitOne(); })); // Simulate listening on web socket
// Act
var adapter = new StreamingTestCloudAdapter(auth.Object, streamingConnection.Object);
var processRequest = adapter.ProcessAsync(httpRequest.Object, httpResponse.Object, bot.Object, CancellationToken.None);
var validContinuation = adapter.ContinueConversationAsync(
authResult.ClaimsIdentity,
validActivity,
(turn, cancellationToken) =>
{
var connectorFactory = turn.TurnState.Get<ConnectorFactory>();
Assert.NotNull(connectorFactory);
var connectorFactoryTypeName = connectorFactory.GetType().FullName ?? string.Empty;
Assert.EndsWith("StreamingConnectorFactory", connectorFactoryTypeName);
verifiedValidContinuation = true;
return Task.CompletedTask;
},
CancellationToken.None);
var invalidContinuation = adapter.ContinueConversationAsync(
authResult.ClaimsIdentity, invalidActivity, (turn, cancellationToken) => Task.CompletedTask, CancellationToken.None);
continueConversationWaiter.Set();
processRequest.Wait();
// Assert
Assert.True(processRequest.IsCompletedSuccessfully);
Assert.True(verifiedValidContinuation);
Assert.True(validContinuation.IsCompletedSuccessfully);
Assert.Null(validContinuation.Exception);
Assert.True(invalidContinuation.IsFaulted);
Assert.NotEmpty(invalidContinuation.Exception.InnerExceptions);
Assert.True(invalidContinuation.Exception.InnerExceptions[0] is ApplicationException);
}
[Fact]
public async Task MessageActivityWithHttpClient()
{
@ -831,5 +974,32 @@ namespace Microsoft.Bot.Builder.Integration.AspNet.Core.Tests
return Task.FromResult((IConnectorClient)new ConnectorClient(new Uri(serviceUrl), credentials, null, disposeHttpClient: true));
}
}
private class TestContentStream : IContentStream
{
public Guid Id { get; set; }
public string ContentType { get; set; }
public int? Length { get; set; }
public Stream Stream { get; set; }
}
private class StreamingTestCloudAdapter : CloudAdapter
{
private readonly StreamingConnection _connection;
public StreamingTestCloudAdapter(BotFrameworkAuthentication auth, StreamingConnection connection)
: base(auth)
{
_connection = connection;
}
protected override StreamingConnection CreateWebSocketConnection(HttpContext httpContext, ILogger logger)
{
return _connection;
}
}
}
}

Просмотреть файл

@ -8,7 +8,8 @@
"triggers",
"generator",
"selector",
"schema"
"schema",
"dialogs"
],
"label": "Adaptive dialog",
"order": [