Moved (re)connection handling to ConnectionManager
This commit is contained in:
@@ -14,7 +14,7 @@ namespace Discord.Audio
|
|||||||
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary>
|
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary>
|
||||||
int Latency { get; }
|
int Latency { get; }
|
||||||
|
|
||||||
Task DisconnectAsync();
|
Task StopAsync();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new outgoing stream accepting Opus-encoded data.
|
/// Creates a new outgoing stream accepting Opus-encoded data.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ namespace Discord
|
|||||||
ConnectionState ConnectionState { get; }
|
ConnectionState ConnectionState { get; }
|
||||||
ISelfUser CurrentUser { get; }
|
ISelfUser CurrentUser { get; }
|
||||||
|
|
||||||
Task ConnectAsync();
|
Task StartAsync();
|
||||||
Task DisconnectAsync();
|
Task StopAsync();
|
||||||
|
|
||||||
Task<IApplication> GetApplicationInfoAsync();
|
Task<IApplication> GetApplicationInfoAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Discord
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => ToString(null);
|
public override string ToString() => ToString(null);
|
||||||
public string ToString(StringBuilder builder = null, bool fullException = true, bool prependTimestamp = true, DateTimeKind timestampKind = DateTimeKind.Local, int? padSource = 9)
|
public string ToString(StringBuilder builder = null, bool fullException = true, bool prependTimestamp = true, DateTimeKind timestampKind = DateTimeKind.Local, int? padSource = 11)
|
||||||
{
|
{
|
||||||
string sourceName = Source;
|
string sourceName = Source;
|
||||||
string message = Message;
|
string message = Message;
|
||||||
@@ -87,8 +87,11 @@ namespace Discord
|
|||||||
}
|
}
|
||||||
if (exMessage != null)
|
if (exMessage != null)
|
||||||
{
|
{
|
||||||
builder.Append(':');
|
if (!string.IsNullOrEmpty(Message))
|
||||||
builder.AppendLine();
|
{
|
||||||
|
builder.Append(':');
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
builder.Append(exMessage);
|
builder.Append(exMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<TargetFrameworks>netstandard1.6</TargetFrameworks>
|
<TargetFrameworks>netstandard1.6</TargetFrameworks>
|
||||||
<AssemblyName>Discord.Net.DebugTools</AssemblyName>
|
<AssemblyName>Discord.Net.DebugTools</AssemblyName>
|
||||||
<Authors>RogueException</Authors>
|
<Authors>RogueException</Authors>
|
||||||
<Description>A Discord.Net extension adding random helper classes for diagnosing issues.</Description>
|
<Description>A Discord.Net extension adding some helper classes for diagnosing issues.</Description>
|
||||||
<PackageTags>discord;discordapp</PackageTags>
|
<PackageTags>discord;discordapp</PackageTags>
|
||||||
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>
|
||||||
<PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl>
|
<PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl>
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ namespace Discord.Rest
|
|||||||
public event Func<Task> LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } }
|
public event Func<Task> LoggedOut { add { _loggedOutEvent.Add(value); } remove { _loggedOutEvent.Remove(value); } }
|
||||||
private readonly AsyncEvent<Func<Task>> _loggedOutEvent = new AsyncEvent<Func<Task>>();
|
private readonly AsyncEvent<Func<Task>> _loggedOutEvent = new AsyncEvent<Func<Task>>();
|
||||||
|
|
||||||
internal readonly Logger _restLogger, _queueLogger;
|
internal readonly Logger _restLogger;
|
||||||
internal readonly SemaphoreSlim _connectionLock;
|
private readonly SemaphoreSlim _stateLock;
|
||||||
private bool _isFirstLogin;
|
private bool _isFirstLogin, _isDisposed;
|
||||||
private bool _isDisposed;
|
|
||||||
|
|
||||||
internal API.DiscordRestApiClient ApiClient { get; }
|
internal API.DiscordRestApiClient ApiClient { get; }
|
||||||
internal LogManager LogManager { get; }
|
internal LogManager LogManager { get; }
|
||||||
@@ -35,17 +34,16 @@ namespace Discord.Rest
|
|||||||
LogManager = new LogManager(config.LogLevel);
|
LogManager = new LogManager(config.LogLevel);
|
||||||
LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false);
|
LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false);
|
||||||
|
|
||||||
_connectionLock = new SemaphoreSlim(1, 1);
|
_stateLock = new SemaphoreSlim(1, 1);
|
||||||
_restLogger = LogManager.CreateLogger("Rest");
|
_restLogger = LogManager.CreateLogger("Rest");
|
||||||
_queueLogger = LogManager.CreateLogger("Queue");
|
|
||||||
_isFirstLogin = config.DisplayInitialLog;
|
_isFirstLogin = config.DisplayInitialLog;
|
||||||
|
|
||||||
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
|
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
|
||||||
{
|
{
|
||||||
if (info == null)
|
if (info == null)
|
||||||
await _queueLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
await _restLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
||||||
else
|
else
|
||||||
await _queueLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
||||||
};
|
};
|
||||||
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
|
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -53,12 +51,12 @@ namespace Discord.Rest
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true)
|
public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true)
|
||||||
{
|
{
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
await _stateLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await LoginInternalAsync(tokenType, token).ConfigureAwait(false);
|
await LoginInternalAsync(tokenType, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally { _connectionLock.Release(); }
|
finally { _stateLock.Release(); }
|
||||||
}
|
}
|
||||||
private async Task LoginInternalAsync(TokenType tokenType, string token)
|
private async Task LoginInternalAsync(TokenType tokenType, string token)
|
||||||
{
|
{
|
||||||
@@ -86,17 +84,17 @@ namespace Discord.Rest
|
|||||||
|
|
||||||
await _loggedInEvent.InvokeAsync().ConfigureAwait(false);
|
await _loggedInEvent.InvokeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
protected virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.Delay(0); }
|
internal virtual Task OnLoginAsync(TokenType tokenType, string token) { return Task.Delay(0); }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task LogoutAsync()
|
public async Task LogoutAsync()
|
||||||
{
|
{
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
await _stateLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await LogoutInternalAsync().ConfigureAwait(false);
|
await LogoutInternalAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally { _connectionLock.Release(); }
|
finally { _stateLock.Release(); }
|
||||||
}
|
}
|
||||||
private async Task LogoutInternalAsync()
|
private async Task LogoutInternalAsync()
|
||||||
{
|
{
|
||||||
@@ -111,7 +109,7 @@ namespace Discord.Rest
|
|||||||
|
|
||||||
await _loggedOutEvent.InvokeAsync().ConfigureAwait(false);
|
await _loggedOutEvent.InvokeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
protected virtual Task OnLogoutAsync() { return Task.Delay(0); }
|
internal virtual Task OnLogoutAsync() { return Task.Delay(0); }
|
||||||
|
|
||||||
internal virtual void Dispose(bool disposing)
|
internal virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
@@ -161,8 +159,9 @@ namespace Discord.Rest
|
|||||||
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id)
|
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id)
|
||||||
=> Task.FromResult<IVoiceRegion>(null);
|
=> Task.FromResult<IVoiceRegion>(null);
|
||||||
|
|
||||||
Task IDiscordClient.ConnectAsync() { throw new NotSupportedException(); }
|
Task IDiscordClient.StartAsync()
|
||||||
Task IDiscordClient.DisconnectAsync() { throw new NotSupportedException(); }
|
=> Task.Delay(0);
|
||||||
|
Task IDiscordClient.StopAsync()
|
||||||
|
=> Task.Delay(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
199
src/Discord.Net.Rest/ConnectionManager.cs
Normal file
199
src/Discord.Net.Rest/ConnectionManager.cs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
using Discord.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
internal class ConnectionManager
|
||||||
|
{
|
||||||
|
public event Func<Task> Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } }
|
||||||
|
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>();
|
||||||
|
public event Func<Exception, bool, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } }
|
||||||
|
private readonly AsyncEvent<Func<Exception, bool, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, bool, Task>>();
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _stateLock;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
private readonly int _connectionTimeout;
|
||||||
|
private readonly Func<Task> _onConnecting;
|
||||||
|
private readonly Func<Exception, Task> _onDisconnecting;
|
||||||
|
|
||||||
|
private TaskCompletionSource<bool> _connectionPromise, _readyPromise;
|
||||||
|
private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken;
|
||||||
|
private Task _task;
|
||||||
|
|
||||||
|
public ConnectionState State { get; private set; }
|
||||||
|
public CancellationToken CancelToken { get; private set; }
|
||||||
|
|
||||||
|
public bool IsCompleted => _readyPromise.Task.IsCompleted;
|
||||||
|
|
||||||
|
internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout,
|
||||||
|
Func<Task> onConnecting, Func<Exception, Task> onDisconnecting, Action<Func<Exception, Task>> clientDisconnectHandler)
|
||||||
|
{
|
||||||
|
_stateLock = stateLock;
|
||||||
|
_logger = logger;
|
||||||
|
_connectionTimeout = connectionTimeout;
|
||||||
|
_onConnecting = onConnecting;
|
||||||
|
_onDisconnecting = onDisconnecting;
|
||||||
|
|
||||||
|
clientDisconnectHandler(ex =>
|
||||||
|
{
|
||||||
|
if (ex != null)
|
||||||
|
Error(new Exception("WebSocket connection was closed", ex));
|
||||||
|
else
|
||||||
|
Error(new Exception("WebSocket connection was closed"));
|
||||||
|
return Task.Delay(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task StartAsync()
|
||||||
|
{
|
||||||
|
await AcquireConnectionLock().ConfigureAwait(false);
|
||||||
|
var reconnectCancelToken = new CancellationTokenSource();
|
||||||
|
_reconnectCancelToken = new CancellationTokenSource();
|
||||||
|
_task = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Random jitter = new Random();
|
||||||
|
int nextReconnectDelay = 1000;
|
||||||
|
while (!reconnectCancelToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ConnectAsync(reconnectCancelToken).ConfigureAwait(false);
|
||||||
|
nextReconnectDelay = 1000; //Reset delay
|
||||||
|
await _connectionPromise.Task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
Cancel(); //In case this exception didn't come from another Error call
|
||||||
|
await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Error(ex); //In case this exception didn't come from another Error call
|
||||||
|
if (!reconnectCancelToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await _logger.WarningAsync(ex).ConfigureAwait(false);
|
||||||
|
await DisconnectAsync(ex, true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _logger.ErrorAsync(ex).ConfigureAwait(false);
|
||||||
|
await DisconnectAsync(ex, false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reconnectCancelToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
//Wait before reconnecting
|
||||||
|
await Task.Delay(nextReconnectDelay, reconnectCancelToken.Token).ConfigureAwait(false);
|
||||||
|
nextReconnectDelay = (nextReconnectDelay * 2) + jitter.Next(-250, 250);
|
||||||
|
if (nextReconnectDelay > 60000)
|
||||||
|
nextReconnectDelay = 60000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { _stateLock.Release(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public virtual async Task StopAsync()
|
||||||
|
{
|
||||||
|
Cancel();
|
||||||
|
var task = _task;
|
||||||
|
if (task != null)
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken)
|
||||||
|
{
|
||||||
|
_connectionCancelToken = new CancellationTokenSource();
|
||||||
|
_combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token);
|
||||||
|
CancelToken = _combinedCancelToken.Token;
|
||||||
|
|
||||||
|
_connectionPromise = new TaskCompletionSource<bool>();
|
||||||
|
State = ConnectionState.Connecting;
|
||||||
|
await _logger.InfoAsync("Connecting").ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var readyPromise = new TaskCompletionSource<bool>();
|
||||||
|
_readyPromise = readyPromise;
|
||||||
|
|
||||||
|
//Abort connection on timeout
|
||||||
|
var cancelToken = CancelToken;
|
||||||
|
var _ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(_connectionTimeout, cancelToken).ConfigureAwait(false);
|
||||||
|
readyPromise.TrySetException(new TimeoutException());
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
await _onConnecting().ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _logger.InfoAsync("Connected").ConfigureAwait(false);
|
||||||
|
State = ConnectionState.Connected;
|
||||||
|
await _logger.DebugAsync("Raising Event").ConfigureAwait(false);
|
||||||
|
await _connectedEvent.InvokeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Error(ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private async Task DisconnectAsync(Exception ex, bool isReconnecting)
|
||||||
|
{
|
||||||
|
if (State == ConnectionState.Disconnected) return;
|
||||||
|
State = ConnectionState.Disconnecting;
|
||||||
|
await _logger.InfoAsync("Disconnecting").ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _onDisconnecting(ex).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _logger.InfoAsync("Disconnected").ConfigureAwait(false);
|
||||||
|
State = ConnectionState.Disconnected;
|
||||||
|
await _disconnectedEvent.InvokeAsync(ex, isReconnecting).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CompleteAsync()
|
||||||
|
{
|
||||||
|
await _readyPromise.TrySetResultAsync(true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
public async Task WaitAsync()
|
||||||
|
{
|
||||||
|
await _readyPromise.Task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
_readyPromise?.TrySetCanceled();
|
||||||
|
_connectionPromise?.TrySetCanceled();
|
||||||
|
_reconnectCancelToken?.Cancel();
|
||||||
|
_connectionCancelToken?.Cancel();
|
||||||
|
}
|
||||||
|
public void Error(Exception ex)
|
||||||
|
{
|
||||||
|
_readyPromise.TrySetException(ex);
|
||||||
|
_connectionPromise.TrySetException(ex);
|
||||||
|
_connectionCancelToken?.Cancel();
|
||||||
|
}
|
||||||
|
public void CriticalError(Exception ex)
|
||||||
|
{
|
||||||
|
_reconnectCancelToken?.Cancel();
|
||||||
|
Error(ex);
|
||||||
|
}
|
||||||
|
private async Task AcquireConnectionLock()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await StopAsync().ConfigureAwait(false);
|
||||||
|
if (await _stateLock.WaitAsync(0).ConfigureAwait(false))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,14 +16,19 @@ namespace Discord.Rest
|
|||||||
|
|
||||||
private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
|
private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
|
||||||
=> new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent);
|
=> new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent);
|
||||||
|
internal override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
ApiClient.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnLoginAsync(TokenType tokenType, string token)
|
internal override async Task OnLoginAsync(TokenType tokenType, string token)
|
||||||
{
|
{
|
||||||
var user = await ApiClient.GetMyUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
|
var user = await ApiClient.GetMyUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false);
|
||||||
ApiClient.CurrentUserId = user.Id;
|
ApiClient.CurrentUserId = user.Id;
|
||||||
base.CurrentUser = RestSelfUser.Create(this, user);
|
base.CurrentUser = RestSelfUser.Create(this, user);
|
||||||
}
|
}
|
||||||
protected override Task OnLogoutAsync()
|
internal override Task OnLogoutAsync()
|
||||||
{
|
{
|
||||||
_applicationInfo = null;
|
_applicationInfo = null;
|
||||||
return Task.Delay(0);
|
return Task.Delay(0);
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ namespace Discord.Rpc
|
|||||||
remove { _connectedEvent.Remove(value); }
|
remove { _connectedEvent.Remove(value); }
|
||||||
}
|
}
|
||||||
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>();
|
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>();
|
||||||
public event Func<Exception, Task> Disconnected
|
public event Func<Exception, bool, Task> Disconnected
|
||||||
{
|
{
|
||||||
add { _disconnectedEvent.Add(value); }
|
add { _disconnectedEvent.Add(value); }
|
||||||
remove { _disconnectedEvent.Remove(value); }
|
remove { _disconnectedEvent.Remove(value); }
|
||||||
}
|
}
|
||||||
private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
|
private readonly AsyncEvent<Func<Exception, bool, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, bool, Task>>();
|
||||||
public event Func<Task> Ready
|
public event Func<Task> Ready
|
||||||
{
|
{
|
||||||
add { _readyEvent.Add(value); }
|
add { _readyEvent.Add(value); }
|
||||||
|
|||||||
@@ -8,28 +8,22 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Discord.Rpc
|
namespace Discord.Rpc
|
||||||
{
|
{
|
||||||
public partial class DiscordRpcClient : BaseDiscordClient, IDiscordClient
|
public partial class DiscordRpcClient : BaseDiscordClient, IDiscordClient
|
||||||
{
|
{
|
||||||
private readonly Logger _rpcLogger;
|
|
||||||
private readonly JsonSerializer _serializer;
|
private readonly JsonSerializer _serializer;
|
||||||
|
private readonly ConnectionManager _connection;
|
||||||
private TaskCompletionSource<bool> _connectTask;
|
private readonly Logger _rpcLogger;
|
||||||
private CancellationTokenSource _cancelToken, _reconnectCancelToken;
|
private readonly SemaphoreSlim _stateLock, _authorizeLock;
|
||||||
private Task _reconnectTask;
|
|
||||||
private bool _canReconnect;
|
|
||||||
|
|
||||||
public ConnectionState ConnectionState { get; private set; }
|
public ConnectionState ConnectionState { get; private set; }
|
||||||
public IReadOnlyCollection<string> Scopes { get; private set; }
|
public IReadOnlyCollection<string> Scopes { get; private set; }
|
||||||
public DateTimeOffset TokenExpiresAt { get; private set; }
|
public DateTimeOffset TokenExpiresAt { get; private set; }
|
||||||
|
|
||||||
//From DiscordRpcConfig
|
|
||||||
internal int ConnectionTimeout { get; private set; }
|
|
||||||
|
|
||||||
internal new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient;
|
internal new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient;
|
||||||
public new RestSelfUser CurrentUser { get { return base.CurrentUser as RestSelfUser; } private set { base.CurrentUser = value; } }
|
public new RestSelfUser CurrentUser { get { return base.CurrentUser as RestSelfUser; } private set { base.CurrentUser = value; } }
|
||||||
public RestApplication ApplicationInfo { get; private set; }
|
public RestApplication ApplicationInfo { get; private set; }
|
||||||
@@ -41,8 +35,11 @@ namespace Discord.Rpc
|
|||||||
public DiscordRpcClient(string clientId, string origin, DiscordRpcConfig config)
|
public DiscordRpcClient(string clientId, string origin, DiscordRpcConfig config)
|
||||||
: base(config, CreateApiClient(clientId, origin, config))
|
: base(config, CreateApiClient(clientId, origin, config))
|
||||||
{
|
{
|
||||||
ConnectionTimeout = config.ConnectionTimeout;
|
_stateLock = new SemaphoreSlim(1, 1);
|
||||||
|
_authorizeLock = new SemaphoreSlim(1, 1);
|
||||||
_rpcLogger = LogManager.CreateLogger("RPC");
|
_rpcLogger = LogManager.CreateLogger("RPC");
|
||||||
|
_connection = new ConnectionManager(_stateLock, _rpcLogger, config.ConnectionTimeout,
|
||||||
|
OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x);
|
||||||
|
|
||||||
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
||||||
_serializer.Error += (s, e) =>
|
_serializer.Error += (s, e) =>
|
||||||
@@ -53,175 +50,50 @@ namespace Discord.Rpc
|
|||||||
|
|
||||||
ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
|
ApiClient.SentRpcMessage += async opCode => await _rpcLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
|
||||||
ApiClient.ReceivedRpcEvent += ProcessMessageAsync;
|
ApiClient.ReceivedRpcEvent += ProcessMessageAsync;
|
||||||
ApiClient.Disconnected += async ex =>
|
|
||||||
{
|
|
||||||
if (ex != null)
|
|
||||||
{
|
|
||||||
await _rpcLogger.WarningAsync($"Connection Closed", ex).ConfigureAwait(false);
|
|
||||||
await StartReconnectAsync(ex).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
await _rpcLogger.WarningAsync($"Connection Closed").ConfigureAwait(false);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static API.DiscordRpcApiClient CreateApiClient(string clientId, string origin, DiscordRpcConfig config)
|
private static API.DiscordRpcApiClient CreateApiClient(string clientId, string origin, DiscordRpcConfig config)
|
||||||
=> new API.DiscordRpcApiClient(clientId, DiscordRestConfig.UserAgent, origin, config.RestClientProvider, config.WebSocketProvider);
|
=> new API.DiscordRpcApiClient(clientId, DiscordRestConfig.UserAgent, origin, config.RestClientProvider, config.WebSocketProvider);
|
||||||
|
internal override void Dispose(bool disposing)
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task ConnectAsync()
|
|
||||||
{
|
{
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
if (disposing)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
await ConnectInternalAsync(false).ConfigureAwait(false);
|
StopAsync().GetAwaiter().GetResult();
|
||||||
}
|
ApiClient.Dispose();
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
private async Task ConnectInternalAsync(bool isReconnecting)
|
|
||||||
{
|
|
||||||
if (!isReconnecting && _reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested)
|
|
||||||
_reconnectCancelToken.Cancel();
|
|
||||||
|
|
||||||
var state = ConnectionState;
|
|
||||||
if (state == ConnectionState.Connecting || state == ConnectionState.Connected)
|
|
||||||
await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false);
|
|
||||||
|
|
||||||
ConnectionState = ConnectionState.Connecting;
|
|
||||||
await _rpcLogger.InfoAsync("Connecting").ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var connectTask = new TaskCompletionSource<bool>();
|
|
||||||
_connectTask = connectTask;
|
|
||||||
_cancelToken = new CancellationTokenSource();
|
|
||||||
|
|
||||||
//Abort connection on timeout
|
|
||||||
var _ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await Task.Delay(ConnectionTimeout).ConfigureAwait(false);
|
|
||||||
connectTask.TrySetException(new TimeoutException());
|
|
||||||
});
|
|
||||||
|
|
||||||
await ApiClient.ConnectAsync().ConfigureAwait(false);
|
|
||||||
await _connectedEvent.InvokeAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _connectTask.Task.ConfigureAwait(false);
|
|
||||||
if (!isReconnecting)
|
|
||||||
_canReconnect = true;
|
|
||||||
ConnectionState = ConnectionState.Connected;
|
|
||||||
await _rpcLogger.InfoAsync("Connected").ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false);
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task DisconnectAsync()
|
public Task StartAsync() => _connection.StartAsync();
|
||||||
|
public Task StopAsync() => _connection.StopAsync();
|
||||||
|
|
||||||
|
private async Task OnConnectingAsync()
|
||||||
{
|
{
|
||||||
if (_connectTask?.TrySetCanceled() ?? false) return;
|
await _rpcLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false);
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
await ApiClient.ConnectAsync().ConfigureAwait(false);
|
||||||
try
|
|
||||||
{
|
await _connection.WaitAsync().ConfigureAwait(false);
|
||||||
await DisconnectInternalAsync(null, false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
}
|
||||||
private async Task DisconnectInternalAsync(Exception ex, bool isReconnecting)
|
private async Task OnDisconnectingAsync(Exception ex)
|
||||||
{
|
{
|
||||||
if (!isReconnecting)
|
await _rpcLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false);
|
||||||
{
|
|
||||||
_canReconnect = false;
|
|
||||||
|
|
||||||
if (_reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested)
|
|
||||||
_reconnectCancelToken.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ConnectionState == ConnectionState.Disconnected) return;
|
|
||||||
ConnectionState = ConnectionState.Disconnecting;
|
|
||||||
await _rpcLogger.InfoAsync("Disconnecting").ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _rpcLogger.DebugAsync("Disconnecting - CancelToken").ConfigureAwait(false);
|
|
||||||
//Signal tasks to complete
|
|
||||||
try { _cancelToken.Cancel(); } catch { }
|
|
||||||
|
|
||||||
await _rpcLogger.DebugAsync("Disconnecting - ApiClient").ConfigureAwait(false);
|
|
||||||
//Disconnect from server
|
|
||||||
await ApiClient.DisconnectAsync().ConfigureAwait(false);
|
await ApiClient.DisconnectAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
ConnectionState = ConnectionState.Disconnected;
|
|
||||||
await _rpcLogger.InfoAsync("Disconnected").ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartReconnectAsync(Exception ex)
|
|
||||||
{
|
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!_canReconnect || _reconnectTask != null) return;
|
|
||||||
_reconnectCancelToken = new CancellationTokenSource();
|
|
||||||
_reconnectTask = ReconnectInternalAsync(ex, _reconnectCancelToken.Token);
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
private async Task ReconnectInternalAsync(Exception ex, CancellationToken cancelToken)
|
|
||||||
{
|
|
||||||
if (ex == null)
|
|
||||||
{
|
|
||||||
if (_connectTask?.TrySetCanceled() ?? false) return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_connectTask?.TrySetException(ex) ?? false) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Random jitter = new Random();
|
|
||||||
int nextReconnectDelay = 1000;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
await Task.Delay(nextReconnectDelay, cancelToken).ConfigureAwait(false);
|
|
||||||
nextReconnectDelay = nextReconnectDelay * 2 + jitter.Next(-250, 250);
|
|
||||||
if (nextReconnectDelay > 60000)
|
|
||||||
nextReconnectDelay = 60000;
|
|
||||||
|
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (cancelToken.IsCancellationRequested) return;
|
|
||||||
await ConnectInternalAsync(true).ConfigureAwait(false);
|
|
||||||
_reconnectTask = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (Exception ex2)
|
|
||||||
{
|
|
||||||
await _rpcLogger.WarningAsync("Reconnect failed", ex2).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _rpcLogger.DebugAsync("Reconnect cancelled").ConfigureAwait(false);
|
|
||||||
_reconnectTask = null;
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> AuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null)
|
public async Task<string> AuthorizeAsync(string[] scopes, string rpcToken = null, RequestOptions options = null)
|
||||||
{
|
{
|
||||||
await ConnectAsync().ConfigureAwait(false);
|
await _authorizeLock.WaitAsync().ConfigureAwait(false);
|
||||||
var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken, options).ConfigureAwait(false);
|
try
|
||||||
await DisconnectAsync().ConfigureAwait(false);
|
{
|
||||||
return result.Code;
|
await _connection.StartAsync().ConfigureAwait(false);
|
||||||
|
await _connection.WaitAsync().ConfigureAwait(false);
|
||||||
|
var result = await ApiClient.SendAuthorizeAsync(scopes, rpcToken, options).ConfigureAwait(false);
|
||||||
|
await _connection.StopAsync().ConfigureAwait(false);
|
||||||
|
return result.Code;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_authorizeLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SubscribeGlobal(RpcGlobalEvent evnt, RequestOptions options = null)
|
public async Task SubscribeGlobal(RpcGlobalEvent evnt, RequestOptions options = null)
|
||||||
@@ -439,8 +311,8 @@ namespace Discord.Rpc
|
|||||||
ApplicationInfo = RestApplication.Create(this, response.Application);
|
ApplicationInfo = RestApplication.Create(this, response.Application);
|
||||||
Scopes = response.Scopes;
|
Scopes = response.Scopes;
|
||||||
TokenExpiresAt = response.Expires;
|
TokenExpiresAt = response.Expires;
|
||||||
|
|
||||||
var __ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete
|
var __ = _connection.CompleteAsync();
|
||||||
await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false);
|
await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -452,7 +324,7 @@ namespace Discord.Rpc
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete
|
var _ = _connection.CompleteAsync();
|
||||||
await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false);
|
await _rpcLogger.InfoAsync("Ready").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -592,6 +464,13 @@ namespace Discord.Rpc
|
|||||||
}
|
}
|
||||||
|
|
||||||
//IDiscordClient
|
//IDiscordClient
|
||||||
|
ConnectionState IDiscordClient.ConnectionState => _connection.State;
|
||||||
|
|
||||||
Task<IApplication> IDiscordClient.GetApplicationInfoAsync() => Task.FromResult<IApplication>(ApplicationInfo);
|
Task<IApplication> IDiscordClient.GetApplicationInfoAsync() => Task.FromResult<IApplication>(ApplicationInfo);
|
||||||
|
|
||||||
|
async Task IDiscordClient.StartAsync()
|
||||||
|
=> await StartAsync().ConfigureAwait(false);
|
||||||
|
async Task IDiscordClient.StopAsync()
|
||||||
|
=> await StopAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Discord.WebSocket;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -12,6 +13,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Discord.Audio
|
namespace Discord.Audio
|
||||||
{
|
{
|
||||||
|
//TODO: Add audio reconnecting
|
||||||
internal class AudioClient : IAudioClient, IDisposable
|
internal class AudioClient : IAudioClient, IDisposable
|
||||||
{
|
{
|
||||||
public event Func<Task> Connected
|
public event Func<Task> Connected
|
||||||
@@ -34,34 +36,37 @@ namespace Discord.Audio
|
|||||||
private readonly AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>();
|
private readonly AsyncEvent<Func<int, int, Task>> _latencyUpdatedEvent = new AsyncEvent<Func<int, int, Task>>();
|
||||||
|
|
||||||
private readonly Logger _audioLogger;
|
private readonly Logger _audioLogger;
|
||||||
internal readonly SemaphoreSlim _connectionLock;
|
|
||||||
private readonly JsonSerializer _serializer;
|
private readonly JsonSerializer _serializer;
|
||||||
|
private readonly ConnectionManager _connection;
|
||||||
|
private readonly SemaphoreSlim _stateLock;
|
||||||
|
private readonly ConcurrentQueue<long> _heartbeatTimes;
|
||||||
|
|
||||||
private TaskCompletionSource<bool> _connectTask;
|
|
||||||
private CancellationTokenSource _cancelTokenSource;
|
|
||||||
private Task _heartbeatTask;
|
private Task _heartbeatTask;
|
||||||
private long _heartbeatTime;
|
private long _lastMessageTime;
|
||||||
private string _url;
|
private string _url, _sessionId, _token;
|
||||||
|
private ulong _userId;
|
||||||
private uint _ssrc;
|
private uint _ssrc;
|
||||||
private byte[] _secretKey;
|
private byte[] _secretKey;
|
||||||
private bool _isDisposed;
|
|
||||||
|
|
||||||
public SocketGuild Guild { get; }
|
public SocketGuild Guild { get; }
|
||||||
public DiscordVoiceAPIClient ApiClient { get; private set; }
|
public DiscordVoiceAPIClient ApiClient { get; private set; }
|
||||||
public ConnectionState ConnectionState { get; private set; }
|
|
||||||
public int Latency { get; private set; }
|
public int Latency { get; private set; }
|
||||||
|
|
||||||
private DiscordSocketClient Discord => Guild.Discord;
|
private DiscordSocketClient Discord => Guild.Discord;
|
||||||
|
public ConnectionState ConnectionState => _connection.State;
|
||||||
|
|
||||||
/// <summary> Creates a new REST/WebSocket discord client. </summary>
|
/// <summary> Creates a new REST/WebSocket discord client. </summary>
|
||||||
internal AudioClient(SocketGuild guild, int id)
|
internal AudioClient(SocketGuild guild, int id)
|
||||||
{
|
{
|
||||||
Guild = guild;
|
Guild = guild;
|
||||||
|
|
||||||
|
_stateLock = new SemaphoreSlim(1, 1);
|
||||||
|
_connection = new ConnectionManager(_stateLock, _audioLogger, 30000,
|
||||||
|
OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x);
|
||||||
|
_heartbeatTimes = new ConcurrentQueue<long>();
|
||||||
|
|
||||||
_audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}");
|
_audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}");
|
||||||
|
|
||||||
_connectionLock = new SemaphoreSlim(1, 1);
|
|
||||||
|
|
||||||
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
||||||
_serializer.Error += (s, e) =>
|
_serializer.Error += (s, e) =>
|
||||||
{
|
{
|
||||||
@@ -76,83 +81,28 @@ namespace Discord.Audio
|
|||||||
//ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
|
//ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
|
||||||
ApiClient.ReceivedEvent += ProcessMessageAsync;
|
ApiClient.ReceivedEvent += ProcessMessageAsync;
|
||||||
ApiClient.ReceivedPacket += ProcessPacketAsync;
|
ApiClient.ReceivedPacket += ProcessPacketAsync;
|
||||||
ApiClient.Disconnected += async ex =>
|
|
||||||
{
|
|
||||||
if (ex != null)
|
|
||||||
await _audioLogger.WarningAsync($"Connection Closed", ex).ConfigureAwait(false);
|
|
||||||
else
|
|
||||||
await _audioLogger.WarningAsync($"Connection Closed").ConfigureAwait(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false);
|
LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
internal async Task StartAsync(string url, ulong userId, string sessionId, string token)
|
||||||
internal async Task ConnectAsync(string url, ulong userId, string sessionId, string token)
|
|
||||||
{
|
{
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
_url = url;
|
||||||
try
|
_userId = userId;
|
||||||
{
|
_sessionId = sessionId;
|
||||||
await ConnectInternalAsync(url, userId, sessionId, token).ConfigureAwait(false);
|
_token = token;
|
||||||
}
|
await _connection.StartAsync().ConfigureAwait(false);
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
}
|
||||||
private async Task ConnectInternalAsync(string url, ulong userId, string sessionId, string token)
|
public async Task StopAsync()
|
||||||
|
=> await _connection.StopAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
private async Task OnConnectingAsync()
|
||||||
{
|
{
|
||||||
var state = ConnectionState;
|
await ApiClient.ConnectAsync("wss://" + _url).ConfigureAwait(false);
|
||||||
if (state == ConnectionState.Connecting || state == ConnectionState.Connected)
|
await ApiClient.SendIdentityAsync(_userId, _sessionId, _token).ConfigureAwait(false);
|
||||||
await DisconnectInternalAsync(null).ConfigureAwait(false);
|
|
||||||
|
|
||||||
ConnectionState = ConnectionState.Connecting;
|
|
||||||
await _audioLogger.InfoAsync("Connecting").ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_url = url;
|
|
||||||
_connectTask = new TaskCompletionSource<bool>();
|
|
||||||
_cancelTokenSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
await ApiClient.ConnectAsync("wss://" + url).ConfigureAwait(false);
|
|
||||||
await ApiClient.SendIdentityAsync(userId, sessionId, token).ConfigureAwait(false);
|
|
||||||
await _connectTask.Task.ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _connectedEvent.InvokeAsync().ConfigureAwait(false);
|
|
||||||
ConnectionState = ConnectionState.Connected;
|
|
||||||
await _audioLogger.InfoAsync("Connected").ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await DisconnectInternalAsync(null).ConfigureAwait(false);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/// <inheritdoc />
|
private async Task OnDisconnectingAsync(Exception ex)
|
||||||
public async Task DisconnectAsync()
|
|
||||||
{
|
{
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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(Exception ex)
|
|
||||||
{
|
|
||||||
if (ConnectionState == ConnectionState.Disconnected) return;
|
|
||||||
ConnectionState = ConnectionState.Disconnecting;
|
|
||||||
await _audioLogger.InfoAsync("Disconnecting").ConfigureAwait(false);
|
|
||||||
|
|
||||||
//Signal tasks to complete
|
|
||||||
try { _cancelTokenSource.Cancel(); } catch { }
|
|
||||||
|
|
||||||
//Disconnect from server
|
//Disconnect from server
|
||||||
await ApiClient.DisconnectAsync().ConfigureAwait(false);
|
await ApiClient.DisconnectAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -162,17 +112,17 @@ namespace Discord.Audio
|
|||||||
await heartbeatTask.ConfigureAwait(false);
|
await heartbeatTask.ConfigureAwait(false);
|
||||||
_heartbeatTask = null;
|
_heartbeatTask = null;
|
||||||
|
|
||||||
ConnectionState = ConnectionState.Disconnected;
|
|
||||||
await _audioLogger.InfoAsync("Disconnected").ConfigureAwait(false);
|
|
||||||
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
|
|
||||||
|
|
||||||
await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false);
|
await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false);
|
||||||
|
|
||||||
|
long time;
|
||||||
|
while (_heartbeatTimes.TryDequeue(out time)) { }
|
||||||
|
_lastMessageTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis)
|
public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis)
|
||||||
{
|
{
|
||||||
CheckSamplesPerFrame(samplesPerFrame);
|
CheckSamplesPerFrame(samplesPerFrame);
|
||||||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token);
|
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _connection.CancelToken);
|
||||||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc);
|
||||||
}
|
}
|
||||||
public AudioOutStream CreateDirectOpusStream(int samplesPerFrame)
|
public AudioOutStream CreateDirectOpusStream(int samplesPerFrame)
|
||||||
@@ -184,7 +134,7 @@ namespace Discord.Audio
|
|||||||
public AudioOutStream CreatePCMStream(int samplesPerFrame, int channels, int? bitrate, int bufferMillis)
|
public AudioOutStream CreatePCMStream(int samplesPerFrame, int channels, int? bitrate, int bufferMillis)
|
||||||
{
|
{
|
||||||
CheckSamplesPerFrame(samplesPerFrame);
|
CheckSamplesPerFrame(samplesPerFrame);
|
||||||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token);
|
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _connection.CancelToken);
|
||||||
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
|
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate);
|
||||||
}
|
}
|
||||||
public AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels, int? bitrate)
|
public AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels, int? bitrate)
|
||||||
@@ -202,6 +152,8 @@ namespace Discord.Audio
|
|||||||
|
|
||||||
private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload)
|
private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload)
|
||||||
{
|
{
|
||||||
|
_lastMessageTime = Environment.TickCount;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
switch (opCode)
|
switch (opCode)
|
||||||
@@ -216,8 +168,7 @@ namespace Discord.Audio
|
|||||||
if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode))
|
if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode))
|
||||||
throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}");
|
throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}");
|
||||||
|
|
||||||
_heartbeatTime = 0;
|
_heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken);
|
||||||
_heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelTokenSource.Token);
|
|
||||||
|
|
||||||
ApiClient.SetUdpEndpoint(_url, data.Port);
|
ApiClient.SetUdpEndpoint(_url, data.Port);
|
||||||
await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false);
|
await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false);
|
||||||
@@ -234,19 +185,17 @@ namespace Discord.Audio
|
|||||||
_secretKey = data.SecretKey;
|
_secretKey = data.SecretKey;
|
||||||
await ApiClient.SendSetSpeaking(true).ConfigureAwait(false);
|
await ApiClient.SendSetSpeaking(true).ConfigureAwait(false);
|
||||||
|
|
||||||
var _ = _connectTask.TrySetResultAsync(true);
|
var _ = _connection.CompleteAsync();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case VoiceOpCode.HeartbeatAck:
|
case VoiceOpCode.HeartbeatAck:
|
||||||
{
|
{
|
||||||
await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false);
|
await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false);
|
||||||
|
|
||||||
var heartbeatTime = _heartbeatTime;
|
long time;
|
||||||
if (heartbeatTime != 0)
|
if (_heartbeatTimes.TryDequeue(out time))
|
||||||
{
|
{
|
||||||
int latency = (int)(Environment.TickCount - _heartbeatTime);
|
int latency = (int)(Environment.TickCount - time);
|
||||||
_heartbeatTime = 0;
|
|
||||||
|
|
||||||
int before = Latency;
|
int before = Latency;
|
||||||
Latency = latency;
|
Latency = latency;
|
||||||
|
|
||||||
@@ -267,7 +216,7 @@ namespace Discord.Audio
|
|||||||
}
|
}
|
||||||
private async Task ProcessPacketAsync(byte[] packet)
|
private async Task ProcessPacketAsync(byte[] packet)
|
||||||
{
|
{
|
||||||
if (!_connectTask.Task.IsCompleted)
|
if (!_connection.IsCompleted)
|
||||||
{
|
{
|
||||||
if (packet.Length == 70)
|
if (packet.Length == 70)
|
||||||
{
|
{
|
||||||
@@ -291,33 +240,50 @@ namespace Discord.Audio
|
|||||||
//Clean this up when Discord's session patch is live
|
//Clean this up when Discord's session patch is live
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await _audioLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false);
|
||||||
while (!cancelToken.IsCancellationRequested)
|
while (!cancelToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
var now = Environment.TickCount;
|
||||||
|
|
||||||
|
//Did server respond to our last heartbeat, or are we still receiving messages (long load?)
|
||||||
|
if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis &&
|
||||||
|
ConnectionState == ConnectionState.Connected)
|
||||||
|
{
|
||||||
|
_connection.Error(new Exception("Server missed last heartbeat"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_heartbeatTimes.Enqueue(now);
|
||||||
|
|
||||||
await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false);
|
await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (_heartbeatTime != 0) //Server never responded to our last heartbeat
|
try
|
||||||
{
|
{
|
||||||
if (ConnectionState == ConnectionState.Connected)
|
await ApiClient.SendHeartbeatAsync().ConfigureAwait(false);
|
||||||
{
|
|
||||||
await _audioLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false);
|
|
||||||
await DisconnectInternalAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
_heartbeatTime = Environment.TickCount;
|
{
|
||||||
await ApiClient.SendHeartbeatAsync().ConfigureAwait(false);
|
await _audioLogger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
await _audioLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
await _audioLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await _audioLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Dispose(bool disposing)
|
internal void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing && !_isDisposed)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_isDisposed = true;
|
StopAsync().GetAwaiter().GetResult();
|
||||||
DisconnectInternalAsync(null).GetAwaiter().GetResult();
|
|
||||||
ApiClient.Dispose();
|
ApiClient.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ namespace Discord.WebSocket
|
|||||||
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
|
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
|
||||||
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent);
|
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent);
|
||||||
|
|
||||||
protected override async Task OnLoginAsync(TokenType tokenType, string token)
|
internal override async Task OnLoginAsync(TokenType tokenType, string token)
|
||||||
{
|
{
|
||||||
if (_automaticShards)
|
if (_automaticShards)
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,7 @@ namespace Discord.WebSocket
|
|||||||
for (int i = 0; i < _shards.Length; i++)
|
for (int i = 0; i < _shards.Length; i++)
|
||||||
await _shards[i].LoginAsync(tokenType, token, false);
|
await _shards[i].LoginAsync(tokenType, token, false);
|
||||||
}
|
}
|
||||||
protected override async Task OnLogoutAsync()
|
internal override async Task OnLogoutAsync()
|
||||||
{
|
{
|
||||||
//Assume threadsafe: already in a connection lock
|
//Assume threadsafe: already in a connection lock
|
||||||
for (int i = 0; i < _shards.Length; i++)
|
for (int i = 0; i < _shards.Length; i++)
|
||||||
@@ -112,42 +112,14 @@ namespace Discord.WebSocket
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task ConnectAsync()
|
public async Task StartAsync()
|
||||||
{
|
{
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false);
|
||||||
try
|
|
||||||
{
|
|
||||||
await ConnectInternalAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await DisconnectInternalAsync().ConfigureAwait(false);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
private async Task ConnectInternalAsync()
|
|
||||||
{
|
|
||||||
await Task.WhenAll(
|
|
||||||
_shards.Select(x => x.ConnectAsync())
|
|
||||||
).ConfigureAwait(false);
|
|
||||||
|
|
||||||
CurrentUser = _shards[0].CurrentUser;
|
|
||||||
}
|
}
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task DisconnectAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false);
|
||||||
try
|
|
||||||
{
|
|
||||||
await DisconnectInternalAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
private async Task DisconnectInternalAsync()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _shards.Length; i++)
|
|
||||||
await _shards[i].DisconnectAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DiscordSocketClient GetShard(int id)
|
public DiscordSocketClient GetShard(int id)
|
||||||
@@ -334,9 +306,6 @@ namespace Discord.WebSocket
|
|||||||
}
|
}
|
||||||
|
|
||||||
//IDiscordClient
|
//IDiscordClient
|
||||||
Task IDiscordClient.ConnectAsync()
|
|
||||||
=> ConnectAsync();
|
|
||||||
|
|
||||||
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync()
|
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync()
|
||||||
=> await GetApplicationInfoAsync().ConfigureAwait(false);
|
=> await GetApplicationInfoAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace Discord.API
|
|||||||
|
|
||||||
private CancellationTokenSource _connectCancelToken;
|
private CancellationTokenSource _connectCancelToken;
|
||||||
private string _gatewayUrl;
|
private string _gatewayUrl;
|
||||||
|
private bool _isExplicitUrl;
|
||||||
|
|
||||||
internal IWebSocketClient WebSocketClient { get; }
|
internal IWebSocketClient WebSocketClient { get; }
|
||||||
|
|
||||||
@@ -38,6 +39,8 @@ namespace Discord.API
|
|||||||
: base(restClientProvider, userAgent, defaultRetryMode, serializer)
|
: base(restClientProvider, userAgent, defaultRetryMode, serializer)
|
||||||
{
|
{
|
||||||
_gatewayUrl = url;
|
_gatewayUrl = url;
|
||||||
|
if (url != null)
|
||||||
|
_isExplicitUrl = true;
|
||||||
WebSocketClient = webSocketProvider();
|
WebSocketClient = webSocketProvider();
|
||||||
//WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+)
|
//WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+)
|
||||||
WebSocketClient.BinaryMessage += async (data, index, count) =>
|
WebSocketClient.BinaryMessage += async (data, index, count) =>
|
||||||
@@ -52,7 +55,8 @@ namespace Discord.API
|
|||||||
using (var jsonReader = new JsonTextReader(reader))
|
using (var jsonReader = new JsonTextReader(reader))
|
||||||
{
|
{
|
||||||
var msg = _serializer.Deserialize<SocketFrame>(jsonReader);
|
var msg = _serializer.Deserialize<SocketFrame>(jsonReader);
|
||||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
if (msg != null)
|
||||||
|
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -62,7 +66,8 @@ namespace Discord.API
|
|||||||
using (var jsonReader = new JsonTextReader(reader))
|
using (var jsonReader = new JsonTextReader(reader))
|
||||||
{
|
{
|
||||||
var msg = _serializer.Deserialize<SocketFrame>(jsonReader);
|
var msg = _serializer.Deserialize<SocketFrame>(jsonReader);
|
||||||
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
if (msg != null)
|
||||||
|
await _receivedGatewayEvent.InvokeAsync((GatewayOpCode)msg.Operation, msg.Sequence, msg.Type, msg.Payload).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
WebSocketClient.Closed += async ex =>
|
WebSocketClient.Closed += async ex =>
|
||||||
@@ -107,7 +112,7 @@ namespace Discord.API
|
|||||||
if (WebSocketClient != null)
|
if (WebSocketClient != null)
|
||||||
WebSocketClient.SetCancelToken(_connectCancelToken.Token);
|
WebSocketClient.SetCancelToken(_connectCancelToken.Token);
|
||||||
|
|
||||||
if (_gatewayUrl == null)
|
if (!_isExplicitUrl)
|
||||||
{
|
{
|
||||||
var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false);
|
var gatewayResponse = await GetGatewayAsync().ConfigureAwait(false);
|
||||||
_gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}";
|
_gatewayUrl = $"{gatewayResponse.Url}?v={DiscordConfig.APIVersion}&encoding={DiscordSocketConfig.GatewayEncoding}";
|
||||||
@@ -118,7 +123,8 @@ namespace Discord.API
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_gatewayUrl = null; //Uncache in case the gateway url changed
|
if (!_isExplicitUrl)
|
||||||
|
_gatewayUrl = null; //Uncache in case the gateway url changed
|
||||||
await DisconnectInternalAsync().ConfigureAwait(false);
|
await DisconnectInternalAsync().ConfigureAwait(false);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,29 +17,27 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GameModel = Discord.API.Game;
|
using GameModel = Discord.API.Game;
|
||||||
using Discord.Net;
|
|
||||||
|
|
||||||
namespace Discord.WebSocket
|
namespace Discord.WebSocket
|
||||||
{
|
{
|
||||||
public partial class DiscordSocketClient : BaseDiscordClient, IDiscordClient
|
public partial class DiscordSocketClient : BaseDiscordClient, IDiscordClient
|
||||||
{
|
{
|
||||||
private readonly ConcurrentQueue<ulong> _largeGuilds;
|
private readonly ConcurrentQueue<ulong> _largeGuilds;
|
||||||
private readonly Logger _gatewayLogger;
|
|
||||||
private readonly JsonSerializer _serializer;
|
private readonly JsonSerializer _serializer;
|
||||||
private readonly SemaphoreSlim _connectionGroupLock;
|
private readonly SemaphoreSlim _connectionGroupLock;
|
||||||
private readonly DiscordSocketClient _parentClient;
|
private readonly DiscordSocketClient _parentClient;
|
||||||
private readonly ConcurrentQueue<long> _heartbeatTimes;
|
private readonly ConcurrentQueue<long> _heartbeatTimes;
|
||||||
|
private readonly ConnectionManager _connection;
|
||||||
|
private readonly Logger _gatewayLogger;
|
||||||
|
private readonly SemaphoreSlim _stateLock;
|
||||||
|
|
||||||
private string _sessionId;
|
private string _sessionId;
|
||||||
private int _lastSeq;
|
private int _lastSeq;
|
||||||
private ImmutableDictionary<string, RestVoiceRegion> _voiceRegions;
|
private ImmutableDictionary<string, RestVoiceRegion> _voiceRegions;
|
||||||
private TaskCompletionSource<bool> _connectTask;
|
private Task _heartbeatTask, _guildDownloadTask;
|
||||||
private CancellationTokenSource _cancelToken, _reconnectCancelToken;
|
|
||||||
private Task _heartbeatTask, _guildDownloadTask, _reconnectTask;
|
|
||||||
private int _unavailableGuilds;
|
private int _unavailableGuilds;
|
||||||
private long _lastGuildAvailableTime, _lastMessageTime;
|
private long _lastGuildAvailableTime, _lastMessageTime;
|
||||||
private int _nextAudioId;
|
private int _nextAudioId;
|
||||||
private bool _canReconnect;
|
|
||||||
private DateTimeOffset? _statusSince;
|
private DateTimeOffset? _statusSince;
|
||||||
private RestApplication _applicationInfo;
|
private RestApplication _applicationInfo;
|
||||||
private ConcurrentHashSet<ulong> _downloadUsersFor;
|
private ConcurrentHashSet<ulong> _downloadUsersFor;
|
||||||
@@ -59,7 +57,6 @@ namespace Discord.WebSocket
|
|||||||
internal int LargeThreshold { get; private set; }
|
internal int LargeThreshold { get; private set; }
|
||||||
internal AudioMode AudioMode { get; private set; }
|
internal AudioMode AudioMode { get; private set; }
|
||||||
internal ClientState State { get; private set; }
|
internal ClientState State { get; private set; }
|
||||||
internal int ConnectionTimeout { get; private set; }
|
|
||||||
internal UdpSocketProvider UdpSocketProvider { get; private set; }
|
internal UdpSocketProvider UdpSocketProvider { get; private set; }
|
||||||
internal WebSocketProvider WebSocketProvider { get; private set; }
|
internal WebSocketProvider WebSocketProvider { get; private set; }
|
||||||
internal bool AlwaysDownloadUsers { get; private set; }
|
internal bool AlwaysDownloadUsers { get; private set; }
|
||||||
@@ -90,35 +87,28 @@ namespace Discord.WebSocket
|
|||||||
UdpSocketProvider = config.UdpSocketProvider;
|
UdpSocketProvider = config.UdpSocketProvider;
|
||||||
WebSocketProvider = config.WebSocketProvider;
|
WebSocketProvider = config.WebSocketProvider;
|
||||||
AlwaysDownloadUsers = config.AlwaysDownloadUsers;
|
AlwaysDownloadUsers = config.AlwaysDownloadUsers;
|
||||||
ConnectionTimeout = config.ConnectionTimeout;
|
|
||||||
State = new ClientState(0, 0);
|
State = new ClientState(0, 0);
|
||||||
_downloadUsersFor = new ConcurrentHashSet<ulong>();
|
_downloadUsersFor = new ConcurrentHashSet<ulong>();
|
||||||
_heartbeatTimes = new ConcurrentQueue<long>();
|
_heartbeatTimes = new ConcurrentQueue<long>();
|
||||||
|
|
||||||
|
_stateLock = new SemaphoreSlim(1, 1);
|
||||||
|
_gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}");
|
||||||
|
_connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout,
|
||||||
|
OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x);
|
||||||
|
|
||||||
_nextAudioId = 1;
|
_nextAudioId = 1;
|
||||||
_gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : "Shard #" + ShardId);
|
|
||||||
_connectionGroupLock = groupLock;
|
_connectionGroupLock = groupLock;
|
||||||
_parentClient = parentClient;
|
_parentClient = parentClient;
|
||||||
|
|
||||||
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
|
||||||
_serializer.Error += (s, e) =>
|
_serializer.Error += (s, e) =>
|
||||||
{
|
{
|
||||||
_gatewayLogger.WarningAsync(e.ErrorContext.Error).GetAwaiter().GetResult();
|
_gatewayLogger.WarningAsync("Serializer Error", e.ErrorContext.Error).GetAwaiter().GetResult();
|
||||||
e.ErrorContext.Handled = true;
|
e.ErrorContext.Handled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
|
ApiClient.SentGatewayMessage += async opCode => await _gatewayLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
|
||||||
ApiClient.ReceivedGatewayEvent += ProcessMessageAsync;
|
ApiClient.ReceivedGatewayEvent += ProcessMessageAsync;
|
||||||
ApiClient.Disconnected += async ex =>
|
|
||||||
{
|
|
||||||
if (ex != null)
|
|
||||||
{
|
|
||||||
await _gatewayLogger.WarningAsync($"Connection Closed", ex).ConfigureAwait(false);
|
|
||||||
await StartReconnectAsync(ex).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
await _gatewayLogger.WarningAsync($"Connection Closed").ConfigureAwait(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
LeftGuild += async g => await _gatewayLogger.InfoAsync($"Left {g.Name}").ConfigureAwait(false);
|
LeftGuild += async g => await _gatewayLogger.InfoAsync($"Left {g.Name}").ConfigureAwait(false);
|
||||||
JoinedGuild += async g => await _gatewayLogger.InfoAsync($"Joined {g.Name}").ConfigureAwait(false);
|
JoinedGuild += async g => await _gatewayLogger.InfoAsync($"Joined {g.Name}").ConfigureAwait(false);
|
||||||
@@ -143,8 +133,16 @@ namespace Discord.WebSocket
|
|||||||
}
|
}
|
||||||
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
|
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
|
||||||
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost);
|
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost);
|
||||||
|
internal override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
StopAsync().GetAwaiter().GetResult();
|
||||||
|
ApiClient.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnLoginAsync(TokenType tokenType, string token)
|
internal override async Task OnLoginAsync(TokenType tokenType, string token)
|
||||||
{
|
{
|
||||||
if (_parentClient == null)
|
if (_parentClient == null)
|
||||||
{
|
{
|
||||||
@@ -154,92 +152,49 @@ namespace Discord.WebSocket
|
|||||||
else
|
else
|
||||||
_voiceRegions = _parentClient._voiceRegions;
|
_voiceRegions = _parentClient._voiceRegions;
|
||||||
}
|
}
|
||||||
protected override async Task OnLogoutAsync()
|
internal override async Task OnLogoutAsync()
|
||||||
{
|
{
|
||||||
if (ConnectionState != ConnectionState.Disconnected)
|
await StopAsync().ConfigureAwait(false);
|
||||||
await DisconnectInternalAsync(null, false).ConfigureAwait(false);
|
|
||||||
|
|
||||||
_applicationInfo = null;
|
_applicationInfo = null;
|
||||||
_voiceRegions = ImmutableDictionary.Create<string, RestVoiceRegion>();
|
_voiceRegions = ImmutableDictionary.Create<string, RestVoiceRegion>();
|
||||||
_downloadUsersFor.Clear();
|
_downloadUsersFor.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
=> await _connection.StartAsync().ConfigureAwait(false);
|
||||||
|
public async Task StopAsync()
|
||||||
|
=> await _connection.StopAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
/// <inheritdoc />
|
private async Task OnConnectingAsync()
|
||||||
public async Task ConnectAsync()
|
|
||||||
{
|
{
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await ConnectInternalAsync(false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
private async Task ConnectInternalAsync(bool isReconnecting)
|
|
||||||
{
|
|
||||||
if (LoginState != LoginState.LoggedIn)
|
|
||||||
throw new InvalidOperationException("Client is not logged in.");
|
|
||||||
|
|
||||||
if (!isReconnecting && _reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested)
|
|
||||||
_reconnectCancelToken.Cancel();
|
|
||||||
|
|
||||||
var state = ConnectionState;
|
|
||||||
if (state == ConnectionState.Connecting || state == ConnectionState.Connected)
|
|
||||||
await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (_connectionGroupLock != null)
|
if (_connectionGroupLock != null)
|
||||||
await _connectionGroupLock.WaitAsync().ConfigureAwait(false);
|
await _connectionGroupLock.WaitAsync(_connection.CancelToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_canReconnect = true;
|
await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false);
|
||||||
ConnectionState = ConnectionState.Connecting;
|
await ApiClient.ConnectAsync().ConfigureAwait(false);
|
||||||
await _gatewayLogger.InfoAsync("Connecting").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false);
|
||||||
|
await _connectedEvent.InvokeAsync().ConfigureAwait(false);
|
||||||
try
|
|
||||||
|
if (_sessionId != null)
|
||||||
{
|
{
|
||||||
var connectTask = new TaskCompletionSource<bool>();
|
await _gatewayLogger.DebugAsync("Resuming").ConfigureAwait(false);
|
||||||
_connectTask = connectTask;
|
await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false);
|
||||||
_cancelToken = new CancellationTokenSource();
|
|
||||||
|
|
||||||
//Abort connection on timeout
|
|
||||||
var _ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await Task.Delay(ConnectionTimeout).ConfigureAwait(false);
|
|
||||||
connectTask.TrySetException(new TimeoutException());
|
|
||||||
});
|
|
||||||
|
|
||||||
await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false);
|
|
||||||
await ApiClient.ConnectAsync().ConfigureAwait(false);
|
|
||||||
await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false);
|
|
||||||
await _connectedEvent.InvokeAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (_sessionId != null)
|
|
||||||
{
|
|
||||||
await _gatewayLogger.DebugAsync("Resuming").ConfigureAwait(false);
|
|
||||||
await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false);
|
|
||||||
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _connectTask.Task.ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false);
|
|
||||||
await SendStatusAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _gatewayLogger.DebugAsync("Raising Event").ConfigureAwait(false);
|
|
||||||
ConnectionState = ConnectionState.Connected;
|
|
||||||
await _gatewayLogger.InfoAsync("Connected").ConfigureAwait(false);
|
|
||||||
|
|
||||||
await ProcessUserDownloadsAsync(_downloadUsersFor.Select(x => GetGuild(x))
|
|
||||||
.Where(x => x != null).ToImmutableArray()).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
else
|
||||||
{
|
{
|
||||||
await DisconnectInternalAsync(null, isReconnecting).ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false);
|
||||||
throw;
|
await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Wait for READY
|
||||||
|
await _connection.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false);
|
||||||
|
await SendStatusAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
await ProcessUserDownloadsAsync(_downloadUsersFor.Select(x => GetGuild(x))
|
||||||
|
.Where(x => x != null).ToImmutableArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -250,41 +205,11 @@ namespace Discord.WebSocket
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// <inheritdoc />
|
private async Task OnDisconnectingAsync(Exception ex)
|
||||||
public async Task DisconnectAsync()
|
|
||||||
{
|
{
|
||||||
if (_connectTask?.TrySetCanceled() ?? false) return;
|
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await DisconnectInternalAsync(null, false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
private async Task DisconnectInternalAsync(Exception ex, bool isReconnecting)
|
|
||||||
{
|
|
||||||
if (!isReconnecting)
|
|
||||||
{
|
|
||||||
_canReconnect = false;
|
|
||||||
_sessionId = null;
|
|
||||||
_lastSeq = 0;
|
|
||||||
|
|
||||||
if (_reconnectCancelToken != null && !_reconnectCancelToken.IsCancellationRequested)
|
|
||||||
_reconnectCancelToken.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
ulong guildId;
|
ulong guildId;
|
||||||
|
|
||||||
if (ConnectionState == ConnectionState.Disconnected) return;
|
|
||||||
ConnectionState = ConnectionState.Disconnecting;
|
|
||||||
await _gatewayLogger.InfoAsync("Disconnecting").ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _gatewayLogger.DebugAsync("Cancelling current tasks").ConfigureAwait(false);
|
|
||||||
//Signal tasks to complete
|
|
||||||
try { _cancelToken.Cancel(); } catch { }
|
|
||||||
|
|
||||||
await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false);
|
||||||
//Disconnect from server
|
|
||||||
await ApiClient.DisconnectAsync().ConfigureAwait(false);
|
await ApiClient.DisconnectAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
//Wait for tasks to complete
|
//Wait for tasks to complete
|
||||||
@@ -294,8 +219,8 @@ namespace Discord.WebSocket
|
|||||||
await heartbeatTask.ConfigureAwait(false);
|
await heartbeatTask.ConfigureAwait(false);
|
||||||
_heartbeatTask = null;
|
_heartbeatTask = null;
|
||||||
|
|
||||||
long times;
|
long time;
|
||||||
while (_heartbeatTimes.TryDequeue(out times)) { }
|
while (_heartbeatTimes.TryDequeue(out time)) { }
|
||||||
_lastMessageTime = 0;
|
_lastMessageTime = 0;
|
||||||
|
|
||||||
await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false);
|
||||||
@@ -315,70 +240,6 @@ namespace Discord.WebSocket
|
|||||||
if (guild._available)
|
if (guild._available)
|
||||||
await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false);
|
await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionState = ConnectionState.Disconnected;
|
|
||||||
await _gatewayLogger.InfoAsync("Disconnected").ConfigureAwait(false);
|
|
||||||
|
|
||||||
await _disconnectedEvent.InvokeAsync(ex).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartReconnectAsync(Exception ex)
|
|
||||||
{
|
|
||||||
if ((ex as WebSocketClosedException)?.CloseCode == 4004) //Bad Token
|
|
||||||
{
|
|
||||||
_canReconnect = false;
|
|
||||||
_connectTask?.TrySetException(ex);
|
|
||||||
await LogoutAsync().ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!_canReconnect || _reconnectTask != null) return;
|
|
||||||
_reconnectCancelToken = new CancellationTokenSource();
|
|
||||||
_reconnectTask = ReconnectInternalAsync(ex, _reconnectCancelToken.Token);
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
private async Task ReconnectInternalAsync(Exception ex, CancellationToken cancelToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Random jitter = new Random();
|
|
||||||
int nextReconnectDelay = 1000;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
await Task.Delay(nextReconnectDelay, cancelToken).ConfigureAwait(false);
|
|
||||||
nextReconnectDelay = nextReconnectDelay * 2 + jitter.Next(-250, 250);
|
|
||||||
if (nextReconnectDelay > 60000)
|
|
||||||
nextReconnectDelay = 60000;
|
|
||||||
|
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (cancelToken.IsCancellationRequested) return;
|
|
||||||
await ConnectInternalAsync(true).ConfigureAwait(false);
|
|
||||||
_reconnectTask = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (Exception ex2)
|
|
||||||
{
|
|
||||||
await _gatewayLogger.WarningAsync("Reconnect failed", ex2).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
await _connectionLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _gatewayLogger.DebugAsync("Reconnect cancelled").ConfigureAwait(false);
|
|
||||||
_reconnectTask = null;
|
|
||||||
}
|
|
||||||
finally { _connectionLock.Release(); }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -555,7 +416,7 @@ namespace Discord.WebSocket
|
|||||||
await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Received Hello").ConfigureAwait(false);
|
||||||
var data = (payload as JToken).ToObject<HelloEvent>(_serializer);
|
var data = (payload as JToken).ToObject<HelloEvent>(_serializer);
|
||||||
|
|
||||||
_heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token, _gatewayLogger);
|
_heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GatewayOpCode.Heartbeat:
|
case GatewayOpCode.Heartbeat:
|
||||||
@@ -593,9 +454,7 @@ namespace Discord.WebSocket
|
|||||||
case GatewayOpCode.Reconnect:
|
case GatewayOpCode.Reconnect:
|
||||||
{
|
{
|
||||||
await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false);
|
||||||
await _gatewayLogger.WarningAsync("Server requested a reconnect").ConfigureAwait(false);
|
_connection.Error(new Exception("Server requested a reconnect"));
|
||||||
|
|
||||||
await StartReconnectAsync(new Exception("Server requested a reconnect")).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case GatewayOpCode.Dispatch:
|
case GatewayOpCode.Dispatch:
|
||||||
@@ -633,8 +492,7 @@ namespace Discord.WebSocket
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_canReconnect = false;
|
_connection.CriticalError(new Exception("Processing READY failed", ex));
|
||||||
_connectTask.TrySetException(new Exception("Processing READY failed", ex));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,11 +500,11 @@ namespace Discord.WebSocket
|
|||||||
await SyncGuildsAsync().ConfigureAwait(false);
|
await SyncGuildsAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
_lastGuildAvailableTime = Environment.TickCount;
|
_lastGuildAvailableTime = Environment.TickCount;
|
||||||
_guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token, _gatewayLogger);
|
_guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger);
|
||||||
|
|
||||||
await _readyEvent.InvokeAsync().ConfigureAwait(false);
|
await _readyEvent.InvokeAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete
|
var _ = _connection.CompleteAsync();
|
||||||
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false);
|
await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -654,7 +512,7 @@ namespace Discord.WebSocket
|
|||||||
{
|
{
|
||||||
await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false);
|
||||||
|
|
||||||
var _ = _connectTask.TrySetResultAsync(true); //Signal the .Connect() call to complete
|
var _ = _connection.CompleteAsync();
|
||||||
|
|
||||||
//Notify the client that these guilds are available again
|
//Notify the client that these guilds are available again
|
||||||
foreach (var guild in State.Guilds)
|
foreach (var guild in State.Guilds)
|
||||||
@@ -1356,7 +1214,6 @@ namespace Discord.WebSocket
|
|||||||
SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage;
|
SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage;
|
||||||
var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly);
|
var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly);
|
||||||
SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user));
|
SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user));
|
||||||
|
|
||||||
if (cachedMsg != null)
|
if (cachedMsg != null)
|
||||||
{
|
{
|
||||||
cachedMsg.AddReaction(reaction);
|
cachedMsg.AddReaction(reaction);
|
||||||
@@ -1691,11 +1548,11 @@ namespace Discord.WebSocket
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken, Logger logger)
|
private async Task RunHeartbeatAsync(int intervalMillis, CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await logger.DebugAsync("Heartbeat Started").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false);
|
||||||
while (!cancelToken.IsCancellationRequested)
|
while (!cancelToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var now = Environment.TickCount;
|
var now = Environment.TickCount;
|
||||||
@@ -1705,8 +1562,7 @@ namespace Discord.WebSocket
|
|||||||
{
|
{
|
||||||
if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true))
|
if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true))
|
||||||
{
|
{
|
||||||
await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false);
|
_connection.Error(new Exception("Server missed last heartbeat"));
|
||||||
await StartReconnectAsync(new Exception("Server missed last heartbeat")).ConfigureAwait(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1718,20 +1574,20 @@ namespace Discord.WebSocket
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await logger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false);
|
await _gatewayLogger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false);
|
await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
await logger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
await logger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false);
|
await _gatewayLogger.DebugAsync("Heartbeat Stopped").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
await logger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false);
|
await _gatewayLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async Task WaitForGuildsAsync()
|
public async Task WaitForGuildsAsync()
|
||||||
@@ -1805,8 +1661,7 @@ namespace Discord.WebSocket
|
|||||||
}
|
}
|
||||||
|
|
||||||
//IDiscordClient
|
//IDiscordClient
|
||||||
Task IDiscordClient.ConnectAsync()
|
ConnectionState IDiscordClient.ConnectionState => _connection.State;
|
||||||
=> ConnectAsync();
|
|
||||||
|
|
||||||
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync()
|
async Task<IApplication> IDiscordClient.GetApplicationInfoAsync()
|
||||||
=> await GetApplicationInfoAsync().ConfigureAwait(false);
|
=> await GetApplicationInfoAsync().ConfigureAwait(false);
|
||||||
@@ -1842,5 +1697,10 @@ namespace Discord.WebSocket
|
|||||||
=> Task.FromResult<IReadOnlyCollection<IVoiceRegion>>(VoiceRegions);
|
=> Task.FromResult<IReadOnlyCollection<IVoiceRegion>>(VoiceRegions);
|
||||||
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id)
|
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id)
|
||||||
=> Task.FromResult<IVoiceRegion>(GetVoiceRegion(id));
|
=> Task.FromResult<IVoiceRegion>(GetVoiceRegion(id));
|
||||||
|
|
||||||
|
async Task IDiscordClient.StartAsync()
|
||||||
|
=> await StartAsync().ConfigureAwait(false);
|
||||||
|
async Task IDiscordClient.StopAsync()
|
||||||
|
=> await StopAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace Discord.WebSocket
|
|||||||
{
|
{
|
||||||
public const string GatewayEncoding = "json";
|
public const string GatewayEncoding = "json";
|
||||||
|
|
||||||
/// <summary> Gets or sets the websocket host to connect to. If null, the client will use the /gateway endpoint.
|
/// <summary> Gets or sets the websocket host to connect to. If null, the client will use the /gateway endpoint. </summary>
|
||||||
public string GatewayHost { get; set; } = null;
|
public string GatewayHost { get; set; } = null;
|
||||||
|
|
||||||
/// <summary> Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. </summary>
|
/// <summary> Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. </summary>
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ namespace Discord.WebSocket
|
|||||||
_audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection
|
_audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection
|
||||||
_audioConnectPromise = null;
|
_audioConnectPromise = null;
|
||||||
if (_audioClient != null)
|
if (_audioClient != null)
|
||||||
await _audioClient.DisconnectAsync().ConfigureAwait(false);
|
await _audioClient.StopAsync().ConfigureAwait(false);
|
||||||
_audioClient = null;
|
_audioClient = null;
|
||||||
}
|
}
|
||||||
internal async Task FinishConnectAudio(int id, string url, string token)
|
internal async Task FinishConnectAudio(int id, string url, string token)
|
||||||
@@ -517,7 +517,6 @@ namespace Discord.WebSocket
|
|||||||
var promise = _audioConnectPromise;
|
var promise = _audioConnectPromise;
|
||||||
audioClient.Disconnected += async ex =>
|
audioClient.Disconnected += async ex =>
|
||||||
{
|
{
|
||||||
//If the initial connection hasn't been made yet, reconnecting will lead to deadlocks
|
|
||||||
if (!promise.Task.IsCompleted)
|
if (!promise.Task.IsCompleted)
|
||||||
{
|
{
|
||||||
try { audioClient.Dispose(); } catch { }
|
try { audioClient.Dispose(); } catch { }
|
||||||
@@ -528,41 +527,15 @@ namespace Discord.WebSocket
|
|||||||
await promise.TrySetCanceledAsync();
|
await promise.TrySetCanceledAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Implement reconnect
|
|
||||||
/*await _audioLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (AudioClient == audioClient) //Only reconnect if we're still assigned as this guild's audio client
|
|
||||||
{
|
|
||||||
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(Discord.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);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try { audioClient.Dispose(); } catch { }
|
|
||||||
AudioClient = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_audioLock.Release();
|
|
||||||
}*/
|
|
||||||
};
|
};
|
||||||
_audioClient = audioClient;
|
_audioClient = audioClient;
|
||||||
}
|
}
|
||||||
await _audioClient.ConnectAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false);
|
_audioClient.Connected += () =>
|
||||||
await _audioConnectPromise.TrySetResultAsync(_audioClient).ConfigureAwait(false);
|
{
|
||||||
|
var _ = _audioConnectPromise.TrySetResultAsync(_audioClient);
|
||||||
|
return Task.Delay(0);
|
||||||
|
};
|
||||||
|
await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user