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; } }
/// <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); } } } }
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."); }