You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
345 lines
10 KiB
345 lines
10 KiB
// 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;
|
|
}
|
|
}
|
|
|
|
protected CancellationToken HostShutdownToken
|
|
{
|
|
get
|
|
{
|
|
return _hostShutdownToken;
|
|
}
|
|
}
|
|
|
|
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 BinaryTextWriter(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());
|
|
}
|
|
}
|
|
}
|