1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-21 22:05:43 -04:00

imported signalr 1.1.3 into NzbDrone.

This commit is contained in:
kayone
2013-11-21 21:26:57 -08:00
parent 891443e05d
commit 0e623e7ce4
236 changed files with 20490 additions and 35 deletions
@@ -0,0 +1,189 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Hosting;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Transports
{
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Disposable fields are disposed from a different method")]
public class ForeverFrameTransport : ForeverTransport
{
private const string _initPrefix = "<!DOCTYPE html>" +
"<html>" +
"<head>" +
"<title>SignalR Forever Frame Transport Stream</title>\r\n" +
"<script>\r\n" + //" debugger;\r\n"+
" var $ = window.parent.jQuery,\r\n" +
" ff = $ ? $.signalR.transports.foreverFrame : null,\r\n" +
" c = ff ? ff.getConnection('";
private const string _initSuffix = "') : null,\r\n" +
" r = ff ? ff.receive : function() {};\r\n" +
" ff ? ff.started(c) : '';" +
"</script></head>" +
"<body>\r\n";
private HTMLTextWriter _htmlOutputWriter;
public ForeverFrameTransport(HostContext context, IDependencyResolver resolver)
: base(context, resolver)
{
}
/// <summary>
/// Pointed to the HTMLOutputWriter to wrap output stream with an HTML friendly one
/// </summary>
public override TextWriter OutputWriter
{
get
{
return HTMLOutputWriter;
}
}
private HTMLTextWriter HTMLOutputWriter
{
get
{
if (_htmlOutputWriter == null)
{
_htmlOutputWriter = new HTMLTextWriter(Context.Response);
_htmlOutputWriter.NewLine = "\n";
}
return _htmlOutputWriter;
}
}
public override Task KeepAlive()
{
if (InitializeTcs == null || !InitializeTcs.Task.IsCompleted)
{
return TaskAsyncHelper.Empty;
}
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return EnqueueOperation(state => PerformKeepAlive(state), this);
}
public override Task Send(PersistentResponse response)
{
OnSendingResponse(response);
var context = new ForeverFrameTransportContext(this, response);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return EnqueueOperation(s => PerformSend(s), context);
}
protected internal override Task InitializeResponse(ITransportConnection connection)
{
uint frameId;
string rawFrameId = Context.Request.QueryString["frameId"];
if (String.IsNullOrWhiteSpace(rawFrameId) || !UInt32.TryParse(rawFrameId, NumberStyles.None, CultureInfo.InvariantCulture, out frameId))
{
// Invalid frameId passed in
throw new InvalidOperationException(Resources.Error_InvalidForeverFrameId);
}
string initScript = _initPrefix +
frameId.ToString(CultureInfo.InvariantCulture) +
_initSuffix;
var context = new ForeverFrameTransportContext(this, initScript);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return base.InitializeResponse(connection).Then(s => Initialize(s), context);
}
private static Task Initialize(object state)
{
var context = (ForeverFrameTransportContext)state;
var initContext = new ForeverFrameTransportContext(context.Transport, context.State);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return WriteInit(initContext);
}
private static Task WriteInit(ForeverFrameTransportContext context)
{
context.Transport.Context.Response.ContentType = "text/html; charset=UTF-8";
context.Transport.HTMLOutputWriter.WriteRaw((string)context.State);
context.Transport.HTMLOutputWriter.Flush();
return context.Transport.Context.Response.Flush();
}
private static Task PerformSend(object state)
{
var context = (ForeverFrameTransportContext)state;
context.Transport.HTMLOutputWriter.WriteRaw("<script>r(c, ");
context.Transport.JsonSerializer.Serialize(context.State, context.Transport.HTMLOutputWriter);
context.Transport.HTMLOutputWriter.WriteRaw(");</script>\r\n");
context.Transport.HTMLOutputWriter.Flush();
return context.Transport.Context.Response.Flush();
}
private static Task PerformKeepAlive(object state)
{
var transport = (ForeverFrameTransport)state;
transport.HTMLOutputWriter.WriteRaw("<script>r(c, {});</script>");
transport.HTMLOutputWriter.WriteLine();
transport.HTMLOutputWriter.WriteLine();
transport.HTMLOutputWriter.Flush();
return transport.Context.Response.Flush();
}
private class ForeverFrameTransportContext
{
public ForeverFrameTransport Transport;
public object State;
public ForeverFrameTransportContext(ForeverFrameTransport transport, object state)
{
Transport = transport;
State = state;
}
}
private class HTMLTextWriter : BufferTextWriter
{
public HTMLTextWriter(IResponse response)
: base(response)
{
}
public void WriteRaw(string value)
{
base.Write(value);
}
public override void Write(string value)
{
base.Write(JavascriptEncode(value));
}
public override void WriteLine(string value)
{
base.WriteLine(JavascriptEncode(value));
}
private static string JavascriptEncode(string input)
{
return input.Replace("<", "\\u003c").Replace(">", "\\u003e");
}
}
}
}
@@ -0,0 +1,405 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Hosting;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Json;
using Microsoft.AspNet.SignalR.Tracing;
namespace Microsoft.AspNet.SignalR.Transports
{
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The disposer is an optimization")]
public abstract class ForeverTransport : TransportDisconnectBase, ITransport
{
private readonly IPerformanceCounterManager _counters;
private IJsonSerializer _jsonSerializer;
private string _lastMessageId;
private const int MaxMessages = 10;
protected ForeverTransport(HostContext context, IDependencyResolver resolver)
: this(context,
resolver.Resolve<IJsonSerializer>(),
resolver.Resolve<ITransportHeartbeat>(),
resolver.Resolve<IPerformanceCounterManager>(),
resolver.Resolve<ITraceManager>())
{
}
protected ForeverTransport(HostContext context,
IJsonSerializer jsonSerializer,
ITransportHeartbeat heartbeat,
IPerformanceCounterManager performanceCounterWriter,
ITraceManager traceManager)
: base(context, heartbeat, performanceCounterWriter, traceManager)
{
_jsonSerializer = jsonSerializer;
_counters = performanceCounterWriter;
}
protected string LastMessageId
{
get
{
if (_lastMessageId == null)
{
_lastMessageId = Context.Request.QueryString["messageId"];
}
return _lastMessageId;
}
}
protected IJsonSerializer JsonSerializer
{
get { return _jsonSerializer; }
}
internal TaskCompletionSource<object> InitializeTcs { get; set; }
protected virtual void OnSending(string payload)
{
Heartbeat.MarkConnection(this);
}
protected virtual void OnSendingResponse(PersistentResponse response)
{
Heartbeat.MarkConnection(this);
}
public Func<string, Task> Received { get; set; }
public Func<Task> TransportConnected { get; set; }
public Func<Task> Connected { get; set; }
public Func<Task> Reconnected { get; set; }
// Unit testing hooks
internal Action AfterReceive;
internal Action BeforeCancellationTokenCallbackRegistered;
internal Action BeforeReceive;
internal Action<Exception> AfterRequestEnd;
protected override void InitializePersistentState()
{
// PersistentConnection.OnConnected must complete before we can write to the output stream,
// so clients don't indicate the connection has started too early.
InitializeTcs = new TaskCompletionSource<object>();
WriteQueue = new TaskQueue(InitializeTcs.Task);
base.InitializePersistentState();
}
protected Task ProcessRequestCore(ITransportConnection connection)
{
Connection = connection;
if (Context.Request.Url.LocalPath.EndsWith("/send", StringComparison.OrdinalIgnoreCase))
{
return ProcessSendRequest();
}
else if (IsAbortRequest)
{
return Connection.Abort(ConnectionId);
}
else
{
InitializePersistentState();
return ProcessReceiveRequest(connection);
}
}
public virtual Task ProcessRequest(ITransportConnection connection)
{
return ProcessRequestCore(connection);
}
public abstract Task Send(PersistentResponse response);
public virtual Task Send(object value)
{
var context = new ForeverTransportContext(this, value);
return EnqueueOperation(state => PerformSend(state), context);
}
protected internal virtual Task InitializeResponse(ITransportConnection connection)
{
return TaskAsyncHelper.Empty;
}
protected internal override Task EnqueueOperation(Func<object, Task> writeAsync, object state)
{
Task task = base.EnqueueOperation(writeAsync, state);
// If PersistentConnection.OnConnected has not completed (as indicated by InitializeTcs),
// the queue will be blocked to prevent clients from prematurely indicating the connection has
// started, but we must keep receive loop running to continue processing commands and to
// prevent deadlocks caused by waiting on ACKs.
if (InitializeTcs == null || InitializeTcs.Task.IsCompleted)
{
return task;
}
return TaskAsyncHelper.Empty;
}
private Task ProcessSendRequest()
{
string data = Context.Request.Form["data"];
if (Received != null)
{
return Received(data);
}
return TaskAsyncHelper.Empty;
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flowed to the caller.")]
private Task ProcessReceiveRequest(ITransportConnection connection)
{
Func<Task> initialize = null;
bool newConnection = Heartbeat.AddConnection(this);
if (IsConnectRequest)
{
if (newConnection)
{
initialize = Connected;
_counters.ConnectionsConnected.Increment();
}
}
else
{
initialize = Reconnected;
}
var series = new Func<object, Task>[]
{
state => ((Func<Task>)state).Invoke(),
state => ((Func<Task>)state).Invoke()
};
var states = new object[] { TransportConnected ?? _emptyTaskFunc,
initialize ?? _emptyTaskFunc };
Func<Task> fullInit = () => TaskAsyncHelper.Series(series, states);
return ProcessMessages(connection, fullInit);
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The object is disposed otherwise")]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are flowed to the caller.")]
private Task ProcessMessages(ITransportConnection connection, Func<Task> initialize)
{
var disposer = new Disposer();
if (BeforeCancellationTokenCallbackRegistered != null)
{
BeforeCancellationTokenCallbackRegistered();
}
var cancelContext = new ForeverTransportContext(this, disposer);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
IDisposable registration = ConnectionEndToken.SafeRegister(state => Cancel(state), cancelContext);
var lifetime = new RequestLifetime(this, _requestLifeTime);
var messageContext = new MessageContext(this, lifetime, registration);
if (BeforeReceive != null)
{
BeforeReceive();
}
try
{
// Ensure we enqueue the response initialization before any messages are received
EnqueueOperation(state => InitializeResponse((ITransportConnection)state), connection)
.Catch((ex, state) => OnError(ex, state), messageContext);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
IDisposable subscription = connection.Receive(LastMessageId,
(response, state) => OnMessageReceived(response, state),
MaxMessages,
messageContext);
disposer.Set(subscription);
if (AfterReceive != null)
{
AfterReceive();
}
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
initialize().Then(tcs => tcs.TrySetResult(null), InitializeTcs)
.Catch((ex, state) => OnError(ex, state), messageContext);
}
catch (OperationCanceledException ex)
{
InitializeTcs.TrySetCanceled();
lifetime.Complete(ex);
}
catch (Exception ex)
{
InitializeTcs.TrySetCanceled();
lifetime.Complete(ex);
}
return _requestLifeTime.Task;
}
private static void Cancel(object state)
{
var context = (ForeverTransportContext)state;
context.Transport.Trace.TraceEvent(TraceEventType.Verbose, 0, "Cancel(" + context.Transport.ConnectionId + ")");
((IDisposable)context.State).Dispose();
}
private static Task<bool> OnMessageReceived(PersistentResponse response, object state)
{
var context = (MessageContext)state;
response.TimedOut = context.Transport.IsTimedOut;
// If we're telling the client to disconnect then clean up the instantiated connection.
if (response.Disconnect)
{
// Send the response before removing any connection data
return context.Transport.Send(response).Then(c => OnDisconnectMessage(c), context)
.Then(() => TaskAsyncHelper.False);
}
else if (response.TimedOut || response.Aborted)
{
context.Registration.Dispose();
if (response.Aborted)
{
// If this was a clean disconnect raise the event.
return context.Transport.Abort()
.Then(() => TaskAsyncHelper.False);
}
}
if (response.Terminal)
{
// End the request on the terminal response
context.Lifetime.Complete();
return TaskAsyncHelper.False;
}
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return context.Transport.Send(response)
.Then(() => TaskAsyncHelper.True);
}
private static void OnDisconnectMessage(MessageContext context)
{
context.Transport.ApplyState(TransportConnectionStates.DisconnectMessageReceived);
context.Registration.Dispose();
// Remove connection without triggering disconnect
context.Transport.Heartbeat.RemoveConnection(context.Transport);
}
private static Task PerformSend(object state)
{
var context = (ForeverTransportContext)state;
if (!context.Transport.IsAlive)
{
return TaskAsyncHelper.Empty;
}
context.Transport.Context.Response.ContentType = JsonUtility.JsonMimeType;
context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter);
context.Transport.OutputWriter.Flush();
return context.Transport.Context.Response.End();
}
private static void OnError(AggregateException ex, object state)
{
var context = (MessageContext)state;
context.Transport.IncrementErrors();
// Cancel any pending writes in the queue
context.Transport.InitializeTcs.TrySetCanceled();
// Complete the http request
context.Lifetime.Complete(ex);
}
private class ForeverTransportContext
{
public object State;
public ForeverTransport Transport;
public ForeverTransportContext(ForeverTransport foreverTransport, object state)
{
State = state;
Transport = foreverTransport;
}
}
private class MessageContext
{
public ForeverTransport Transport;
public RequestLifetime Lifetime;
public IDisposable Registration;
public MessageContext(ForeverTransport transport, RequestLifetime lifetime, IDisposable registration)
{
Registration = registration;
Lifetime = lifetime;
Transport = transport;
}
}
private class RequestLifetime
{
private readonly HttpRequestLifeTime _lifetime;
private readonly ForeverTransport _transport;
public RequestLifetime(ForeverTransport transport, HttpRequestLifeTime lifetime)
{
_lifetime = lifetime;
_transport = transport;
}
public void Complete()
{
Complete(error: null);
}
public void Complete(Exception error)
{
_lifetime.Complete(error);
_transport.Dispose();
if (_transport.AfterRequestEnd != null)
{
_transport.AfterRequestEnd(error);
}
}
}
}
}
@@ -0,0 +1,94 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Transports
{
internal class HttpRequestLifeTime
{
private readonly TaskCompletionSource<object> _lifetimeTcs = new TaskCompletionSource<object>();
private readonly TransportDisconnectBase _transport;
private readonly TaskQueue _writeQueue;
private readonly TraceSource _trace;
private readonly string _connectionId;
public HttpRequestLifeTime(TransportDisconnectBase transport, TaskQueue writeQueue, TraceSource trace, string connectionId)
{
_transport = transport;
_trace = trace;
_connectionId = connectionId;
_writeQueue = writeQueue;
}
public Task Task
{
get
{
return _lifetimeTcs.Task;
}
}
public void Complete()
{
Complete(error: null);
}
public void Complete(Exception error)
{
_trace.TraceEvent(TraceEventType.Verbose, 0, "DrainWrites(" + _connectionId + ")");
var context = new LifetimeContext(_transport, _lifetimeTcs, error);
_transport.ApplyState(TransportConnectionStates.QueueDrained);
// Drain the task queue for pending write operations so we don't end the request and then try to write
// to a corrupted request object.
_writeQueue.Drain().Catch().Finally(state =>
{
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
((LifetimeContext)state).Complete();
},
context);
if (error != null)
{
_trace.TraceEvent(TraceEventType.Error, 0, "CompleteRequest (" + _connectionId + ") failed: " + error.GetBaseException());
}
else
{
_trace.TraceInformation("CompleteRequest (" + _connectionId + ")");
}
}
private class LifetimeContext
{
private readonly TaskCompletionSource<object> _lifetimeTcs;
private readonly Exception _error;
private readonly TransportDisconnectBase _transport;
public LifetimeContext(TransportDisconnectBase transport, TaskCompletionSource<object> lifeTimetcs, Exception error)
{
_transport = transport;
_lifetimeTcs = lifeTimetcs;
_error = error;
}
public void Complete()
{
_transport.ApplyState(TransportConnectionStates.HttpRequestEnded);
if (_error != null)
{
_lifetimeTcs.TrySetUnwrappedException(_error);
}
else
{
_lifetimeTcs.TrySetResult(null);
}
}
}
}
}
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNet.SignalR.Transports
{
/// <summary>
/// Represents a connection that can be tracked by an <see cref="ITransportHeartbeat"/>.
/// </summary>
public interface ITrackingConnection : IDisposable
{
/// <summary>
/// Gets the id of the connection.
/// </summary>
string ConnectionId { get; }
/// <summary>
/// Gets a cancellation token that represents the connection's lifetime.
/// </summary>
CancellationToken CancellationToken { get; }
/// <summary>
/// Gets a value that represents if the connection is alive.
/// </summary>
bool IsAlive { get; }
/// <summary>
/// Gets a value that represents if the connection is timed out.
/// </summary>
bool IsTimedOut { get; }
/// <summary>
/// Gets a value that represents if the connection supprots keep alive.
/// </summary>
bool SupportsKeepAlive { get; }
/// <summary>
/// Gets a value indicating the amount of time to wait after the connection dies before firing the disconnecting the connection.
/// </summary>
TimeSpan DisconnectThreshold { get; }
/// <summary>
/// Gets the uri of the connection.
/// </summary>
Uri Url { get; }
/// <summary>
/// Applies a new state to the connection.
/// </summary>
void ApplyState(TransportConnectionStates states);
/// <summary>
/// Causes the connection to disconnect.
/// </summary>
Task Disconnect();
/// <summary>
/// Causes the connection to timeout.
/// </summary>
void Timeout();
/// <summary>
/// Sends a keep alive ping over the connection.
/// </summary>
Task KeepAlive();
/// <summary>
/// Kills the connection.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "End", Justification = "Ends the connction thus the name is appropriate.")]
void End();
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Transports
{
/// <summary>
/// Represents a transport that communicates
/// </summary>
public interface ITransport
{
/// <summary>
/// Gets or sets a callback that is invoked when the transport receives data.
/// </summary>
Func<string, Task> Received { get; set; }
/// <summary>
/// Gets or sets a callback that is invoked when the initial connection connects to the transport.
/// </summary>
Func<Task> Connected { get; set; }
/// <summary>
/// Gets or sets a callback that is invoked when the transport connects.
/// </summary>
Func<Task> TransportConnected { get; set; }
/// <summary>
/// Gets or sets a callback that is invoked when the transport reconnects.
/// </summary>
Func<Task> Reconnected { get; set; }
/// <summary>
/// Gets or sets a callback that is invoked when the transport disconnects.
/// </summary>
Func<Task> Disconnected { get; set; }
/// <summary>
/// Gets or sets the connection id for the transport.
/// </summary>
string ConnectionId { get; set; }
/// <summary>
/// Processes the specified <see cref="ITransportConnection"/> for this transport.
/// </summary>
/// <param name="connection">The <see cref="ITransportConnection"/> to process.</param>
/// <returns>A <see cref="Task"/> that completes when the transport has finished processing the connection.</returns>
Task ProcessRequest(ITransportConnection connection);
/// <summary>
/// Sends data over the transport.
/// </summary>
/// <param name="value">The value to be sent.</param>
/// <returns>A <see cref="Task"/> that completes when the send is complete.</returns>
Task Send(object value);
}
}
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNet.SignalR.Transports
{
public interface ITransportConnection
{
IDisposable Receive(string messageId, Func<PersistentResponse, object, Task<bool>> callback, int maxMessages, object state);
Task Send(ConnectionMessage message);
}
}
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.AspNet.SignalR.Transports
{
/// <summary>
/// Manages tracking the state of connections.
/// </summary>
public interface ITransportHeartbeat
{
/// <summary>
/// Adds a new connection to the list of tracked connections.
/// </summary>
/// <param name="connection">The connection to be added.</param>
bool AddConnection(ITrackingConnection connection);
/// <summary>
/// Marks an existing connection as active.
/// </summary>
/// <param name="connection">The connection to mark.</param>
void MarkConnection(ITrackingConnection connection);
/// <summary>
/// Removes a connection from the list of tracked connections.
/// </summary>
/// <param name="connection">The connection to remove.</param>
void RemoveConnection(ITrackingConnection connection);
/// <summary>
/// Gets a list of connections being tracked.
/// </summary>
/// <returns>A list of connections.</returns>
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive.")]
IList<ITrackingConnection> GetConnections();
}
}
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using Microsoft.AspNet.SignalR.Hosting;
namespace Microsoft.AspNet.SignalR.Transports
{
/// <summary>
/// Manages the transports for connections.
/// </summary>
public interface ITransportManager
{
/// <summary>
/// Gets the specified transport for the specified <see cref="HostContext"/>.
/// </summary>
/// <param name="hostContext">The <see cref="HostContext"/> for the current request.</param>
/// <returns>The <see cref="ITransport"/> for the specified <see cref="HostContext"/>.</returns>
ITransport GetTransport(HostContext hostContext);
/// <summary>
/// Determines whether the specified transport is supported.
/// </summary>
/// <param name="transportName">The name of the transport to test.</param>
/// <returns>True if the transport is supported, otherwise False.</returns>
bool SupportsTransport(string transportName);
}
}
@@ -0,0 +1,401 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Hosting;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Json;
using Microsoft.AspNet.SignalR.Tracing;
namespace Microsoft.AspNet.SignalR.Transports
{
public class LongPollingTransport : TransportDisconnectBase, ITransport
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IPerformanceCounterManager _counters;
// This should be ok to do since long polling request never hang around too long
// so we won't bloat memory
private const int MaxMessages = 5000;
public LongPollingTransport(HostContext context, IDependencyResolver resolver)
: this(context,
resolver.Resolve<IJsonSerializer>(),
resolver.Resolve<ITransportHeartbeat>(),
resolver.Resolve<IPerformanceCounterManager>(),
resolver.Resolve<ITraceManager>())
{
}
public LongPollingTransport(HostContext context,
IJsonSerializer jsonSerializer,
ITransportHeartbeat heartbeat,
IPerformanceCounterManager performanceCounterManager,
ITraceManager traceManager)
: base(context, heartbeat, performanceCounterManager, traceManager)
{
_jsonSerializer = jsonSerializer;
_counters = performanceCounterManager;
}
/// <summary>
/// The number of milliseconds to tell the browser to wait before restablishing a
/// long poll connection after data is sent from the server. Defaults to 0.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "long", Justification = "Longpolling is a well known term")]
public static long LongPollDelay
{
get;
set;
}
public override TimeSpan DisconnectThreshold
{
get { return TimeSpan.FromMilliseconds(LongPollDelay); }
}
public override bool IsConnectRequest
{
get
{
return Context.Request.Url.LocalPath.EndsWith("/connect", StringComparison.OrdinalIgnoreCase);
}
}
private bool IsReconnectRequest
{
get
{
return Context.Request.Url.LocalPath.EndsWith("/reconnect", StringComparison.OrdinalIgnoreCase);
}
}
private bool IsJsonp
{
get
{
return !String.IsNullOrEmpty(JsonpCallback);
}
}
private bool IsSendRequest
{
get
{
return Context.Request.Url.LocalPath.EndsWith("/send", StringComparison.OrdinalIgnoreCase);
}
}
private string MessageId
{
get
{
return Context.Request.QueryString["messageId"];
}
}
private string JsonpCallback
{
get
{
return Context.Request.QueryString["callback"];
}
}
public override bool SupportsKeepAlive
{
get
{
return false;
}
}
public Func<string, Task> Received { get; set; }
public Func<Task> TransportConnected { get; set; }
public Func<Task> Connected { get; set; }
public Func<Task> Reconnected { get; set; }
public Task ProcessRequest(ITransportConnection connection)
{
Connection = connection;
if (IsSendRequest)
{
return ProcessSendRequest();
}
else if (IsAbortRequest)
{
return Connection.Abort(ConnectionId);
}
else
{
InitializePersistentState();
return ProcessReceiveRequest(connection);
}
}
public Task Send(PersistentResponse response)
{
Heartbeat.MarkConnection(this);
AddTransportData(response);
return Send((object)response);
}
public Task Send(object value)
{
var context = new LongPollingTransportContext(this, value);
return EnqueueOperation(state => PerformSend(state), context);
}
private Task ProcessSendRequest()
{
string data = Context.Request.Form["data"] ?? Context.Request.QueryString["data"];
if (Received != null)
{
return Received(data);
}
return TaskAsyncHelper.Empty;
}
private Task ProcessReceiveRequest(ITransportConnection connection)
{
Func<Task> initialize = null;
bool newConnection = Heartbeat.AddConnection(this);
if (IsConnectRequest)
{
if (newConnection)
{
initialize = Connected;
_counters.ConnectionsConnected.Increment();
}
}
else if (IsReconnectRequest)
{
initialize = Reconnected;
}
var series = new Func<object, Task>[]
{
state => ((Func<Task>)state).Invoke(),
state => ((Func<Task>)state).Invoke()
};
var states = new object[] { TransportConnected ?? _emptyTaskFunc,
initialize ?? _emptyTaskFunc };
Func<Task> fullInit = () => TaskAsyncHelper.Series(series, states);
return ProcessMessages(connection, fullInit);
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The subscription is disposed in the callback")]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception is captured in a task")]
private Task ProcessMessages(ITransportConnection connection, Func<Task> initialize)
{
var disposer = new Disposer();
var cancelContext = new LongPollingTransportContext(this, disposer);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
IDisposable registration = ConnectionEndToken.SafeRegister(state => Cancel(state), cancelContext);
var lifeTime = new RequestLifetime(this, _requestLifeTime, registration);
var messageContext = new MessageContext(this, lifeTime);
try
{
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
IDisposable subscription = connection.Receive(MessageId,
(response, state) => OnMessageReceived(response, state),
MaxMessages,
messageContext);
// Set the disposable
disposer.Set(subscription);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
initialize().Catch((ex, state) => OnError(ex, state), messageContext);
}
catch (Exception ex)
{
lifeTime.Complete(ex);
}
return _requestLifeTime.Task;
}
private static void Cancel(object state)
{
var context = (LongPollingTransportContext)state;
context.Transport.Trace.TraceEvent(TraceEventType.Verbose, 0, "Cancel(" + context.Transport.ConnectionId + ")");
((IDisposable)context.State).Dispose();
}
private static Task<bool> OnMessageReceived(PersistentResponse response, object state)
{
var context = (MessageContext)state;
response.TimedOut = context.Transport.IsTimedOut;
Task task = TaskAsyncHelper.Empty;
if (response.Aborted)
{
// If this was a clean disconnect then raise the event
task = context.Transport.Abort();
}
if (response.Terminal)
{
// If the response wasn't sent, send it before ending the request
if (!context.ResponseSent)
{
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return task.Then((ctx, resp) => ctx.Transport.Send(resp), context, response)
.Then(() =>
{
context.Lifetime.Complete();
return TaskAsyncHelper.False;
});
}
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return task.Then(() =>
{
context.Lifetime.Complete();
return TaskAsyncHelper.False;
});
}
// Mark the response as sent
context.ResponseSent = true;
// Send the response and return false
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return task.Then((ctx, resp) => ctx.Transport.Send(resp), context, response)
.Then(() => TaskAsyncHelper.False);
}
private static Task PerformSend(object state)
{
var context = (LongPollingTransportContext)state;
if (!context.Transport.IsAlive)
{
return TaskAsyncHelper.Empty;
}
context.Transport.Context.Response.ContentType = context.Transport.IsJsonp ? JsonUtility.JavaScriptMimeType : JsonUtility.JsonMimeType;
if (context.Transport.IsJsonp)
{
context.Transport.OutputWriter.Write(context.Transport.JsonpCallback);
context.Transport.OutputWriter.Write("(");
}
context.Transport._jsonSerializer.Serialize(context.State, context.Transport.OutputWriter);
if (context.Transport.IsJsonp)
{
context.Transport.OutputWriter.Write(");");
}
context.Transport.OutputWriter.Flush();
return context.Transport.Context.Response.End();
}
private static void OnError(AggregateException ex, object state)
{
var context = (MessageContext)state;
context.Transport.IncrementErrors();
context.Lifetime.Complete(ex);
}
private static void AddTransportData(PersistentResponse response)
{
if (LongPollDelay > 0)
{
response.LongPollDelay = LongPollDelay;
}
}
private class LongPollingTransportContext
{
public object State;
public LongPollingTransport Transport;
public LongPollingTransportContext(LongPollingTransport transport, object state)
{
State = state;
Transport = transport;
}
}
private class MessageContext
{
public LongPollingTransport Transport;
public RequestLifetime Lifetime;
public bool ResponseSent;
public MessageContext(LongPollingTransport longPollingTransport, RequestLifetime requestLifetime)
{
Transport = longPollingTransport;
Lifetime = requestLifetime;
}
}
private class RequestLifetime
{
private readonly HttpRequestLifeTime _requestLifeTime;
private readonly LongPollingTransport _transport;
private readonly IDisposable _registration;
public RequestLifetime(LongPollingTransport transport, HttpRequestLifeTime requestLifeTime, IDisposable registration)
{
_transport = transport;
_registration = registration;
_requestLifeTime = requestLifeTime;
}
public void Complete()
{
Complete(exception: null);
}
public void Complete(Exception exception)
{
// End the request
_requestLifeTime.Complete(exception);
// Dispose of the cancellation token subscription
_registration.Dispose();
// Dispose any state on the transport
_transport.Dispose();
}
}
}
}
@@ -0,0 +1,180 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Json;
using Microsoft.AspNet.SignalR.Messaging;
using Newtonsoft.Json;
namespace Microsoft.AspNet.SignalR.Transports
{
/// <summary>
/// Represents a response to a connection.
/// </summary>
public sealed class PersistentResponse : IJsonWritable
{
private readonly Func<Message, bool> _exclude;
private readonly Action<TextWriter> _writeCursor;
public PersistentResponse()
: this(message => true, writer => { })
{
}
/// <summary>
/// Creates a new instance of <see cref="PersistentResponse"/>.
/// </summary>
/// <param name="exclude">A filter that determines whether messages should be written to the client.</param>
/// <param name="writeCursor">The cursor writer.</param>
public PersistentResponse(Func<Message, bool> exclude, Action<TextWriter> writeCursor)
{
_exclude = exclude;
_writeCursor = writeCursor;
}
/// <summary>
/// The list of messages to be sent to the receiving connection.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an optimization and this type is only used for serialization.")]
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This type is only used for serialization")]
public IList<ArraySegment<Message>> Messages { get; set; }
public bool Terminal { get; set; }
/// <summary>
/// The total count of the messages sent the receiving connection.
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// True if the connection receives a disconnect command.
/// </summary>
public bool Disconnect { get; set; }
/// <summary>
/// True if the connection was forcibly closed.
/// </summary>
public bool Aborted { get; set; }
/// <summary>
/// True if the connection timed out.
/// </summary>
public bool TimedOut { get; set; }
/// <summary>
/// Signed token representing the list of groups. Updates on change.
/// </summary>
public string GroupsToken { get; set; }
/// <summary>
/// The time the long polling client should wait before reestablishing a connection if no data is received.
/// </summary>
public long? LongPollDelay { get; set; }
/// <summary>
/// Serializes only the necessary components of the <see cref="PersistentResponse"/> to JSON
/// using Json.NET's JsonTextWriter to improve performance.
/// </summary>
/// <param name="writer">The <see cref="System.IO.TextWriter"/> that receives the JSON serialization.</param>
void IJsonWritable.WriteJson(TextWriter writer)
{
if (writer == null)
{
throw new ArgumentNullException("writer");
}
var jsonWriter = new JsonTextWriter(writer);
jsonWriter.WriteStartObject();
// REVIEW: Is this 100% correct?
writer.Write('"');
writer.Write("C");
writer.Write('"');
writer.Write(':');
writer.Write('"');
_writeCursor(writer);
writer.Write('"');
writer.Write(',');
if (Disconnect)
{
jsonWriter.WritePropertyName("D");
jsonWriter.WriteValue(1);
}
if (TimedOut)
{
jsonWriter.WritePropertyName("T");
jsonWriter.WriteValue(1);
}
if (GroupsToken != null)
{
jsonWriter.WritePropertyName("G");
jsonWriter.WriteValue(GroupsToken);
}
if (LongPollDelay.HasValue)
{
jsonWriter.WritePropertyName("L");
jsonWriter.WriteValue(LongPollDelay.Value);
}
jsonWriter.WritePropertyName("M");
jsonWriter.WriteStartArray();
WriteMessages(writer, jsonWriter);
jsonWriter.WriteEndArray();
jsonWriter.WriteEndObject();
}
private void WriteMessages(TextWriter writer, JsonTextWriter jsonWriter)
{
if (Messages == null)
{
return;
}
// If the writer is a binary writer then write to the underlying writer directly
var binaryWriter = writer as IBinaryWriter;
bool first = true;
for (int i = 0; i < Messages.Count; i++)
{
ArraySegment<Message> segment = Messages[i];
for (int j = segment.Offset; j < segment.Offset + segment.Count; j++)
{
Message message = segment.Array[j];
if (!message.IsCommand && !_exclude(message))
{
if (binaryWriter != null)
{
if (!first)
{
// We need to write the array separator manually
writer.Write(',');
}
// If we can write binary then just write it
binaryWriter.Write(message.Value);
first = false;
}
else
{
// Write the raw JSON value
jsonWriter.WriteRawValue(message.GetString());
}
}
}
}
}
}
}
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Hosting;
namespace Microsoft.AspNet.SignalR.Transports
{
public class ServerSentEventsTransport : ForeverTransport
{
public ServerSentEventsTransport(HostContext context, IDependencyResolver resolver)
: base(context, resolver)
{
}
public override Task KeepAlive()
{
if (InitializeTcs == null || !InitializeTcs.Task.IsCompleted)
{
return TaskAsyncHelper.Empty;
}
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return EnqueueOperation(state => PerformKeepAlive(state), this);
}
public override Task Send(PersistentResponse response)
{
OnSendingResponse(response);
var context = new SendContext(this, response);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return EnqueueOperation(state => PerformSend(state), context);
}
protected internal override Task InitializeResponse(ITransportConnection connection)
{
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return base.InitializeResponse(connection)
.Then(s => WriteInit(s), this);
}
private static Task PerformKeepAlive(object state)
{
var transport = (ServerSentEventsTransport)state;
transport.OutputWriter.Write("data: {}");
transport.OutputWriter.WriteLine();
transport.OutputWriter.WriteLine();
transport.OutputWriter.Flush();
return transport.Context.Response.Flush();
}
private static Task PerformSend(object state)
{
var context = (SendContext)state;
context.Transport.OutputWriter.Write("data: ");
context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter);
context.Transport.OutputWriter.WriteLine();
context.Transport.OutputWriter.WriteLine();
context.Transport.OutputWriter.Flush();
return context.Transport.Context.Response.Flush();
}
private static Task WriteInit(ServerSentEventsTransport transport)
{
transport.Context.Response.ContentType = "text/event-stream";
// "data: initialized\n\n"
transport.OutputWriter.Write("data: initialized");
transport.OutputWriter.WriteLine();
transport.OutputWriter.WriteLine();
transport.OutputWriter.Flush();
return transport.Context.Response.Flush();
}
private class SendContext
{
public ServerSentEventsTransport Transport;
public object State;
public SendContext(ServerSentEventsTransport transport, object state)
{
Transport = transport;
State = state;
}
}
}
}
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Messaging;
namespace Microsoft.AspNet.SignalR.Transports
{
internal static class TransportConnectionExtensions
{
internal static Task Close(this ITransportConnection connection, string connectionId)
{
return SendCommand(connection, connectionId, CommandType.Disconnect);
}
internal static Task Abort(this ITransportConnection connection, string connectionId)
{
return SendCommand(connection, connectionId, CommandType.Abort);
}
private static Task SendCommand(ITransportConnection connection, string connectionId, CommandType commandType)
{
var command = new Command
{
CommandType = commandType
};
var message = new ConnectionMessage(PrefixHelper.GetConnectionId(connectionId),
command);
return connection.Send(message);
}
}
}
@@ -0,0 +1,19 @@
using System;
namespace Microsoft.AspNet.SignalR.Transports
{
[Flags]
public enum TransportConnectionStates
{
None = 0,
Added = 1,
Removed = 2,
Replaced = 4,
QueueDrained = 8,
HttpRequestEnded = 16,
Disconnected = 32,
Aborted = 64,
DisconnectMessageReceived = 128,
Disposed = 65536,
}
}
@@ -0,0 +1,336 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Hosting;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Tracing;
namespace Microsoft.AspNet.SignalR.Transports
{
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Disposable fields are disposed from a different method")]
public abstract class TransportDisconnectBase : ITrackingConnection
{
private readonly HostContext _context;
private readonly ITransportHeartbeat _heartbeat;
private TextWriter _outputWriter;
private TraceSource _trace;
private int _timedOut;
private readonly IPerformanceCounterManager _counters;
private int _ended;
private TransportConnectionStates _state;
internal static readonly Func<Task> _emptyTaskFunc = () => TaskAsyncHelper.Empty;
// Token that represents the end of the connection based on a combination of
// conditions (timeout, disconnect, connection forcibly ended, host shutdown)
private CancellationToken _connectionEndToken;
private SafeCancellationTokenSource _connectionEndTokenSource;
// Token that represents the host shutting down
private CancellationToken _hostShutdownToken;
private IDisposable _hostRegistration;
private IDisposable _connectionEndRegistration;
internal HttpRequestLifeTime _requestLifeTime;
protected TransportDisconnectBase(HostContext context, ITransportHeartbeat heartbeat, IPerformanceCounterManager performanceCounterManager, ITraceManager traceManager)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (heartbeat == null)
{
throw new ArgumentNullException("heartbeat");
}
if (performanceCounterManager == null)
{
throw new ArgumentNullException("performanceCounterManager");
}
if (traceManager == null)
{
throw new ArgumentNullException("traceManager");
}
_context = context;
_heartbeat = heartbeat;
_counters = performanceCounterManager;
// Queue to protect against overlapping writes to the underlying response stream
WriteQueue = new TaskQueue();
_trace = traceManager["SignalR.Transports." + GetType().Name];
}
protected TraceSource Trace
{
get
{
return _trace;
}
}
public string ConnectionId
{
get;
set;
}
public virtual TextWriter OutputWriter
{
get
{
if (_outputWriter == null)
{
_outputWriter = CreateResponseWriter();
_outputWriter.NewLine = "\n";
}
return _outputWriter;
}
}
internal TaskQueue WriteQueue
{
get;
set;
}
public Func<Task> Disconnected { get; set; }
public virtual CancellationToken CancellationToken
{
get { return _context.Response.CancellationToken; }
}
public virtual bool IsAlive
{
get
{
// If the CTS is tripped or the request has ended then the connection isn't alive
return !(CancellationToken.IsCancellationRequested || (_requestLifeTime != null && _requestLifeTime.Task.IsCompleted));
}
}
protected CancellationToken ConnectionEndToken
{
get
{
return _connectionEndToken;
}
}
public bool IsTimedOut
{
get
{
return _timedOut == 1;
}
}
public virtual bool SupportsKeepAlive
{
get
{
return true;
}
}
public virtual TimeSpan DisconnectThreshold
{
get { return TimeSpan.FromSeconds(5); }
}
public virtual bool IsConnectRequest
{
get
{
return Context.Request.Url.LocalPath.EndsWith("/connect", StringComparison.OrdinalIgnoreCase);
}
}
protected bool IsAbortRequest
{
get
{
return Context.Request.Url.LocalPath.EndsWith("/abort", StringComparison.OrdinalIgnoreCase);
}
}
protected ITransportConnection Connection { get; set; }
protected HostContext Context
{
get { return _context; }
}
protected ITransportHeartbeat Heartbeat
{
get { return _heartbeat; }
}
public Uri Url
{
get { return _context.Request.Url; }
}
protected virtual TextWriter CreateResponseWriter()
{
return new BufferTextWriter(Context.Response);
}
protected void IncrementErrors()
{
_counters.ErrorsTransportTotal.Increment();
_counters.ErrorsTransportPerSec.Increment();
_counters.ErrorsAllTotal.Increment();
_counters.ErrorsAllPerSec.Increment();
}
public Task Disconnect()
{
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return Abort(clean: false).Then(transport => transport.Connection.Close(transport.ConnectionId), this);
}
public Task Abort()
{
return Abort(clean: true);
}
public Task Abort(bool clean)
{
if (clean)
{
ApplyState(TransportConnectionStates.Aborted);
}
else
{
ApplyState(TransportConnectionStates.Disconnected);
}
Trace.TraceInformation("Abort(" + ConnectionId + ")");
// When a connection is aborted (graceful disconnect) we send a command to it
// telling to to disconnect. At that moment, we raise the disconnect event and
// remove this connection from the heartbeat so we don't end up raising it for the same connection.
Heartbeat.RemoveConnection(this);
// End the connection
End();
var disconnected = Disconnected ?? _emptyTaskFunc;
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return disconnected().Catch((ex, state) => OnDisconnectError(ex, state), Trace)
.Then(counters => counters.ConnectionsDisconnected.Increment(), _counters);
}
public void ApplyState(TransportConnectionStates states)
{
_state |= states;
}
public void Timeout()
{
if (Interlocked.Exchange(ref _timedOut, 1) == 0)
{
Trace.TraceInformation("Timeout(" + ConnectionId + ")");
End();
}
}
public virtual Task KeepAlive()
{
return TaskAsyncHelper.Empty;
}
public void End()
{
if (Interlocked.Exchange(ref _ended, 1) == 0)
{
Trace.TraceInformation("End(" + ConnectionId + ")");
if (_connectionEndTokenSource != null)
{
_connectionEndTokenSource.Cancel();
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_connectionEndTokenSource.Dispose();
_connectionEndRegistration.Dispose();
_hostRegistration.Dispose();
ApplyState(TransportConnectionStates.Disposed);
}
}
protected virtual internal Task EnqueueOperation(Func<Task> writeAsync)
{
return EnqueueOperation(state => ((Func<Task>)state).Invoke(), writeAsync);
}
protected virtual internal Task EnqueueOperation(Func<object, Task> writeAsync, object state)
{
if (!IsAlive)
{
return TaskAsyncHelper.Empty;
}
// Only enqueue new writes if the connection is alive
return WriteQueue.Enqueue(writeAsync, state);
}
protected virtual void InitializePersistentState()
{
_hostShutdownToken = _context.HostShutdownToken();
_requestLifeTime = new HttpRequestLifeTime(this, WriteQueue, Trace, ConnectionId);
// Create a token that represents the end of this connection's life
_connectionEndTokenSource = new SafeCancellationTokenSource();
_connectionEndToken = _connectionEndTokenSource.Token;
// Handle the shutdown token's callback so we can end our token if it trips
_hostRegistration = _hostShutdownToken.SafeRegister(state =>
{
((SafeCancellationTokenSource)state).Cancel();
},
_connectionEndTokenSource);
// When the connection ends release the request
_connectionEndRegistration = CancellationToken.SafeRegister(state =>
{
((HttpRequestLifeTime)state).Complete();
},
_requestLifeTime);
}
private static void OnDisconnectError(AggregateException ex, object state)
{
((TraceSource)state).TraceEvent(TraceEventType.Error, 0, "Failed to raise disconnect: " + ex.GetBaseException());
}
}
}
@@ -0,0 +1,384 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.AspNet.SignalR.Configuration;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Tracing;
namespace Microsoft.AspNet.SignalR.Transports
{
/// <summary>
/// Default implementation of <see cref="ITransportHeartbeat"/>.
/// </summary>
public class TransportHeartbeat : ITransportHeartbeat, IDisposable
{
private readonly ConcurrentDictionary<string, ConnectionMetadata> _connections = new ConcurrentDictionary<string, ConnectionMetadata>();
private readonly Timer _timer;
private readonly IConfigurationManager _configurationManager;
private readonly IServerCommandHandler _serverCommandHandler;
private readonly TraceSource _trace;
private readonly string _serverId;
private readonly IPerformanceCounterManager _counters;
private readonly object _counterLock = new object();
private int _running;
private ulong _heartbeatCount;
/// <summary>
/// Initializes and instance of the <see cref="TransportHeartbeat"/> class.
/// </summary>
/// <param name="resolver">The <see cref="IDependencyResolver"/>.</param>
public TransportHeartbeat(IDependencyResolver resolver)
{
_configurationManager = resolver.Resolve<IConfigurationManager>();
_serverCommandHandler = resolver.Resolve<IServerCommandHandler>();
_serverId = resolver.Resolve<IServerIdManager>().ServerId;
_counters = resolver.Resolve<IPerformanceCounterManager>();
var traceManager = resolver.Resolve<ITraceManager>();
_trace = traceManager["SignalR.Transports.TransportHeartBeat"];
_serverCommandHandler.Command = ProcessServerCommand;
// REVIEW: When to dispose the timer?
_timer = new Timer(Beat,
null,
_configurationManager.HeartbeatInterval(),
_configurationManager.HeartbeatInterval());
}
private TraceSource Trace
{
get
{
return _trace;
}
}
private void ProcessServerCommand(ServerCommand command)
{
switch (command.ServerCommandType)
{
case ServerCommandType.RemoveConnection:
// Only remove connections if this command didn't originate from the owner
if (!command.IsFromSelf(_serverId))
{
var connectionId = (string)command.Value;
// Remove the connection
ConnectionMetadata metadata;
if (_connections.TryGetValue(connectionId, out metadata))
{
metadata.Connection.End();
RemoveConnection(metadata.Connection);
}
}
break;
default:
break;
}
}
/// <summary>
/// Adds a new connection to the list of tracked connections.
/// </summary>
/// <param name="connection">The connection to be added.</param>
public bool AddConnection(ITrackingConnection connection)
{
if (connection == null)
{
throw new ArgumentNullException("connection");
}
var newMetadata = new ConnectionMetadata(connection);
bool isNewConnection = true;
_connections.AddOrUpdate(connection.ConnectionId, newMetadata, (key, old) =>
{
Trace.TraceEvent(TraceEventType.Verbose, 0, "Connection {0} exists. Closing previous connection.", old.Connection.ConnectionId);
// Kick out the older connection. This should only happen when
// a previous connection attempt fails on the client side (e.g. transport fallback).
old.Connection.ApplyState(TransportConnectionStates.Replaced);
// Don't bother disposing the registration here since the token source
// gets disposed after the request has ended
old.Connection.End();
// If we have old metadata this isn't a new connection
isNewConnection = false;
return newMetadata;
});
if (isNewConnection)
{
Trace.TraceInformation("Connection {0} is New.", connection.ConnectionId);
}
lock (_counterLock)
{
_counters.ConnectionsCurrent.RawValue = _connections.Count;
}
// Set the initial connection time
newMetadata.Initial = DateTime.UtcNow;
newMetadata.Connection.ApplyState(TransportConnectionStates.Added);
return isNewConnection;
}
/// <summary>
/// Removes a connection from the list of tracked connections.
/// </summary>
/// <param name="connection">The connection to remove.</param>
public void RemoveConnection(ITrackingConnection connection)
{
if (connection == null)
{
throw new ArgumentNullException("connection");
}
// Remove the connection and associated metadata
ConnectionMetadata metadata;
if (_connections.TryRemove(connection.ConnectionId, out metadata))
{
lock (_counterLock)
{
_counters.ConnectionsCurrent.RawValue = _connections.Count;
}
connection.ApplyState(TransportConnectionStates.Removed);
Trace.TraceInformation("Removing connection {0}", connection.ConnectionId);
}
}
/// <summary>
/// Marks an existing connection as active.
/// </summary>
/// <param name="connection">The connection to mark.</param>
public void MarkConnection(ITrackingConnection connection)
{
if (connection == null)
{
throw new ArgumentNullException("connection");
}
// Do nothing if the connection isn't alive
if (!connection.IsAlive)
{
return;
}
ConnectionMetadata metadata;
if (_connections.TryGetValue(connection.ConnectionId, out metadata))
{
metadata.LastMarked = DateTime.UtcNow;
}
}
public IList<ITrackingConnection> GetConnections()
{
return _connections.Values.Select(metadata => metadata.Connection).ToList();
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're tracing exceptions and don't want to crash the process.")]
private void Beat(object state)
{
if (Interlocked.Exchange(ref _running, 1) == 1)
{
Trace.TraceEvent(TraceEventType.Verbose, 0, "Timer handler took longer than current interval");
return;
}
lock (_counterLock)
{
_counters.ConnectionsCurrent.RawValue = _connections.Count;
}
try
{
_heartbeatCount++;
foreach (var metadata in _connections.Values)
{
if (metadata.Connection.IsAlive)
{
CheckTimeoutAndKeepAlive(metadata);
}
else
{
Trace.TraceEvent(TraceEventType.Verbose, 0, metadata.Connection.ConnectionId + " is dead");
// Check if we need to disconnect this connection
CheckDisconnect(metadata);
}
}
}
catch (Exception ex)
{
Trace.TraceEvent(TraceEventType.Error, 0, "SignalR error during transport heart beat on background thread: {0}", ex);
}
finally
{
Interlocked.Exchange(ref _running, 0);
}
}
private void CheckTimeoutAndKeepAlive(ConnectionMetadata metadata)
{
if (RaiseTimeout(metadata))
{
// If we're past the expiration time then just timeout the connection
metadata.Connection.Timeout();
}
else
{
// The connection is still alive so we need to keep it alive with a server side "ping".
// This is for scenarios where networking hardware (proxies, loadbalancers) get in the way
// of us handling timeout's or disconnects gracefully
if (RaiseKeepAlive(metadata))
{
Trace.TraceEvent(TraceEventType.Verbose, 0, "KeepAlive(" + metadata.Connection.ConnectionId + ")");
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
metadata.Connection.KeepAlive().Catch((ex, state) => OnKeepAliveError(ex, state), Trace);
}
MarkConnection(metadata.Connection);
}
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're tracing exceptions and don't want to crash the process.")]
private void CheckDisconnect(ConnectionMetadata metadata)
{
try
{
if (RaiseDisconnect(metadata))
{
// Remove the connection from the list
RemoveConnection(metadata.Connection);
// Fire disconnect on the connection
metadata.Connection.Disconnect();
}
}
catch (Exception ex)
{
// Swallow exceptions that might happen during disconnect
Trace.TraceEvent(TraceEventType.Error, 0, "Raising Disconnect failed: {0}", ex);
}
}
private bool RaiseDisconnect(ConnectionMetadata metadata)
{
// The transport is currently dead but it could just be reconnecting
// so we to check it's last active time to see if it's over the disconnect
// threshold
TimeSpan elapsed = DateTime.UtcNow - metadata.LastMarked;
// The threshold for disconnect is the transport threshold + (potential network issues)
var threshold = metadata.Connection.DisconnectThreshold + _configurationManager.DisconnectTimeout;
return elapsed >= threshold;
}
private bool RaiseKeepAlive(ConnectionMetadata metadata)
{
var keepAlive = _configurationManager.KeepAlive;
// Don't raise keep alive if it's set to 0 or the transport doesn't support
// keep alive
if (keepAlive == null || !metadata.Connection.SupportsKeepAlive)
{
return false;
}
// Raise keep alive if the keep alive value has passed
return _heartbeatCount % (ulong)ConfigurationExtensions.HeartBeatsPerKeepAlive == 0;
}
private bool RaiseTimeout(ConnectionMetadata metadata)
{
// The connection already timed out so do nothing
if (metadata.Connection.IsTimedOut)
{
return false;
}
var keepAlive = _configurationManager.KeepAlive;
// If keep alive is configured and the connection supports keep alive
// don't ever time out
if (keepAlive != null && metadata.Connection.SupportsKeepAlive)
{
return false;
}
TimeSpan elapsed = DateTime.UtcNow - metadata.Initial;
// Only raise timeout if we're past the configured connection timeout.
return elapsed >= _configurationManager.ConnectionTimeout;
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_timer != null)
{
_timer.Dispose();
}
Trace.TraceInformation("Dispose(). Closing all connections");
// Kill all connections
foreach (var pair in _connections)
{
ConnectionMetadata metadata;
if (_connections.TryGetValue(pair.Key, out metadata))
{
metadata.Connection.End();
}
}
}
}
public void Dispose()
{
Dispose(true);
}
private static void OnKeepAliveError(AggregateException ex, object state)
{
((TraceSource)state).TraceEvent(TraceEventType.Error, 0, "Failed to send keep alive: " + ex.GetBaseException());
}
private class ConnectionMetadata
{
public ConnectionMetadata(ITrackingConnection connection)
{
Connection = connection;
Initial = DateTime.UtcNow;
LastMarked = DateTime.UtcNow;
}
// The connection instance
public ITrackingConnection Connection { get; set; }
// The last time the connection had any activity
public DateTime LastMarked { get; set; }
// The initial connection time of the connection
public DateTime Initial { get; set; }
}
}
}
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNet.SignalR.Hosting;
namespace Microsoft.AspNet.SignalR.Transports
{
/// <summary>
/// The default <see cref="ITransportManager"/> implementation.
/// </summary>
public class TransportManager : ITransportManager
{
private readonly ConcurrentDictionary<string, Func<HostContext, ITransport>> _transports = new ConcurrentDictionary<string, Func<HostContext, ITransport>>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Initializes a new instance of <see cref="TransportManager"/> class.
/// </summary>
/// <param name="resolver">The default <see cref="IDependencyResolver"/>.</param>
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Those are factory methods")]
public TransportManager(IDependencyResolver resolver)
{
if (resolver == null)
{
throw new ArgumentNullException("resolver");
}
Register("foreverFrame", context => new ForeverFrameTransport(context, resolver));
Register("serverSentEvents", context => new ServerSentEventsTransport(context, resolver));
Register("longPolling", context => new LongPollingTransport(context, resolver));
Register("webSockets", context => new WebSocketTransport(context, resolver));
}
/// <summary>
/// Adds a new transport to the list of supported transports.
/// </summary>
/// <param name="transportName">The specified transport.</param>
/// <param name="transportFactory">The factory method for the specified transport.</param>
public void Register(string transportName, Func<HostContext, ITransport> transportFactory)
{
if (String.IsNullOrEmpty(transportName))
{
throw new ArgumentNullException("transportName");
}
if (transportFactory == null)
{
throw new ArgumentNullException("transportFactory");
}
_transports.TryAdd(transportName, transportFactory);
}
/// <summary>
/// Removes a transport from the list of supported transports.
/// </summary>
/// <param name="transportName">The specified transport.</param>
public void Remove(string transportName)
{
if (String.IsNullOrEmpty(transportName))
{
throw new ArgumentNullException("transportName");
}
Func<HostContext, ITransport> removed;
_transports.TryRemove(transportName, out removed);
}
/// <summary>
/// Gets the specified transport for the specified <see cref="HostContext"/>.
/// </summary>
/// <param name="hostContext">The <see cref="HostContext"/> for the current request.</param>
/// <returns>The <see cref="ITransport"/> for the specified <see cref="HostContext"/>.</returns>
public ITransport GetTransport(HostContext hostContext)
{
if (hostContext == null)
{
throw new ArgumentNullException("hostContext");
}
string transportName = hostContext.Request.QueryString["transport"];
if (String.IsNullOrEmpty(transportName))
{
return null;
}
Func<HostContext, ITransport> factory;
if (_transports.TryGetValue(transportName, out factory))
{
return factory(hostContext);
}
return null;
}
/// <summary>
/// Determines whether the specified transport is supported.
/// </summary>
/// <param name="transportName">The name of the transport to test.</param>
/// <returns>True if the transport is supported, otherwise False.</returns>
public bool SupportsTransport(string transportName)
{
if (String.IsNullOrEmpty(transportName))
{
return false;
}
return _transports.ContainsKey(transportName);
}
}
}
@@ -0,0 +1,166 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Hosting;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Json;
using Microsoft.AspNet.SignalR.Tracing;
namespace Microsoft.AspNet.SignalR.Transports
{
public class WebSocketTransport : ForeverTransport
{
private readonly HostContext _context;
private IWebSocket _socket;
private bool _isAlive = true;
private readonly Action<string> _message;
private readonly Action<bool> _closed;
private readonly Action<Exception> _error;
public WebSocketTransport(HostContext context,
IDependencyResolver resolver)
: this(context,
resolver.Resolve<IJsonSerializer>(),
resolver.Resolve<ITransportHeartbeat>(),
resolver.Resolve<IPerformanceCounterManager>(),
resolver.Resolve<ITraceManager>())
{
}
public WebSocketTransport(HostContext context,
IJsonSerializer serializer,
ITransportHeartbeat heartbeat,
IPerformanceCounterManager performanceCounterWriter,
ITraceManager traceManager)
: base(context, serializer, heartbeat, performanceCounterWriter, traceManager)
{
_context = context;
_message = OnMessage;
_closed = OnClosed;
_error = OnError;
}
public override bool IsAlive
{
get
{
return _isAlive;
}
}
public override CancellationToken CancellationToken
{
get
{
return CancellationToken.None;
}
}
public override Task KeepAlive()
{
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return EnqueueOperation(state =>
{
var webSocket = (IWebSocket)state;
return webSocket.Send("{}");
},
_socket);
}
public override Task ProcessRequest(ITransportConnection connection)
{
var webSocketRequest = _context.Request as IWebSocketRequest;
// Throw if the server implementation doesn't support websockets
if (webSocketRequest == null)
{
throw new InvalidOperationException(Resources.Error_WebSocketsNotSupported);
}
return webSocketRequest.AcceptWebSocketRequest(socket =>
{
_socket = socket;
socket.OnClose = _closed;
socket.OnMessage = _message;
socket.OnError = _error;
return ProcessRequestCore(connection);
});
}
protected override TextWriter CreateResponseWriter()
{
return new BufferTextWriter(_socket);
}
public override Task Send(object value)
{
var context = new WebSocketTransportContext(this, value);
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
return EnqueueOperation(state => PerformSend(state), context);
}
public override Task Send(PersistentResponse response)
{
OnSendingResponse(response);
return Send((object)response);
}
private static Task PerformSend(object state)
{
var context = (WebSocketTransportContext)state;
context.Transport.JsonSerializer.Serialize(context.State, context.Transport.OutputWriter);
context.Transport.OutputWriter.Flush();
return context.Transport._socket.Flush();
}
private void OnMessage(string message)
{
if (Received != null)
{
Received(message).Catch();
}
}
private void OnClosed(bool clean)
{
Trace.TraceInformation("CloseSocket({0}, {1})", clean, ConnectionId);
// If we performed a clean disconnect then we go through the normal disconnect routine. However,
// If we performed an unclean disconnect we want to mark the connection as "not alive" and let the
// HeartBeat clean it up. This is to maintain consistency across the transports.
if (clean)
{
Abort();
}
_isAlive = false;
}
private void OnError(Exception error)
{
Trace.TraceError("OnError({0}, {1})", ConnectionId, error);
}
private class WebSocketTransportContext
{
public WebSocketTransport Transport;
public object State;
public WebSocketTransportContext(WebSocketTransport transport, object state)
{
Transport = transport;
State = state;
}
}
}
}