Example #1
0
        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;
 }
Example #3
0
        /// <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));
            }
        }
Example #4
0
        /// <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;
            }
        }