/// <summary> /// Warning: Do not call from the context of the connect loop! A deadlock will occur! /// </summary> async Task FullDisconnect() { log.LogVerbose("Disconnecting..."); state = GatewayState.Disconnected; handshakeCompleteCancellationSource.Cancel(); if (connectTask != null) { // Cancel any automatic reconnection connectTaskCancellationSource?.Cancel(); // Wait for the automatic reconnection to end try { await connectTask.ConfigureAwait(false); } catch (OperationCanceledException) { /* Expected to happen. */ } catch (Exception ex) { // Should never happen, but there isn't anything we can do here. log.LogError($"[DisconnectAsync] Uncaught exception found in connect task: {ex}"); } } // Disconnect the socket if needed if (socket.CanBeDisconnected) { await socket.DisconnectAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting...", CancellationToken.None) .ConfigureAwait(false); } log.LogVerbose("Disconnected."); }
protected override void OnCloseReceived(WebSocketCloseStatus closeStatus, string closeDescription) { // If we initiated a disconnect, this is just the remote end's acknowledgment // and we should not start reconnecting if (areWeDisconnecting) { return; } GatewayCloseCode code = (GatewayCloseCode)closeStatus; switch (code) { case GatewayCloseCode.InvalidShard: case GatewayCloseCode.AuthenticationFailed: case GatewayCloseCode.ShardingRequired: // Not safe to reconnect log.LogError($"[{code} ({(int)code})] Unsafe to continue, NOT reconnecting gateway."); OnFatalDisconnection?.Invoke(this, code); break; case GatewayCloseCode.InvalidSeq: case GatewayCloseCode.InvalidSession: case GatewayCloseCode.SessionTimeout: case GatewayCloseCode.UnknownError: // Safe to reconnect, but needs a new session. OnReconnectionRequired?.Invoke(this, new ReconnectionEventArgs(true)); break; case GatewayCloseCode.NotAuthenticated: // This really should never happen, but will require a new session. log.LogWarning("[NotAuthenticated] Sent gateway payload before we identified!"); OnReconnectionRequired?.Invoke(this, new ReconnectionEventArgs(true)); break; case GatewayCloseCode.RateLimited: // Doesn't require a new session, but we need to wait a bit. log.LogError("Gateway is being rate limited!"); // Error level because we have code that should prevent this. OnReconnectionRequired?.Invoke(this, new ReconnectionEventArgs(false, 5000)); break; default: // Safe to just resume OnReconnectionRequired?.Invoke(this, new ReconnectionEventArgs(false)); break; } }
public async Task HeartbeatLoop(int heartbeatInterval) { receivedHeartbeatAck = true; while (State == WebSocketState.Open && !heartbeatCancellationSource.IsCancellationRequested) { if (!receivedHeartbeatAck) { log.LogWarning("[HeartbeatLoop] Connection timed out (did not receive ack for last heartbeat)."); OnTimedOut?.Invoke(this, EventArgs.Empty); break; } try { receivedHeartbeatAck = false; // 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: {dwex}"); } else { break; } } try { // Wait heartbeat interval await Task.Delay(heartbeatInterval, heartbeatCancellationSource.Token) .ConfigureAwait(false); } catch (ObjectDisposedException) { // VoiceWebSocket was disposed between sending a heartbeat payload and beginning to wait break; } catch (OperationCanceledException) { // Socket is disconnecting break; } } }
public void Shutdown() { try { socket.Shutdown(SocketShutdown.Both); } catch (SocketException ex) { // If this occurs then the socket is already borked, // technically shouldn't happen though. log.LogError($"[Shutdown] Unexpected error: code = {ex.SocketErrorCode}, error = {ex}"); } }
/// <summary> /// This task will deadlock (for 5s then abort the socket) if called from the same thread as the receive loop! /// </summary> /// <param name="cancellationToken">Token that when cancelled will abort the entire socket.</param> /// <exception cref="OperationCanceledException"></exception> /// <exception cref="WebSocketException">Thrown if the socket is not in a valid state to be closed.</exception> public virtual Task DisconnectAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) { // Since this operation can take up to 5s and is synchronous code, wrap it in a task. return(Task.Run(() => { log.LogVerbose($"Disconnecting with code {closeStatus} ({(int)closeStatus})..."); // Since the state check will be eaten by the code below, // and we cannot reliably check for this below, check if // the socket state is valid for a close, and throw the // same exception CloseAsync would have if not valid. if (socket.State != WebSocketState.Open && socket.State != WebSocketState.CloseReceived && socket.State != WebSocketState.CloseSent) { throw new WebSocketException(WebSocketError.InvalidState, $"Socket is in an invalid state: {socket.State}"); } // CloseAsync will send the close message and then wait until // a close frame has been received. Once nothing is holding // the receive lock, CloseAsync will then try and receive // data until it comes to a close frame, or another receive // call gets it. Task closeTask = socket.CloseAsync(closeStatus, statusDescription, cancellationToken); try { // Give the socket 5s to gracefully disconnect. if (!Task.WaitAll(new Task[] { closeTask, receiveTask }, 5000, cancellationToken)) { // Socket did not gracefully disconnect in the given time, // so abort the socket and move on. log.LogWarning("Socket failed to disconnect after 5s, aborting..."); abortCancellationSource.Cancel(); } } catch (OperationCanceledException) { // Caller cancelled, socket has been aborted. log.LogVerbose("Disconnect cancelled by caller, socket has been aborted."); throw; } catch (AggregateException aex) { foreach (Exception ex in aex.InnerExceptions) { if (ex is WebSocketException wsex) { if (wsex.WebSocketErrorCode == WebSocketError.InvalidState) { // The socket being aborted is normal. continue; } else { log.LogError($"Uncaught exception found while disconnecting: code = {wsex.WebSocketErrorCode}, error = {ex}"); } } else { log.LogError($"Uncaught exception found while disconnecting: {ex}"); } } } log.LogVerbose("Disconnected."); })); }