diff --git a/src/Discord.Net/API/DiscordVoiceAPIClient.cs b/src/Discord.Net/API/DiscordVoiceAPIClient.cs index e64d74c10..ddb8f2b6b 100644 --- a/src/Discord.Net/API/DiscordVoiceAPIClient.cs +++ b/src/Discord.Net/API/DiscordVoiceAPIClient.cs @@ -28,24 +28,19 @@ namespace Discord.Audio private readonly AsyncEvent> _receivedEvent = new AsyncEvent>(); public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - - private readonly ulong _userId; - private readonly string _token; + private readonly JsonSerializer _serializer; private readonly IWebSocketClient _gatewayClient; private readonly SemaphoreSlim _connectionLock; private CancellationTokenSource _connectCancelToken; + private bool _isDisposed; - public ulong GuildId { get; } - public string SessionId { get; } + public ulong GuildId { get; } public ConnectionState ConnectionState { get; private set; } - internal DiscordVoiceAPIClient(ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) + internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, JsonSerializer serializer = null) { GuildId = guildId; - _userId = userId; - SessionId = sessionId; - _token = token; _connectionLock = new SemaphoreSlim(1, 1); _gatewayClient = webSocketProvider(); @@ -78,6 +73,19 @@ namespace Discord.Audio _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; } + void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _connectCancelToken?.Dispose(); + (_gatewayClient as IDisposable)?.Dispose(); + } + _isDisposed = true; + } + } + public void Dispose() => Dispose(true); public Task SendAsync(VoiceOpCode opCode, object payload, RequestOptions options = null) { @@ -105,16 +113,16 @@ namespace Discord.Audio }); } - public async Task ConnectAsync(string url) + public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - await ConnectInternalAsync(url).ConfigureAwait(false); + await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task ConnectInternalAsync(string url) + private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) { ConnectionState = ConnectionState.Connecting; try @@ -123,7 +131,7 @@ namespace Discord.Audio _gatewayClient.SetCancelToken(_connectCancelToken.Token); await _gatewayClient.ConnectAsync(url).ConfigureAwait(false); - await SendIdentityAsync(GuildId, _userId, SessionId, _token).ConfigureAwait(false); + await SendIdentityAsync(GuildId, userId, sessionId, token).ConfigureAwait(false); ConnectionState = ConnectionState.Connected; } diff --git a/src/Discord.Net/Audio/AudioClient.cs b/src/Discord.Net/Audio/AudioClient.cs index 854c3219b..b59f3d267 100644 --- a/src/Discord.Net/Audio/AudioClient.cs +++ b/src/Discord.Net/Audio/AudioClient.cs @@ -1,7 +1,6 @@ using Discord.API.Voice; using Discord.Logging; using Discord.Net.Converters; -using Discord.Net.WebSockets; using Newtonsoft.Json; using System; using System.Threading; @@ -9,7 +8,7 @@ using System.Threading.Tasks; namespace Discord.Audio { - internal class AudioClient : IAudioClient + internal class AudioClient : IAudioClient, IDisposable { public event Func Connected { @@ -17,12 +16,12 @@ namespace Discord.Audio remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); public event Func LatencyUpdated { add { _latencyUpdatedEvent.Add(value); } @@ -34,28 +33,30 @@ namespace Discord.Audio #if BENCHMARK private readonly ILogger _benchmarkLogger; #endif - private readonly JsonSerializer _serializer; internal readonly SemaphoreSlim _connectionLock; + private readonly JsonSerializer _serializer; private TaskCompletionSource _connectTask; private CancellationTokenSource _cancelToken; - private Task _heartbeatTask, _reconnectTask; + private Task _heartbeatTask; private long _heartbeatTime; - private bool _isReconnecting; private string _url; + private bool _isDisposed; - private DiscordSocketClient Discord { get; } + public CachedGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } public ConnectionState ConnectionState { get; private set; } public int Latency { get; private set; } + private DiscordSocketClient Discord => Guild.Discord; + /// Creates a new REST/WebSocket discord client. - internal AudioClient(DiscordSocketClient discord, ulong guildId, ulong userId, string sessionId, string token, WebSocketProvider webSocketProvider, ILogManager logManager) + internal AudioClient(CachedGuild guild) { - Discord = discord; + Guild = guild; - _webSocketLogger = logManager.CreateLogger("Audio"); - _udpLogger = logManager.CreateLogger("AudioUDP"); + _webSocketLogger = Discord.LogManager.CreateLogger("Audio"); + _udpLogger = Discord.LogManager.CreateLogger("AudioUDP"); #if BENCHMARK _benchmarkLogger = logManager.CreateLogger("Benchmark"); #endif @@ -69,38 +70,34 @@ namespace Discord.Audio e.ErrorContext.Handled = true; }; - ApiClient = new DiscordVoiceAPIClient(guildId, userId, sessionId, token, webSocketProvider); + ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider); ApiClient.SentGatewayMessage += async opCode => await _webSocketLogger.DebugAsync($"Sent {(VoiceOpCode)opCode}").ConfigureAwait(false); ApiClient.ReceivedEvent += ProcessMessageAsync; ApiClient.Disconnected += async ex => { if (ex != null) - { await _webSocketLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); - } else await _webSocketLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); }; } /// - public async Task ConnectAsync(string url) + public async Task ConnectAsync(string url, ulong userId, string sessionId, string token) { await _connectionLock.WaitAsync().ConfigureAwait(false); try { - _isReconnecting = false; - await ConnectInternalAsync(url).ConfigureAwait(false); + await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task ConnectInternalAsync(string url) + private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token) { var state = ConnectionState; if (state == ConnectionState.Connecting || state == ConnectionState.Connected) - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); ConnectionState = ConnectionState.Connecting; await _webSocketLogger.InfoAsync("Connecting").ConfigureAwait(false); @@ -109,7 +106,7 @@ namespace Discord.Audio _url = url; _connectTask = new TaskCompletionSource(); _cancelToken = new CancellationTokenSource(); - await ApiClient.ConnectAsync(url).ConfigureAwait(false); + await ApiClient.ConnectAsync(url, userId, sessionId, token).ConfigureAwait(false); await _connectedEvent.InvokeAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); @@ -119,7 +116,7 @@ namespace Discord.Audio } catch (Exception) { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); throw; } } @@ -129,12 +126,20 @@ namespace Discord.Audio await _connectionLock.WaitAsync().ConfigureAwait(false); try { - _isReconnecting = false; - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task DisconnectAsync(Exception ex) + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync(ex).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task DisconnectInternalAsync() + private async Task DisconnectInternalAsync(Exception ex) { if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; @@ -155,61 +160,7 @@ namespace Discord.Audio ConnectionState = ConnectionState.Disconnected; await _webSocketLogger.InfoAsync("Disconnected").ConfigureAwait(false); - await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); - } - - private async Task StartReconnectAsync() - { - //TODO: Is this thread-safe? - if (_reconnectTask != null) return; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - if (_reconnectTask != null) return; - _isReconnecting = true; - _reconnectTask = ReconnectInternalAsync(); - } - finally { _connectionLock.Release(); } - } - private async Task ReconnectInternalAsync() - { - try - { - int nextReconnectDelay = 1000; - while (_isReconnecting) - { - try - { - await Task.Delay(nextReconnectDelay).ConfigureAwait(false); - nextReconnectDelay *= 2; - if (nextReconnectDelay > 30000) - nextReconnectDelay = 30000; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - await ConnectInternalAsync(_url).ConfigureAwait(false); - } - finally { _connectionLock.Release(); } - return; - } - catch (Exception ex) - { - await _webSocketLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); - } - } - } - finally - { - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - _isReconnecting = false; - _reconnectTask = null; - } - finally { _connectionLock.Release(); } - } + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); } private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) @@ -285,7 +236,7 @@ namespace Discord.Audio if (ConnectionState == ConnectionState.Connected) { await _webSocketLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); + await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); return; } } @@ -296,5 +247,14 @@ namespace Discord.Audio } catch (OperationCanceledException) { } } + + internal virtual void Dispose(bool disposing) + { + if (!_isDisposed) + _isDisposed = true; + ApiClient.Dispose(); + } + /// + public void Dispose() => Dispose(true); } } diff --git a/src/Discord.Net/Audio/IAudioClient.cs b/src/Discord.Net/Audio/IAudioClient.cs index 5f59851ee..40a75d4b5 100644 --- a/src/Discord.Net/Audio/IAudioClient.cs +++ b/src/Discord.Net/Audio/IAudioClient.cs @@ -6,7 +6,7 @@ namespace Discord.Audio public interface IAudioClient { event Func Connected; - event Func Disconnected; + event Func Disconnected; event Func LatencyUpdated; DiscordVoiceAPIClient ApiClient { get; } diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 49681702b..4cf53d2a3 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -26,13 +26,13 @@ namespace Discord internal readonly ILogger _discordLogger, _restLogger, _queueLogger; internal readonly SemaphoreSlim _connectionLock; - internal readonly LogManager _log; internal readonly RequestQueue _requestQueue; internal bool _isDisposed; internal SelfUser _currentUser; + public API.DiscordApiClient ApiClient { get; } + internal LogManager LogManager { get; } public LoginState LoginState { get; private set; } - public API.DiscordApiClient ApiClient { get; private set; } /// Creates a new REST-only discord client. public DiscordClient() @@ -40,11 +40,11 @@ namespace Discord /// Creates a new REST-only discord client. public DiscordClient(DiscordConfig config) { - _log = new LogManager(config.LogLevel); - _log.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); - _discordLogger = _log.CreateLogger("Discord"); - _restLogger = _log.CreateLogger("Rest"); - _queueLogger = _log.CreateLogger("Queue"); + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + _discordLogger = LogManager.CreateLogger("Discord"); + _restLogger = LogManager.CreateLogger("Rest"); + _queueLogger = LogManager.CreateLogger("Queue"); _connectionLock = new SemaphoreSlim(1, 1); @@ -267,6 +267,8 @@ namespace Discord public void Dispose() => Dispose(true); ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; + ILogManager IDiscordClient.LogManager => LogManager; + Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); } Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); } } diff --git a/src/Discord.Net/DiscordSocketClient.Events.cs b/src/Discord.Net/DiscordSocketClient.Events.cs index 545f3a4fb..092b19674 100644 --- a/src/Discord.Net/DiscordSocketClient.Events.cs +++ b/src/Discord.Net/DiscordSocketClient.Events.cs @@ -13,12 +13,12 @@ namespace Discord remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected + public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); public event Func Ready { add { _readyEvent.Add(value); } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index d09675e18..eb1096988 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -75,7 +75,7 @@ namespace Discord AudioMode = config.AudioMode; WebSocketProvider = config.WebSocketProvider; - _gatewayLogger = _log.CreateLogger("Gateway"); + _gatewayLogger = LogManager.CreateLogger("Gateway"); #if BENCHMARK _benchmarkLogger = _log.CreateLogger("Benchmark"); #endif @@ -94,7 +94,7 @@ namespace Discord if (ex != null) { await _gatewayLogger.WarningAsync($"Connection Closed: {ex.Message}").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); + await StartReconnectAsync(ex).ConfigureAwait(false); } else await _gatewayLogger.WarningAsync($"Connection Closed").ConfigureAwait(false); @@ -112,7 +112,7 @@ namespace Discord protected override async Task OnLogoutAsync() { if (ConnectionState != ConnectionState.Disconnected) - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); _voiceRegions = ImmutableDictionary.Create(); } @@ -142,7 +142,7 @@ namespace Discord var state = ConnectionState; if (state == ConnectionState.Connecting || state == ConnectionState.Connected) - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); ConnectionState = ConnectionState.Connecting; await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false); @@ -165,7 +165,7 @@ namespace Discord } catch (Exception) { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); throw; } } @@ -176,11 +176,11 @@ namespace Discord try { _isReconnecting = false; - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); } finally { _connectionLock.Release(); } } - private async Task DisconnectInternalAsync() + private async Task DisconnectInternalAsync(Exception ex) { ulong guildId; @@ -211,10 +211,10 @@ namespace Discord ConnectionState = ConnectionState.Disconnected; await _gatewayLogger.InfoAsync("Disconnected").ConfigureAwait(false); - await _disconnectedEvent.InvokeAsync().ConfigureAwait(false); + await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false); } - private async Task StartReconnectAsync() + private async Task StartReconnectAsync(Exception ex) { //TODO: Is this thread-safe? if (_reconnectTask != null) return; @@ -222,6 +222,7 @@ namespace Discord await _connectionLock.WaitAsync().ConfigureAwait(false); try { + await DisconnectInternalAsync(ex).ConfigureAwait(false); if (_reconnectTask != null) return; _isReconnecting = true; _reconnectTask = ReconnectInternalAsync(); @@ -469,7 +470,7 @@ namespace Discord await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); await _gatewayLogger.WarningAsync("Server requested a reconnect").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); + await StartReconnectAsync(new Exception("Server requested a reconnect")).ConfigureAwait(false); } break; case GatewayOpCode.Dispatch: @@ -1113,9 +1114,7 @@ namespace Discord var user = guild.GetUser(data.UserId); if (user != null) - { await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); - } else { await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); @@ -1131,7 +1130,21 @@ namespace Discord } break; case "VOICE_SERVER_UPDATE": - await _gatewayLogger.DebugAsync("Ignored Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + + if (AudioMode != AudioMode.Disabled) + { + var data = (payload as JToken).ToObject(_serializer); + var guild = DataStore.GetGuild(data.GuildId); + if (guild != null) + await guild.ConnectAudio("wss://" + data.Endpoint, data.Token).ConfigureAwait(false); + else + { + await _gatewayLogger.WarningAsync("VOICE_SERVER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + return; + } + } + return; //Ignored (User only) @@ -1183,7 +1196,7 @@ namespace Discord if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? false)) { await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); + await StartReconnectAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false); return; } } diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs index 2f383b4bc..22b2df52b 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuild.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuild.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ChannelModel = Discord.API.Channel; using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; @@ -17,8 +18,9 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord { - internal class CachedGuild : Guild, IUserGuild, ICachedEntity + internal class CachedGuild : Guild, ICachedEntity, IGuild, IUserGuild { + private readonly SemaphoreSlim _audioLock; private TaskCompletionSource _downloaderPromise; private ConcurrentHashSet _channels; private ConcurrentDictionary _members; @@ -27,7 +29,7 @@ namespace Discord public bool Available { get; private set; } public int MemberCount { get; private set; } public int DownloadedMemberCount { get; private set; } - public IAudioClient AudioClient { get; private set; } + public AudioClient AudioClient { get; private set; } public bool HasAllMembers => _downloaderPromise.Task.IsCompleted; public Task DownloaderPromise => _downloaderPromise.Task; @@ -48,6 +50,7 @@ namespace Discord public CachedGuild(DiscordSocketClient discord, ExtendedModel model, DataStore dataStore) : base(discord, model) { + _audioLock = new SemaphoreSlim(1, 1); _downloaderPromise = new TaskCompletionSource(); Update(model, UpdateSource.Creation, dataStore); } @@ -236,6 +239,55 @@ namespace Discord return null; } + public async Task ConnectAudio(string url, string token) + { + AudioClient audioClient; + await _audioLock.WaitAsync().ConfigureAwait(false); + var voiceState = GetVoiceState(CurrentUser.Id).Value; + try + { + audioClient = AudioClient; + if (audioClient == null) + { + audioClient = new AudioClient(this); + audioClient.Disconnected += async ex => + { + await _audioLock.WaitAsync().ConfigureAwait(false); + try + { + if (ex != null) + { + //Reconnect if we still have channel info. + //TODO: Is this threadsafe? Could channel data be deleted before we access it? + var voiceState2 = GetVoiceState(CurrentUser.Id); + if (voiceState2.HasValue) + { + var voiceChannelId = voiceState2.Value.VoiceChannel?.Id; + if (voiceChannelId != null) + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, voiceChannelId, voiceState2.Value.IsSelfDeafened, voiceState2.Value.IsSelfMuted); + } + } + else + { + try { AudioClient.Dispose(); } catch { } + AudioClient = null; + } + } + finally + { + _audioLock.Release(); + } + }; + AudioClient = audioClient; + } + } + finally + { + _audioLock.Release(); + } + await audioClient.ConnectAsync(url, CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); + } + public CachedGuild Clone() => MemberwiseClone() as CachedGuild; new internal ICachedGuildChannel ToChannel(ChannelModel model) @@ -253,5 +305,6 @@ namespace Discord bool IUserGuild.IsOwner => OwnerId == Discord.CurrentUser.Id; GuildPermissions IUserGuild.Permissions => CurrentUser.GuildPermissions; + IAudioClient IGuild.AudioClient => AudioClient; } } diff --git a/src/Discord.Net/IDiscordClient.cs b/src/Discord.Net/IDiscordClient.cs index c65d7b49e..796eb2611 100644 --- a/src/Discord.Net/IDiscordClient.cs +++ b/src/Discord.Net/IDiscordClient.cs @@ -1,4 +1,5 @@ using Discord.API; +using Discord.Logging; using System; using System.Collections.Generic; using System.IO; @@ -13,6 +14,7 @@ namespace Discord ConnectionState ConnectionState { get; } DiscordApiClient ApiClient { get; } + ILogManager LogManager { get; } Task LoginAsync(TokenType tokenType, string token, bool validateToken = true); Task LogoutAsync();