Esempio n. 1
0
        protected override void OnCloseReceived(WebSocketCloseStatus closeStatus, string closeDescription)
        {
            if (closeStatus == WebSocketCloseStatus.NormalClosure)
            {
                return;
            }

            VoiceCloseCode voiceCloseCode = (VoiceCloseCode)closeStatus;

            switch (voiceCloseCode)
            {
            case VoiceCloseCode.Disconnected:
            case VoiceCloseCode.VoiceServerCrashed:
                heartbeatCancellationSource?.Cancel();
                OnResumeRequested?.Invoke(this, EventArgs.Empty);
                break;

            case VoiceCloseCode.InvalidSession:
            case VoiceCloseCode.SessionTimeout:
                heartbeatCancellationSource?.Cancel();
                OnNewSessionRequested?.Invoke(this, EventArgs.Empty);
                break;

            default:
                if ((int)voiceCloseCode >= 4000)
                {
                    log.LogVerbose($"Fatal close code: {voiceCloseCode} ({(int)voiceCloseCode}), {closeDescription}");
                }
                else
                {
                    log.LogVerbose($"Fatal close code: {closeStatus} ({(int)closeStatus}), {closeDescription}");
                }

                OnUnexpectedClose?.Invoke(this, EventArgs.Empty);
                break;
            }
        }
Esempio n. 2
0
        /// <summary>
        /// Continuously retries to call the specified callback (which should only be a payload send).
        /// <para>
        /// Retries if the callback throws a InvalidOperationException or DiscordWebSocketException.
        /// Also waits for the gateway connection to be ready before calling the callback.
        /// </para>
        /// </summary>
        /// <exception cref="OperationCanceledException">
        /// Thrown if the cancellation token is cancelled or the gateway connection is closed while sending.
        /// </exception>
        async Task RepeatTrySendPayload(CancellationToken ct, string opName, Func <Task> callback)
        {
            CancellationTokenSource cts = new CancellationTokenSource();

            // This can be cancelled either by the caller, or the gateway disconnecting.
            using (ct.Register(() => cts.Cancel()))
                using (handshakeCompleteCancellationSource.Token.Register(() => cts.Cancel()))
                {
                    while (true)
                    {
                        cts.Token.ThrowIfCancellationRequested();

                        if (state != GatewayState.Connected)
                        {
                            // Cancel if the gateway connection is closed from the outside.
                            throw new OperationCanceledException("The gateway connection was closed.");
                        }

                        bool waitingForReady = false;
                        if (!handshakeCompleteEvent.IsSet)
                        {
                            waitingForReady = true;
                            log.LogVerbose($"[{opName}:RepeatTrySendPayload] Awaiting completed gateway handshake...");
                        }

                        // Wait until the gateway connection is ready
                        await handshakeCompleteEvent.WaitAsync(cts.Token).ConfigureAwait(false);

                        if (waitingForReady)
                        {
                            log.LogVerbose($"[{opName}:RepeatTrySendPayload] Gateway is now fully connected after waiting.");
                        }

                        try
                        {
                            // Try the callback
                            await callback().ConfigureAwait(false);

                            // Call succeeded
                            break;
                        }
                        catch (InvalidOperationException)
                        {
                            log.LogVerbose($"[{opName}:RepeatTrySendPayload] InvalidOperationException, retrying...");

                            // The socket was closed between waiting for the socket to open
                            // and sending the payload. Shouldn't ever happen, give the socket
                            // some time to flip back to disconnected.
                            await Task.Delay(500, cts.Token).ConfigureAwait(false);
                        }
                        catch (DiscordWebSocketException dwex)
                        {
                            log.LogVerbose($"[{opName}:RepeatTrySendPayload] DiscordWebSocketException: " +
                                           $"{dwex.Error} = {dwex.Message}, retrying...");

                            // Payload failed to send because the socket blew up,
                            // just retry after giving the socket some time to flip to
                            // a disconencted state.
                            await Task.Delay(500, cts.Token).ConfigureAwait(false);
                        }
                    }
                }
        }
Esempio n. 3
0
        async Task HeartbeatLoop()
        {
            // Default to true for the first heartbeat payload we send.
            receivedHeartbeatAck = true;

            log.LogVerbose("[HeartbeatLoop] Begin.");

            while (State == WebSocketState.Open && !heartbeatCancellationSource.IsCancellationRequested)
            {
                if (receivedHeartbeatAck)
                {
                    receivedHeartbeatAck = false;

                    try
                    {
                        // Send heartbeat
                        await SendHeartbeatPayload().ConfigureAwait(false);
                    }
                    catch (InvalidOperationException)
                    {
                        // Socket was closed between the loop check and sending the heartbeat
                        break;
                    }
                    catch (DiscordWebSocketException dwex)
                    {
                        // Expected to be the socket closing while sending a heartbeat
                        if (dwex.Error != DiscordWebSocketError.ConnectionClosed)
                        {
                            // Unexpected errors may not be the socket closing/aborting, so just log and loop around.
                            log.LogError("[HeartbeatLoop] Unexpected error occured while sending a heartbeat: " +
                                         $"code = {dwex.Error}, error = {dwex}");
                        }
                        else
                        {
                            break;
                        }
                    }

                    try
                    {
                        // Wait heartbeat interval
                        await Task.Delay(heartbeatInterval, heartbeatCancellationSource.Token)
                        .ConfigureAwait(false);
                    }
                    catch (ObjectDisposedException)
                    {
                        // GatewaySocket was disposed between sending a heartbeat payload and beginning to wait
                        break;
                    }
                    catch (OperationCanceledException)
                    {
                        // Socket is disconnecting
                        break;
                    }
                }
                else
                {
                    // Gateway connection has timed out
                    log.LogInfo("Gateway connection timed out (did not receive ack for last heartbeat).");

                    // Notify that this connection needs to be resumed
                    OnReconnectionRequired?.Invoke(this, new ReconnectionEventArgs(false));

                    break;
                }
            }

            log.LogVerbose($"[HeartbeatLoop] Done. isDisposed = {isDisposed}");
        }
        protected abstract void OnClosedPrematurely(); // Unsuccessful close

        /// <param name="cancellationToken">Token that when cancelled will abort the entire socket.</param>
        /// <exception cref="ArgumentException">Thrown if <paramref name="uri"/> does not start with ws:// or wss://.</exception>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="uri"/> is null.</exception>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the socket attempts to start after a first time. A WebSocket instance
        /// can only be used for one connection attempt.
        /// </exception>
        /// <exception cref="OperationCanceledException"></exception>
        /// <exception cref="ObjectDisposedException">Thrown if this socket has already been disposed.</exception>
        /// <exception cref="WebSocketException">Thrown if the socket fails to connect.</exception>
        public virtual async Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
        {
            if (uri == null)
            {
                throw new ArgumentNullException(nameof(uri));
            }

            // Attempt to connect
            log.LogVerbose($"Connecting to {uri}...");
            await socket.ConnectAsync(uri, cancellationToken).ConfigureAwait(false);

            // Shouldn't ever happen, but just in case
            if (socket.State != WebSocketState.Open)
            {
                log.LogWarning($"Socket.ConnectAsync succeeded but the state is {socket.State}!");
                throw new WebSocketException(WebSocketError.Faulted, "Failed to connect. No other information is available.");
            }

            // Connection successful (exception would be thrown otherwise)
            abortCancellationSource = new CancellationTokenSource();

            sendLock = new AsyncLock();

            // Start receive task
            receiveTask = ReceiveLoop();

            log.LogVerbose("Successfully connected.");
        }