private void Socket_OnFatalDisconnection(object sender, GatewayCloseCode e) { if (isDisposed) { return; } log.LogVerbose("Fatal disconnection occured, setting state to Disconnected."); state = GatewayState.Disconnected; (string message, ShardFailureReason reason) = GatewayCloseCodeToReason(e); gatewayFailureData = new GatewayFailureData(message, reason, null); handshakeCompleteEvent.Set(); OnFailure?.Invoke(this, gatewayFailureData); }
public GatewayHandshakeException(GatewayFailureData failureData) { FailureData = failureData; }
/// <exception cref="OperationCanceledException"></exception> async Task ConnectLoop(bool resume, CancellationToken cancellationToken) { // Keep track of whether this is a resume or new session so // we can respond to the HELLO payload appropriately. isConnectionResuming = resume; log.LogVerbose($"[ConnectLoop] resume = {resume}"); handshakeCompleteEvent.Reset(); handshakeCompleteCancellationSource = new CancellationTokenSource(); while (!cancellationToken.IsCancellationRequested) { // Ensure previous socket has been closed if (socket != null) { UnsubscribeSocketEvents(); if (resume) { // Store previous sequence lastSequence = socket.Sequence; } if (socket.CanBeDisconnected) { log.LogVerbose("[ConnectLoop] Disconnecting previous socket..."); // If for some reason the socket cannot be disconnected gracefully, // DisconnectAsync will abort the socket after 5s. if (resume) { // Make sure to disconnect with a non 1000 code to ensure Discord doesn't // force us to make a new session since we are resuming. await socket.DisconnectAsync(DiscordClientWebSocket.INTERNAL_CLIENT_ERROR, "Reconnecting to resume...", cancellationToken).ConfigureAwait(false); } else { await socket.DisconnectAsync(WebSocketCloseStatus.NormalClosure, "Starting new session...", cancellationToken).ConfigureAwait(false); } } socket.Dispose(); } if (!resume) { // If not resuming, reset gateway session state. lastSequence = 0; } // Create a new socket socket = new GatewaySocket($"GatewaySocket#{shard.Id}", lastSequence, outboundPayloadRateLimiter, gameStatusUpdateRateLimiter, identifyRateLimiter); socket.OnHello = async() => { if (isDisposed) { return; } if (isConnectionResuming) { // Resume await socket.SendResumePayload(botToken, sessionId, lastSequence); } else { // Identify await socket.SendIdentifyPayload(botToken, lastShardStartConfig.GatewayLargeThreshold, shard.Id, totalShards); } }; SubscribeSocketEvents(); // Get the gateway URL DiscoreLocalStorage localStorage; string gatewayUrl; try { localStorage = await DiscoreLocalStorage.GetInstanceAsync().ConfigureAwait(false); gatewayUrl = await localStorage.GetGatewayUrlAsync(http).ConfigureAwait(false); } catch (Exception ex) when(ex is DiscordHttpApiException || ex is HttpRequestException) { log.LogError($"[ConnectLoop:GetGatewayUrl] {ex}"); log.LogError("[ConnectLoop] No gateway URL to connect with, trying again in 10s..."); await Task.Delay(10 * 1000, cancellationToken).ConfigureAwait(false); continue; } catch (Exception ex) when(ex is IOException || ex is UnauthorizedAccessException) { log.LogError(ex); log.LogError("IO Error occured while getting/storing gateway URL, setting state to Disconnected."); state = GatewayState.Disconnected; gatewayFailureData = new GatewayFailureData( "Failed to retrieve/store the Gateway URL because of an IO error.", ShardFailureReason.IOError, ex); handshakeCompleteEvent.Set(); OnFailure?.Invoke(this, gatewayFailureData); break; } catch (Exception ex) { // This should never-ever happen, but we need to handle it just in-case. log.LogError(ex); log.LogError("Uncaught error occured while getting/storing gateway URL, setting state to Disconnected."); state = GatewayState.Disconnected; gatewayFailureData = new GatewayFailureData( "Failed to retrieve/store the Gateway URL because of an unknown error.", ShardFailureReason.Unknown, ex); handshakeCompleteEvent.Set(); OnFailure?.Invoke(this, gatewayFailureData); break; } log.LogVerbose($"[ConnectLoop] gatewayUrl = {gatewayUrl}"); // Wait if necessary if (nextConnectionDelayMs > 0) { log.LogVerbose($"[ConnectLoop] Waiting {nextConnectionDelayMs}ms before connecting socket..."); await Task.Delay(nextConnectionDelayMs, cancellationToken).ConfigureAwait(false); nextConnectionDelayMs = 0; } try { // Attempt to connect await socket.ConnectAsync(new Uri($"{gatewayUrl}?v={GATEWAY_VERSION}&encoding=json"), cancellationToken) .ConfigureAwait(false); // At this point the socket has successfully connected log.LogVerbose("[ConnectLoop] Socket connected successfully."); break; } catch (WebSocketException wsex) { UnsubscribeSocketEvents(); // Failed to connect log.LogError("[ConnectLoop] Failed to connect: " + $"{wsex.WebSocketErrorCode} ({(int)wsex.WebSocketErrorCode}), {wsex.Message}"); try { // Invalidate the cached URL since we failed to connect the socket await localStorage.InvalidateGatewayUrlAsync(); } catch (Exception ex) { log.LogError(ex); log.LogVerbose("IO Error occured while invalidating gateway URL, setting state to Disconnected."); state = GatewayState.Disconnected; gatewayFailureData = new GatewayFailureData( "Failed to update the Gateway URL because of an IO error.", ShardFailureReason.IOError, ex); handshakeCompleteEvent.Set(); OnFailure?.Invoke(this, gatewayFailureData); break; } // Wait 5s then retry log.LogVerbose("[ConnectLoop] Waiting 5s before retrying..."); await Task.Delay(5000, cancellationToken).ConfigureAwait(false); } } // If the token is cancelled between the socket successfully connecting and the loop exiting, // do not throw an exception because the connection did technically complete before the cancel. if (socket == null || !socket.IsConnected) { // If the loop stopped from the token being cancelled, ensure an exception is still thrown. cancellationToken.ThrowIfCancellationRequested(); } // If this is an automatic reconnection, fire OnReconnected event if (state == GatewayState.Connected) { if (resume) { log.LogInfo("[ConnectLoop:Reconnection] Successfully resumed."); } else { log.LogInfo("[ConnectLoop:Reconnection] Successfully created new session."); } OnReconnected?.Invoke(this, new GatewayReconnectedEventArgs(!resume)); } }
/// <exception cref="GatewayHandshakeException"></exception> /// <exception cref="InvalidOperationException"></exception> /// <exception cref="ObjectDisposedException"></exception> public async Task ConnectAsync(ShardStartConfig config, CancellationToken cancellationToken) { if (isDisposed) { throw new ObjectDisposedException(GetType().FullName); } if (state == GatewayState.Connected) { throw new InvalidOperationException("The gateway is already connected!"); } if (state == GatewayState.Connecting) { throw new InvalidOperationException("The gateway is already connecting!"); } // Begin connecting state = GatewayState.Connecting; lastShardStartConfig = config; gatewayFailureData = null; handshakeCompleteEvent.Reset(); connectTask = ConnectLoop(false, cancellationToken); try { // Connect socket await connectTask.ConfigureAwait(false); try { // Wait for the handshake to complete await handshakeCompleteEvent.WaitAsync(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { // Since this was cancelled after the socket connected, // we need to do a full disconnect. await FullDisconnect(); throw; } // Check for errors if (gatewayFailureData != null) { throw new GatewayHandshakeException(gatewayFailureData); } // Connection successful log.LogVerbose("[ConnectAsync] Setting state to Connected."); state = GatewayState.Connected; } catch { // Reset to disconnected if cancelled or failed log.LogVerbose("[ConnectAsync] Setting state to Disconnected."); state = GatewayState.Disconnected; throw; } }