#if !BESTHTTP_DISABLE_SIGNALR_CORE using System.Threading; #if CSHARP_7_OR_LATER using System.Threading.Tasks; #endif using BestHTTP.Futures; using BestHTTP.SignalRCore.Authentication; using BestHTTP.SignalRCore.Messages; using System; using System.Collections.Generic; using BestHTTP.Logger; using System.Collections.Concurrent; using BestHTTP.PlatformSupport.Threading; namespace BestHTTP.SignalRCore { public sealed class HubConnection : BestHTTP.Extensions.IHeartbeat { public static readonly object[] EmptyArgs = new object[0]; /// /// Uri of the Hub endpoint /// public Uri Uri { get; private set; } /// /// Current state of this connection. /// public ConnectionStates State { get { return (ConnectionStates)this._state; } private set { Interlocked.Exchange(ref this._state, (int)value); } } private volatile int _state; /// /// Current, active ITransport instance. /// public ITransport Transport { get; private set; } /// /// The IProtocol implementation that will parse, encode and decode messages. /// public IProtocol Protocol { get; private set; } /// /// This event is called when the connection is redirected to a new uri. /// public event Action OnRedirected; /// /// This event is called when successfully connected to the hub. /// public event Action OnConnected; /// /// This event is called when an unexpected error happen and the connection is closed. /// public event Action OnError; /// /// This event is called when the connection is gracefully terminated. /// public event Action OnClosed; /// /// This event is called for every server-sent message. When returns false, no further processing of the message is done by the plugin. /// public event Func OnMessage; /// /// Called when the HubConnection start its reconnection process after loosing its underlying connection. /// public event Action OnReconnecting; /// /// Called after a successful reconnection. /// public event Action OnReconnected; /// /// Called for transport related events. /// public event Action OnTransportEvent; /// /// An IAuthenticationProvider implementation that will be used to authenticate the connection. /// public IAuthenticationProvider AuthenticationProvider { get; set; } /// /// Negotiation response sent by the server. /// public NegotiationResult NegotiationResult { get; private set; } /// /// Options that has been used to create the HubConnection. /// public HubOptions Options { get; private set; } /// /// How many times this connection is redirected. /// public int RedirectCount { get; private set; } /// /// The reconnect policy that will be used when the underlying connection is lost. Its default value is null. /// public IRetryPolicy ReconnectPolicy { get; set; } /// /// Logging context of this HubConnection instance. /// public LoggingContext Context { get; private set; } /// /// This will be increment to add a unique id to every message the plugin will send. /// private long lastInvocationId = 1; /// /// Id of the last streaming parameter. /// private int lastStreamId = 1; /// /// Store the callback for all sent message that expect a return value from the server. All sent message has /// a unique invocationId that will be sent back from the server. /// private ConcurrentDictionary invocations = new ConcurrentDictionary(); /// /// This is where we store the methodname => callback mapping. /// private ConcurrentDictionary subscriptions = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); /// /// When we sent out the last message to the server. /// private DateTime lastMessageSentAt; private DateTime lastMessageReceivedAt; private DateTime connectionStartedAt; private RetryContext currentContext; private DateTime reconnectStartTime = DateTime.MinValue; private DateTime reconnectAt; private List triedoutTransports = new List(); private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); private bool pausedInLastFrame; public HubConnection(Uri hubUri, IProtocol protocol) : this(hubUri, protocol, new HubOptions()) { } public HubConnection(Uri hubUri, IProtocol protocol, HubOptions options) { this.Context = new LoggingContext(this); this.Uri = hubUri; this.State = ConnectionStates.Initial; this.Options = options; this.Protocol = protocol; this.Protocol.Connection = this; this.AuthenticationProvider = new DefaultAccessTokenAuthenticator(this); } public void StartConnect() { if (this.State != ConnectionStates.Initial && this.State != ConnectionStates.Redirected && this.State != ConnectionStates.Reconnecting) { HTTPManager.Logger.Warning("HubConnection", "StartConnect - Expected Initial or Redirected state, got " + this.State.ToString(), this.Context); return; } if (this.State == ConnectionStates.Initial) { this.connectionStartedAt = DateTime.Now; HTTPManager.Heartbeats.Subscribe(this); } HTTPManager.Logger.Verbose("HubConnection", $"StartConnect State: {this.State}, connectionStartedAt: {this.connectionStartedAt.ToString(System.Globalization.CultureInfo.InvariantCulture)}", this.Context); if (this.AuthenticationProvider != null && this.AuthenticationProvider.IsPreAuthRequired) { HTTPManager.Logger.Information("HubConnection", "StartConnect - Authenticating", this.Context); SetState(ConnectionStates.Authenticating, null, this.defaultReconnect); this.AuthenticationProvider.OnAuthenticationSucceded += OnAuthenticationSucceded; this.AuthenticationProvider.OnAuthenticationFailed += OnAuthenticationFailed; // Start the authentication process this.AuthenticationProvider.StartAuthentication(); } else StartNegotiation(); } #if CSHARP_7_OR_LATER TaskCompletionSource connectAsyncTaskCompletionSource; public Task ConnectAsync() { if (this.State != ConnectionStates.Initial && this.State != ConnectionStates.Redirected && this.State != ConnectionStates.Reconnecting) throw new Exception("HubConnection - ConnectAsync - Expected Initial or Redirected state, got " + this.State.ToString()); if (this.connectAsyncTaskCompletionSource != null) throw new Exception("Connect process already started!"); this.connectAsyncTaskCompletionSource = new TaskCompletionSource(); this.OnConnected += OnAsyncConnectedCallback; this.OnError += OnAsyncConnectFailedCallback; this.StartConnect(); return connectAsyncTaskCompletionSource.Task; } private void OnAsyncConnectedCallback(HubConnection hub) { this.OnConnected -= OnAsyncConnectedCallback; this.OnError -= OnAsyncConnectFailedCallback; this.connectAsyncTaskCompletionSource.TrySetResult(this); this.connectAsyncTaskCompletionSource = null; } private void OnAsyncConnectFailedCallback(HubConnection hub, string error) { this.OnConnected -= OnAsyncConnectedCallback; this.OnError -= OnAsyncConnectFailedCallback; this.connectAsyncTaskCompletionSource.TrySetException(new Exception(error)); this.connectAsyncTaskCompletionSource = null; } #endif private void OnAuthenticationSucceded(IAuthenticationProvider provider) { HTTPManager.Logger.Verbose("HubConnection", "OnAuthenticationSucceded", this.Context); this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded; this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed; StartNegotiation(); } private void OnAuthenticationFailed(IAuthenticationProvider provider, string reason) { HTTPManager.Logger.Error("HubConnection", "OnAuthenticationFailed: " + reason, this.Context); this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded; this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed; SetState(ConnectionStates.Closed, reason, this.defaultReconnect); } private void StartNegotiation() { HTTPManager.Logger.Verbose("HubConnection", "StartNegotiation", this.Context); if (this.State == ConnectionStates.CloseInitiated) { SetState(ConnectionStates.Closed, null, this.defaultReconnect); return; } #if !BESTHTTP_DISABLE_WEBSOCKET if (this.Options.SkipNegotiation && this.Options.PreferedTransport == TransportTypes.WebSocket) { HTTPManager.Logger.Verbose("HubConnection", "Skipping negotiation", this.Context); ConnectImpl(this.Options.PreferedTransport); return; } #endif SetState(ConnectionStates.Negotiating, null, this.defaultReconnect); // https://github.com/dotnet/aspnetcore/blob/master/src/SignalR/docs/specs/TransportProtocols.md#post-endpoint-basenegotiate-request // Send out a negotiation request. While we could skip it and connect right with the websocket transport // it might return with additional information that could be useful. UriBuilder builder = new UriBuilder(this.Uri); if (builder.Path.EndsWith("/")) builder.Path += "negotiate"; else builder.Path += "/negotiate"; string query = builder.Query; if (string.IsNullOrEmpty(query)) query = "negotiateVersion=1"; else query = query.Remove(0, 1) + "&negotiateVersion=1"; builder.Query = query; var request = new HTTPRequest(builder.Uri, HTTPMethods.Post, OnNegotiationRequestFinished); request.Context.Add("Hub", this.Context); if (this.AuthenticationProvider != null) this.AuthenticationProvider.PrepareRequest(request); request.Send(); } private void ConnectImpl(TransportTypes transport) { HTTPManager.Logger.Verbose("HubConnection", "ConnectImpl - " + transport, this.Context); switch (transport) { #if !BESTHTTP_DISABLE_WEBSOCKET case TransportTypes.WebSocket: if (this.NegotiationResult != null && !IsTransportSupported("WebSockets")) { SetState(ConnectionStates.Closed, "Couldn't use preferred transport, as the 'WebSockets' transport isn't supported by the server!", this.defaultReconnect); return; } this.Transport = new Transports.WebSocketTransport(this); this.Transport.OnStateChanged += Transport_OnStateChanged; break; #endif case TransportTypes.LongPolling: if (this.NegotiationResult != null && !IsTransportSupported("LongPolling")) { SetState(ConnectionStates.Closed, "Couldn't use preferred transport, as the 'LongPolling' transport isn't supported by the server!", this.defaultReconnect); return; } this.Transport = new Transports.LongPollingTransport(this); this.Transport.OnStateChanged += Transport_OnStateChanged; break; default: SetState(ConnectionStates.Closed, "Unsupported transport: " + transport, this.defaultReconnect); break; } try { if (this.OnTransportEvent != null) this.OnTransportEvent(this, this.Transport, TransportEvents.SelectedToConnect); } catch(Exception ex) { HTTPManager.Logger.Exception("HubConnection", "ConnectImpl - OnTransportEvent exception in user code!", ex, this.Context); } this.Transport.StartConnect(); } private bool IsTransportSupported(string transportName) { // https://github.com/dotnet/aspnetcore/blob/master/src/SignalR/docs/specs/TransportProtocols.md#post-endpoint-basenegotiate-request // If the negotiation response contains only the url and accessToken, no 'availableTransports' list is sent if (this.NegotiationResult.SupportedTransports == null) return true; for (int i = 0; i < this.NegotiationResult.SupportedTransports.Count; ++i) if (this.NegotiationResult.SupportedTransports[i].Name.Equals(transportName, StringComparison.OrdinalIgnoreCase)) return true; return false; } private void OnNegotiationRequestFinished(HTTPRequest req, HTTPResponse resp) { if (this.State == ConnectionStates.Closed) return; if (this.State == ConnectionStates.CloseInitiated) { SetState(ConnectionStates.Closed, null, this.defaultReconnect); return; } string errorReason = null; switch (req.State) { // The request finished without any problem. case HTTPRequestStates.Finished: if (resp.IsSuccess) { HTTPManager.Logger.Information("HubConnection", "Negotiation Request Finished Successfully! Response: " + resp.DataAsText, this.Context); // Parse negotiation this.NegotiationResult = NegotiationResult.Parse(resp, out errorReason, this); // Room for improvement: check validity of the negotiation result: // If url and accessToken is present, the other two must be null. // https://github.com/dotnet/aspnetcore/blob/master/src/SignalR/docs/specs/TransportProtocols.md#post-endpoint-basenegotiate-request if (string.IsNullOrEmpty(errorReason)) { if (this.NegotiationResult.Url != null) { this.SetState(ConnectionStates.Redirected, null, this.defaultReconnect); if (++this.RedirectCount >= this.Options.MaxRedirects) errorReason = string.Format("MaxRedirects ({0:N0}) reached!", this.Options.MaxRedirects); else { var oldUri = this.Uri; this.Uri = this.NegotiationResult.Url; if (this.OnRedirected != null) { try { this.OnRedirected(this, oldUri, Uri); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "OnNegotiationRequestFinished - OnRedirected", ex, this.Context); } } StartConnect(); } } else ConnectImpl(this.Options.PreferedTransport); } } else // Internal server error? errorReason = string.Format("Negotiation Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}", resp.StatusCode, resp.Message, resp.DataAsText); break; // The request finished with an unexpected error. The request's Exception property may contain more info about the error. case HTTPRequestStates.Error: errorReason = "Negotiation Request Finished with Error! " + (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception"); break; // The request aborted, initiated by the user. case HTTPRequestStates.Aborted: errorReason = "Negotiation Request Aborted!"; break; // Connecting to the server is timed out. case HTTPRequestStates.ConnectionTimedOut: errorReason = "Negotiation Request - Connection Timed Out!"; break; // The request didn't finished in the given time. case HTTPRequestStates.TimedOut: errorReason = "Negotiation Request - Processing the request Timed Out!"; break; } if (errorReason != null) { this.NegotiationResult = new NegotiationResult(); this.NegotiationResult.NegotiationResponse = resp; SetState(ConnectionStates.Closed, errorReason, this.defaultReconnect); } } public void StartClose() { HTTPManager.Logger.Verbose("HubConnection", "StartClose", this.Context); this.defaultReconnect = false; switch(this.State) { case ConnectionStates.Initial: SetState(ConnectionStates.Closed, null, this.defaultReconnect); break; case ConnectionStates.Authenticating: this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded; this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed; this.AuthenticationProvider.Cancel(); SetState(ConnectionStates.Closed, null, this.defaultReconnect); break; case ConnectionStates.Reconnecting: SetState(ConnectionStates.Closed, null, this.defaultReconnect); break; case ConnectionStates.CloseInitiated: case ConnectionStates.Closed: // Already initiated/closed break; default: if (HTTPManager.IsQuitting) { SetState(ConnectionStates.Closed, null, this.defaultReconnect); } else { SetState(ConnectionStates.CloseInitiated, null, this.defaultReconnect); if (this.Transport != null) this.Transport.StartClose(); } break; } } #if CSHARP_7_OR_LATER TaskCompletionSource closeAsyncTaskCompletionSource; public Task CloseAsync() { if (this.closeAsyncTaskCompletionSource != null) throw new Exception("CloseAsync already called!"); this.closeAsyncTaskCompletionSource = new TaskCompletionSource(); this.OnClosed += OnClosedAsyncCallback; this.OnError += OnClosedAsyncErrorCallback; // Avoid race condition by caching task prior to StartClose, // which asynchronously calls OnClosedAsyncCallback, which nulls // this.closeAsyncTaskCompletionSource immediately before we have // a chance to read from it. var task = this.closeAsyncTaskCompletionSource.Task; this.StartClose(); return task; } void OnClosedAsyncCallback(HubConnection hub) { this.OnClosed -= OnClosedAsyncCallback; this.OnError -= OnClosedAsyncErrorCallback; this.closeAsyncTaskCompletionSource.TrySetResult(this); this.closeAsyncTaskCompletionSource = null; } void OnClosedAsyncErrorCallback(HubConnection hub, string error) { this.OnClosed -= OnClosedAsyncCallback; this.OnError -= OnClosedAsyncErrorCallback; this.closeAsyncTaskCompletionSource.TrySetException(new Exception(error)); this.closeAsyncTaskCompletionSource = null; } #endif public IFuture Invoke(string target, params object[] args) { Future future = new Future(); long id = InvokeImp(target, args, (message) => { bool isSuccess = string.IsNullOrEmpty(message.error); if (isSuccess) future.Assign((TResult)this.Protocol.ConvertTo(typeof(TResult), message.result)); else future.Fail(new Exception(message.error)); }, typeof(TResult)); if (id < 0) future.Fail(new Exception("Not in Connected state! Current state: " + this.State)); return future; } #if CSHARP_7_OR_LATER public Task InvokeAsync(string target, params object[] args) { return InvokeAsync(target, default(CancellationToken), args); } public Task InvokeAsync(string target, CancellationToken cancellationToken = default, params object[] args) { TaskCompletionSource tcs = new TaskCompletionSource(); long id = InvokeImp(target, args, (message) => { if (cancellationToken.IsCancellationRequested) { tcs.TrySetCanceled(cancellationToken); return; } bool isSuccess = string.IsNullOrEmpty(message.error); if (isSuccess) tcs.TrySetResult((TResult)this.Protocol.ConvertTo(typeof(TResult), message.result)); else tcs.TrySetException(new Exception(message.error)); }, typeof(TResult)); if (id < 0) tcs.TrySetException(new Exception("Not in Connected state! Current state: " + this.State)); else cancellationToken.Register(() => tcs.TrySetCanceled()); return tcs.Task; } #endif public IFuture Send(string target, params object[] args) { Future future = new Future(); long id = InvokeImp(target, args, (message) => { bool isSuccess = string.IsNullOrEmpty(message.error); if (isSuccess) future.Assign(message.item); else future.Fail(new Exception(message.error)); }, typeof(object)); if (id < 0) future.Fail(new Exception("Not in Connected state! Current state: " + this.State)); return future; } #if CSHARP_7_OR_LATER public Task SendAsync(string target, params object[] args) { return SendAsync(target, default(CancellationToken), args); } public Task SendAsync(string target, CancellationToken cancellationToken = default, params object[] args) { TaskCompletionSource tcs = new TaskCompletionSource(); long id = InvokeImp(target, args, (message) => { if (cancellationToken.IsCancellationRequested) { tcs.TrySetCanceled(cancellationToken); return; } bool isSuccess = string.IsNullOrEmpty(message.error); if (isSuccess) tcs.TrySetResult(message.item); else tcs.TrySetException(new Exception(message.error)); }, typeof(object)); if (id < 0) tcs.TrySetException(new Exception("Not in Connected state! Current state: " + this.State)); else cancellationToken.Register(() => tcs.TrySetCanceled()); return tcs.Task; } #endif private long InvokeImp(string target, object[] args, Action callback, Type itemType, bool isStreamingInvocation = false) { if (this.State != ConnectionStates.Connected) return -1; bool blockingInvocation = callback == null; long invocationId = blockingInvocation ? 0 : System.Threading.Interlocked.Increment(ref this.lastInvocationId); var message = new Message { type = isStreamingInvocation ? MessageTypes.StreamInvocation : MessageTypes.Invocation, invocationId = blockingInvocation ? null : invocationId.ToString(), target = target, arguments = args, nonblocking = callback == null, }; SendMessage(message); if (!blockingInvocation) if (!this.invocations.TryAdd(invocationId, new InvocationDefinition { callback = callback, returnType = itemType })) HTTPManager.Logger.Warning("HubConnection", "InvokeImp - invocations already contains id: " + invocationId, this.Context); return invocationId; } internal void SendMessage(Message message) { if (HTTPManager.Logger.Level == Logger.Loglevels.All) HTTPManager.Logger.Verbose("HubConnection", "SendMessage: " + message.ToString(), this.Context); try { using (new WriteLock(this.rwLock)) { var encoded = this.Protocol.EncodeMessage(message); if (encoded.Data != null) { this.lastMessageSentAt = DateTime.Now; this.Transport.Send(encoded); } } } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "SendMessage", ex, this.Context); } } public DownStreamItemController GetDownStreamController(string target, params object[] args) { long invocationId = System.Threading.Interlocked.Increment(ref this.lastInvocationId); var future = new Future(); future.BeginProcess(); var controller = new DownStreamItemController(this, invocationId, future); Action callback = (Message msg) => { switch (msg.type) { // StreamItem message contains only one item. case MessageTypes.StreamItem: { if (controller.IsCanceled) break; TDown item = (TDown)this.Protocol.ConvertTo(typeof(TDown), msg.item); future.AssignItem(item); break; } case MessageTypes.Completion: { bool isSuccess = string.IsNullOrEmpty(msg.error); if (isSuccess) { // While completion message must not contain any result, this should be future-proof if (!controller.IsCanceled && msg.result != null) { TDown result = (TDown)this.Protocol.ConvertTo(typeof(TDown), msg.result); future.AssignItem(result); } future.Finish(); } else future.Fail(new Exception(msg.error)); break; } } }; var message = new Message { type = MessageTypes.StreamInvocation, invocationId = invocationId.ToString(), target = target, arguments = args, nonblocking = false, }; SendMessage(message); if (callback != null) if (!this.invocations.TryAdd(invocationId, new InvocationDefinition { callback = callback, returnType = typeof(TDown) })) HTTPManager.Logger.Warning("HubConnection", "GetDownStreamController - invocations already contains id: " + invocationId, this.Context); return controller; } public UpStreamItemController GetUpStreamController(string target, int paramCount, bool downStream, object[] args) { Future future = new Future(); future.BeginProcess(); long invocationId = System.Threading.Interlocked.Increment(ref this.lastInvocationId); string[] streamIds = new string[paramCount]; for (int i = 0; i < paramCount; i++) streamIds[i] = System.Threading.Interlocked.Increment(ref this.lastStreamId).ToString(); var controller = new UpStreamItemController(this, invocationId, streamIds, future); Action callback = (Message msg) => { switch (msg.type) { // StreamItem message contains only one item. case MessageTypes.StreamItem: { if (controller.IsCanceled) break; TResult item = (TResult)this.Protocol.ConvertTo(typeof(TResult), msg.item); future.AssignItem(item); break; } case MessageTypes.Completion: { bool isSuccess = string.IsNullOrEmpty(msg.error); if (isSuccess) { // While completion message must not contain any result, this should be future-proof if (!controller.IsCanceled && msg.result != null) { TResult result = (TResult)this.Protocol.ConvertTo(typeof(TResult), msg.result); future.AssignItem(result); } future.Finish(); } else { var ex = new Exception(msg.error); future.Fail(ex); } break; } } }; var messageToSend = new Message { type = downStream ? MessageTypes.StreamInvocation : MessageTypes.Invocation, invocationId = invocationId.ToString(), target = target, arguments = args, streamIds = streamIds, nonblocking = false, }; SendMessage(messageToSend); if (!this.invocations.TryAdd(invocationId, new InvocationDefinition { callback = callback, returnType = typeof(TResult) })) HTTPManager.Logger.Warning("HubConnection", "GetUpStreamController - invocations already contains id: " + invocationId, this.Context); return controller; } public void On(string methodName, Action callback) { On(methodName, null, (args) => callback()); } public void On(string methodName, Action callback) { On(methodName, new Type[] { typeof(T1) }, (args) => callback((T1)args[0])); } public void On(string methodName, Action callback) { On(methodName, new Type[] { typeof(T1), typeof(T2) }, (args) => callback((T1)args[0], (T2)args[1])); } public void On(string methodName, Action callback) { On(methodName, new Type[] { typeof(T1), typeof(T2), typeof(T3) }, (args) => callback((T1)args[0], (T2)args[1], (T3)args[2])); } public void On(string methodName, Action callback) { On(methodName, new Type[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4) }, (args) => callback((T1)args[0], (T2)args[1], (T3)args[2], (T4)args[3])); } private void On(string methodName, Type[] paramTypes, Action callback) { if (this.State >= ConnectionStates.CloseInitiated) throw new Exception("Hub connection already closing or closed!"); this.subscriptions.GetOrAdd(methodName, _ => new Subscription()) .Add(paramTypes, callback); } /// /// Remove all event handlers for that subscribed with an On call. /// public void Remove(string methodName) { if (this.State >= ConnectionStates.CloseInitiated) throw new Exception("Hub connection already closing or closed!"); Subscription _; this.subscriptions.TryRemove(methodName, out _); } internal Subscription GetSubscription(string methodName) { Subscription subscribtion = null; this.subscriptions.TryGetValue(methodName, out subscribtion); return subscribtion; } internal Type GetItemType(long invocationId) { InvocationDefinition def; this.invocations.TryGetValue(invocationId, out def); return def.returnType; } List delayedMessages; internal void OnMessages(List messages) { this.lastMessageReceivedAt = DateTime.Now; if (pausedInLastFrame) { if (this.delayedMessages == null) this.delayedMessages = new List(messages.Count); foreach(var msg in messages) delayedMessages.Add(msg); messages.Clear(); } for (int messageIdx = 0; messageIdx < messages.Count; ++messageIdx) { var message = messages[messageIdx]; if (this.OnMessage != null) { try { if (!this.OnMessage(this, message)) continue; } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in OnMessage user code!", ex, this.Context); } } switch (message.type) { case MessageTypes.Invocation: { Subscription subscribtion = null; if (this.subscriptions.TryGetValue(message.target, out subscribtion)) { for (int i = 0; i < subscribtion.callbacks.Count; ++i) { var callbackDesc = subscribtion.callbacks[i]; object[] realArgs = null; try { realArgs = this.Protocol.GetRealArguments(callbackDesc.ParamTypes, message.arguments); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "OnMessages - Invocation - GetRealArguments", ex, this.Context); } try { callbackDesc.Callback.Invoke(realArgs); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "OnMessages - Invocation - Invoke", ex, this.Context); } } } break; } case MessageTypes.StreamItem: { long invocationId; if (long.TryParse(message.invocationId, out invocationId)) { InvocationDefinition def; if (this.invocations.TryGetValue(invocationId, out def) && def.callback != null) { try { def.callback(message); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "OnMessages - StreamItem - callback", ex, this.Context); } } } break; } case MessageTypes.Completion: { long invocationId; if (long.TryParse(message.invocationId, out invocationId)) { InvocationDefinition def; if (this.invocations.TryRemove(invocationId, out def) && def.callback != null) { try { def.callback(message); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "OnMessages - Completion - callback", ex, this.Context); } } } break; } case MessageTypes.Ping: // Send back an answer SendMessage(new Message() { type = MessageTypes.Ping }); break; case MessageTypes.Close: SetState(ConnectionStates.Closed, message.error, message.allowReconnect); if (this.Transport != null) this.Transport.StartClose(); return; } } } private void Transport_OnStateChanged(TransportStates oldState, TransportStates newState) { HTTPManager.Logger.Verbose("HubConnection", string.Format("Transport_OnStateChanged - oldState: {0} newState: {1}", oldState.ToString(), newState.ToString()), this.Context); if (this.State == ConnectionStates.Closed) { HTTPManager.Logger.Verbose("HubConnection", "Transport_OnStateChanged - already closed!", this.Context); return; } switch (newState) { case TransportStates.Connected: try { if (this.OnTransportEvent != null) this.OnTransportEvent(this, this.Transport, TransportEvents.Connected); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in OnTransportEvent user code!", ex, this.Context); } SetState(ConnectionStates.Connected, null, this.defaultReconnect); break; case TransportStates.Failed: if (this.State == ConnectionStates.Negotiating && !HTTPManager.IsQuitting) { try { if (this.OnTransportEvent != null) this.OnTransportEvent(this, this.Transport, TransportEvents.FailedToConnect); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in OnTransportEvent user code!", ex, this.Context); } this.triedoutTransports.Add(this.Transport.TransportType); var nextTransport = GetNextTransportToTry(); if (nextTransport == null) { var reason = this.Transport.ErrorReason; this.Transport = null; SetState(ConnectionStates.Closed, reason, this.defaultReconnect); } else ConnectImpl(nextTransport.Value); } else { try { if (this.OnTransportEvent != null) this.OnTransportEvent(this, this.Transport, TransportEvents.ClosedWithError); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in OnTransportEvent user code!", ex, this.Context); } var reason = this.Transport.ErrorReason; this.Transport = null; SetState(ConnectionStates.Closed, HTTPManager.IsQuitting ? null : reason, this.defaultReconnect); } break; case TransportStates.Closed: { try { if (this.OnTransportEvent != null) this.OnTransportEvent(this, this.Transport, TransportEvents.Closed); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in OnTransportEvent user code!", ex, this.Context); } // Check wheter we have any delayed message and a Close message among them. If there's one, delay the SetState(Close) too. if (this.delayedMessages == null || this.delayedMessages.FindLast(dm => dm.type == MessageTypes.Close).type != MessageTypes.Close) SetState(ConnectionStates.Closed, null, this.defaultReconnect); } break; } } private TransportTypes? GetNextTransportToTry() { foreach (TransportTypes val in Enum.GetValues(typeof(TransportTypes))) if (!this.triedoutTransports.Contains(val) && IsTransportSupported(val.ToString())) return val; return null; } bool defaultReconnect = true; private void SetState(ConnectionStates state, string errorReason, bool allowReconnect) { HTTPManager.Logger.Information("HubConnection", string.Format("SetState - from State: '{0}' to State: '{1}', errorReason: '{2}', allowReconnect: {3}, isQuitting: {4}", this.State, state, errorReason, allowReconnect, HTTPManager.IsQuitting), this.Context); if (this.State == state) return; var previousState = this.State; this.State = state; switch (state) { case ConnectionStates.Initial: case ConnectionStates.Authenticating: case ConnectionStates.Negotiating: case ConnectionStates.CloseInitiated: break; case ConnectionStates.Reconnecting: break; case ConnectionStates.Connected: // If reconnectStartTime isn't its default value we reconnected if (this.reconnectStartTime != DateTime.MinValue) { try { if (this.OnReconnected != null) this.OnReconnected(this); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "OnReconnected", ex, this.Context); } } else { try { if (this.OnConnected != null) this.OnConnected(this); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in OnConnected user code!", ex, this.Context); } } this.lastMessageSentAt = DateTime.Now; this.lastMessageReceivedAt = DateTime.Now; // Clean up reconnect related fields this.currentContext = new RetryContext(); this.reconnectStartTime = DateTime.MinValue; this.reconnectAt = DateTime.MinValue; HTTPUpdateDelegator.OnApplicationForegroundStateChanged -= this.OnApplicationForegroundStateChanged; HTTPUpdateDelegator.OnApplicationForegroundStateChanged += this.OnApplicationForegroundStateChanged; break; case ConnectionStates.Closed: // Go through all invocations and cancel them. var error = new Message(); error.type = MessageTypes.Close; error.error = errorReason; foreach (var kvp in this.invocations) { try { kvp.Value.callback(error); } catch { } } this.invocations.Clear(); // No errorReason? It's an expected closure. if (errorReason == null && (!allowReconnect || HTTPManager.IsQuitting)) { if (this.OnClosed != null) { try { this.OnClosed(this); } catch(Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in OnClosed user code!", ex, this.Context); } } HTTPManager.Logger.Information("HubConnection", "Cleaning up", this.Context); this.delayedMessages?.Clear(); HTTPManager.Heartbeats.Unsubscribe(this); this.rwLock?.Dispose(); this.rwLock = null; HTTPUpdateDelegator.OnApplicationForegroundStateChanged -= this.OnApplicationForegroundStateChanged; } else { // If possible, try to reconnect if (allowReconnect && this.ReconnectPolicy != null && (previousState == ConnectionStates.Connected || this.reconnectStartTime != DateTime.MinValue)) { // It's the first attempt after a successful connection if (this.reconnectStartTime == DateTime.MinValue) { this.connectionStartedAt = this.reconnectStartTime = DateTime.Now; try { if (this.OnReconnecting != null) this.OnReconnecting(this, errorReason); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "SetState - ConnectionStates.Reconnecting", ex, this.Context); } } RetryContext context = new RetryContext { ElapsedTime = DateTime.Now - this.reconnectStartTime, PreviousRetryCount = this.currentContext.PreviousRetryCount, RetryReason = errorReason }; TimeSpan? nextAttempt = null; try { nextAttempt = this.ReconnectPolicy.GetNextRetryDelay(context); } catch (Exception ex) { HTTPManager.Logger.Exception("HubConnection", "ReconnectPolicy.GetNextRetryDelay", ex, this.Context); } // No more reconnect attempt, we are closing if (nextAttempt == null) { HTTPManager.Logger.Warning("HubConnection", "No more reconnect attempt!", this.Context); // Clean up everything this.currentContext = new RetryContext(); this.reconnectStartTime = DateTime.MinValue; this.reconnectAt = DateTime.MinValue; } else { HTTPManager.Logger.Information("HubConnection", "Next reconnect attempt after " + nextAttempt.Value.ToString(), this.Context); this.currentContext = context; this.currentContext.PreviousRetryCount += 1; this.reconnectAt = DateTime.Now + nextAttempt.Value; this.SetState(ConnectionStates.Reconnecting, null, this.defaultReconnect); return; } } if (this.OnError != null) { try { this.OnError(this, errorReason); } catch(Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in OnError user code!", ex, this.Context); } } } break; } } private void OnApplicationForegroundStateChanged(bool isPaused) { pausedInLastFrame = !isPaused; HTTPManager.Logger.Information("HubConnection", $"OnApplicationForegroundStateChanged isPaused: {isPaused} pausedInLastFrame: {pausedInLastFrame}", this.Context); } void BestHTTP.Extensions.IHeartbeat.OnHeartbeatUpdate(TimeSpan dif) { switch (this.State) { case ConnectionStates.Negotiating: case ConnectionStates.Authenticating: case ConnectionStates.Redirected: if (DateTime.Now >= this.connectionStartedAt + this.Options.ConnectTimeout) { if (this.AuthenticationProvider != null) { this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded; this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed; try { this.AuthenticationProvider.Cancel(); } catch(Exception ex) { HTTPManager.Logger.Exception("HubConnection", "Exception in AuthenticationProvider.Cancel !", ex, this.Context); } } if (this.Transport != null) { this.Transport.OnStateChanged -= Transport_OnStateChanged; this.Transport.StartClose(); } SetState(ConnectionStates.Closed, string.Format("Couldn't connect in the given time({0})!", this.Options.ConnectTimeout), this.defaultReconnect); } break; case ConnectionStates.Connected: if (this.delayedMessages?.Count > 0) { pausedInLastFrame = false; try { // if there's any Close message clear any other one. int idx = this.delayedMessages.FindLastIndex(dm => dm.type == MessageTypes.Close); if (idx > 0) this.delayedMessages.RemoveRange(0, idx); OnMessages(this.delayedMessages); } finally { this.delayedMessages.Clear(); } } // Still connected? Check pinging. if (this.State == ConnectionStates.Connected) { if (this.Options.PingInterval != TimeSpan.Zero && DateTime.Now - this.lastMessageReceivedAt >= this.Options.PingTimeoutInterval) { // The transport itself can be in a failure state or in a completely valid one, so while we do not want to receive anything from it, we have to try to close it if (this.Transport != null) { this.Transport.OnStateChanged -= Transport_OnStateChanged; this.Transport.StartClose(); } SetState(ConnectionStates.Closed, string.Format("PingInterval set to '{0}' and no message is received since '{1}'. PingTimeoutInterval: '{2}'", this.Options.PingInterval, this.lastMessageReceivedAt, this.Options.PingTimeoutInterval), this.defaultReconnect); } else if (this.Options.PingInterval != TimeSpan.Zero && DateTime.Now - this.lastMessageSentAt >= this.Options.PingInterval) SendMessage(new Message() { type = MessageTypes.Ping }); } break; case ConnectionStates.Reconnecting: if (this.reconnectAt != DateTime.MinValue && DateTime.Now >= this.reconnectAt) { this.delayedMessages?.Clear(); this.connectionStartedAt = DateTime.Now; this.reconnectAt = DateTime.MinValue; this.triedoutTransports.Clear(); this.StartConnect(); } break; } } } } #endif