mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-22 22:16:13 -04:00
imported signalr 1.1.3 into NzbDrone.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user