Improved redirect opcode support, fixed connect deadlock on unstable connections
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
<ProjectGuid>{1B5603B4-6F8F-4289-B945-7BAAE523D740}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Discord.Net</RootNamespace>
|
||||
<RootNamespace>Discord</RootNamespace>
|
||||
<AssemblyName>Discord.Net.Commands</AssemblyName>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<ProjectGuid>{8D71A857-879A-4A10-859E-5FF824ED6688}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Discord.Net</RootNamespace>
|
||||
<RootNamespace>Discord</RootNamespace>
|
||||
<AssemblyName>Discord.Net</AssemblyName>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
@@ -159,6 +159,9 @@
|
||||
<Compile Include="..\Discord.Net\Net\API\Endpoints.cs">
|
||||
<Link>Net\API\Endpoints.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Discord.Net\Net\API\HttpException.cs">
|
||||
<Link>Net\API\HttpException.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Discord.Net\Net\API\Requests.cs">
|
||||
<Link>Net\API\Requests.cs</Link>
|
||||
</Compile>
|
||||
@@ -177,9 +180,6 @@
|
||||
<Compile Include="..\Discord.Net\Net\API\RestClient.SharpRest.cs">
|
||||
<Link>Net\API\RestClient.SharpRest.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Discord.Net\Net\HttpException.cs">
|
||||
<Link>Net\HttpException.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Discord.Net\Net\WebSockets\Commands.cs">
|
||||
<Link>Net\WebSockets\Commands.cs</Link>
|
||||
</Compile>
|
||||
@@ -187,7 +187,7 @@
|
||||
<Link>Net\WebSockets\DataWebSocket.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Discord.Net\Net\WebSockets\DataWebSockets.Events.cs">
|
||||
<Link>Net\DataWebSockets.Events.cs</Link>
|
||||
<Link>Net\WebSockets\DataWebSockets.Events.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Discord.Net\Net\WebSockets\Events.cs">
|
||||
<Link>Net\WebSockets\Events.cs</Link>
|
||||
@@ -216,6 +216,9 @@
|
||||
<Compile Include="..\Discord.Net\Net\WebSockets\WebSocketMessage.cs">
|
||||
<Link>Net\WebSockets\WebSocketMessage.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Discord.Net\TimeoutException.cs">
|
||||
<Link>TimeoutException.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace Discord
|
||||
_api = new DiscordAPIClient(_config.LogLevel);
|
||||
_dataSocket = new DataWebSocket(this);
|
||||
_dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); };
|
||||
_dataSocket.Disconnected += async (s, e) => { RaiseDisconnected(e); if (e.WasUnexpected) await Reconnect(_token); };
|
||||
_dataSocket.Disconnected += async (s, e) => { RaiseDisconnected(e); if (e.WasUnexpected) await _dataSocket.Login(_token); };
|
||||
if (_config.EnableVoice)
|
||||
{
|
||||
_voiceSocket = new VoiceWebSocket(this);
|
||||
@@ -112,7 +112,7 @@ namespace Discord
|
||||
}
|
||||
RaiseVoiceDisconnected(e);
|
||||
if (e.WasUnexpected)
|
||||
await _voiceSocket.Reconnect(_cancelToken);
|
||||
await _voiceSocket.Reconnect();
|
||||
};
|
||||
_voiceSocket.IsSpeaking += (s, e) =>
|
||||
{
|
||||
@@ -292,6 +292,8 @@ namespace Discord
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "RESUMED":
|
||||
break;
|
||||
|
||||
//Servers
|
||||
case "GUILD_CREATE":
|
||||
@@ -358,7 +360,7 @@ namespace Discord
|
||||
var user = _users.GetOrAdd(data.User.Id);
|
||||
user.Update(data.User);
|
||||
if (_config.TrackActivity)
|
||||
user.UpdateActivity(DateTime.UtcNow);
|
||||
user.UpdateActivity();
|
||||
var member = _members.GetOrAdd(data.User.Id, data.GuildId);
|
||||
member.Update(data);
|
||||
RaiseUserAdded(member);
|
||||
@@ -536,7 +538,7 @@ namespace Discord
|
||||
if (user != null)
|
||||
{
|
||||
if (_config.TrackActivity)
|
||||
user.UpdateActivity(DateTime.UtcNow);
|
||||
user.UpdateActivity();
|
||||
if (channel != null)
|
||||
RaiseUserIsTyping(user, channel);
|
||||
}
|
||||
@@ -550,8 +552,8 @@ namespace Discord
|
||||
var server = _servers[data.GuildId];
|
||||
if (_config.EnableVoice)
|
||||
{
|
||||
string host = "wss://" + data.Endpoint.Split(':')[0];
|
||||
await _voiceSocket.Login(host, data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false);
|
||||
_voiceSocket.Host = "wss://" + data.Endpoint.Split(':')[0];
|
||||
await _voiceSocket.Login(data.GuildId, _currentUserId, _dataSocket.SessionId, data.Token, _cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -582,11 +584,6 @@ namespace Discord
|
||||
};
|
||||
}
|
||||
|
||||
private void _dataSocket_Connected(object sender, EventArgs e)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
//Connection
|
||||
/// <summary> Connects to the Discord server with the provided token. </summary>
|
||||
public async Task Connect(string token)
|
||||
@@ -594,46 +591,54 @@ namespace Discord
|
||||
if (_state != (int)DiscordClientState.Disconnected)
|
||||
await Disconnect().ConfigureAwait(false);
|
||||
|
||||
if (_config.LogLevel >= LogMessageSeverity.Verbose)
|
||||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Using cached token.");
|
||||
|
||||
await ConnectInternal(token).ConfigureAwait(false);
|
||||
}
|
||||
await ConnectInternal(token)
|
||||
.Timeout(_config.ConnectionTimeout)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
/// <summary> Connects to the Discord server with the provided email and password. </summary>
|
||||
/// <returns> Returns a token for future connections. </returns>
|
||||
public async Task<string> Connect(string email, string password)
|
||||
{
|
||||
if (_state != (int)DiscordClientState.Disconnected)
|
||||
await Disconnect().ConfigureAwait(false);
|
||||
|
||||
var response = await _api.Login(email, password).ConfigureAwait(false);
|
||||
if (_config.LogLevel >= LogMessageSeverity.Verbose)
|
||||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, "Login successful, got token.");
|
||||
|
||||
return await ConnectInternal(response.Token).ConfigureAwait(false);
|
||||
}
|
||||
private Task Reconnect(string token)
|
||||
{
|
||||
if (_config.LogLevel >= LogMessageSeverity.Verbose)
|
||||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Using cached token.");
|
||||
|
||||
return ConnectInternal(token);
|
||||
string token;
|
||||
try
|
||||
{
|
||||
var cancelToken = new CancellationTokenSource();
|
||||
cancelToken.CancelAfter(5000);
|
||||
_api.CancelToken = cancelToken.Token;
|
||||
var response = await _api.Login(email, password).ConfigureAwait(false);
|
||||
token = response.Token;
|
||||
if (_config.LogLevel >= LogMessageSeverity.Verbose)
|
||||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, "Login successful, got token.");
|
||||
}
|
||||
catch (TaskCanceledException) { throw new TimeoutException(); }
|
||||
|
||||
return await ConnectInternal(token)
|
||||
.Timeout(_config.ConnectionTimeout)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
private async Task<string> ConnectInternal(string token)
|
||||
{
|
||||
try
|
||||
try
|
||||
{
|
||||
_disconnectedEvent.Reset();
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
_cancelToken = _cancelTokenSource.Token;
|
||||
_state = (int)DiscordClientState.Connecting;
|
||||
|
||||
_api.Token = token;
|
||||
_api.CancelToken = _cancelToken;
|
||||
_token = token;
|
||||
_state = (int)DiscordClientState.Connecting;
|
||||
|
||||
string url = (await _api.GetWebSocketEndpoint().ConfigureAwait(false)).Url;
|
||||
url = "wss://gateway-besaid.discord.gg/";
|
||||
if (_config.LogLevel >= LogMessageSeverity.Verbose)
|
||||
RaiseOnLog(LogMessageSeverity.Verbose, LogMessageSource.Authentication, $"Websocket endpoint: {url}");
|
||||
|
||||
await _dataSocket.Login(url, token, _cancelToken).ConfigureAwait(false);
|
||||
|
||||
_dataSocket.Host = url;
|
||||
_dataSocket.ParentCancelToken = _cancelToken;
|
||||
await _dataSocket.Login(token).ConfigureAwait(false);
|
||||
|
||||
_runTask = RunTasks();
|
||||
|
||||
@@ -641,8 +646,7 @@ namespace Discord
|
||||
{
|
||||
//Cancel if either Disconnect is called, data socket errors or timeout is reached
|
||||
var cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, _dataSocket.CancelToken).Token;
|
||||
if (!_connectedEvent.Wait(_config.ConnectionTimeout, cancelToken))
|
||||
throw new Exception("Operation timed out.");
|
||||
_connectedEvent.Wait(cancelToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -656,6 +660,7 @@ namespace Discord
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
await Disconnect().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -13,5 +13,26 @@ namespace Discord.Helpers
|
||||
CompletedTask = Task.Delay(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static async Task Timeout(this Task self, int milliseconds)
|
||||
{
|
||||
Task timeoutTask = Task.Delay(milliseconds);
|
||||
Task finishedTask = await Task.WhenAny(self, timeoutTask);
|
||||
if (finishedTask == timeoutTask)
|
||||
{
|
||||
throw new TimeoutException();
|
||||
}
|
||||
else
|
||||
await self;
|
||||
}
|
||||
public static async Task<T> Timeout<T>(this Task<T> self, int milliseconds)
|
||||
{
|
||||
Task timeoutTask = Task.Delay(milliseconds);
|
||||
Task finishedTask = await Task.WhenAny(self, timeoutTask).ConfigureAwait(false);
|
||||
if (finishedTask == timeoutTask)
|
||||
throw new TimeoutException();
|
||||
else
|
||||
return await self.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Net.API
|
||||
@@ -21,6 +22,12 @@ namespace Discord.Net.API
|
||||
get { return _token; }
|
||||
set { _token = value; _rest.SetToken(value); }
|
||||
}
|
||||
private CancellationToken _cancelToken;
|
||||
public CancellationToken CancelToken
|
||||
{
|
||||
get { return _cancelToken; }
|
||||
set { _cancelToken = value; _rest.SetCancelToken(value); }
|
||||
}
|
||||
|
||||
//Auth
|
||||
public Task<Responses.Gateway> GetWebSocketEndpoint()
|
||||
@@ -191,5 +198,15 @@ namespace Discord.Net.API
|
||||
var request = new Requests.ChangeAvatar { Avatar = $"data:{type},/9j/{base64}", CurrentEmail = currentEmail, CurrentPassword = currentPassword };
|
||||
return _rest.Patch<Responses.ChangeProfile>(Endpoints.UserMe, request);
|
||||
}
|
||||
|
||||
//Other
|
||||
/*public Task<Responses.Status> GetUnresolvedIncidents()
|
||||
{
|
||||
return _rest.Get<Responses.Status>(Endpoints.StatusUnresolvedMaintenance);
|
||||
}
|
||||
public Task<Responses.Status> GetActiveIncidents()
|
||||
{
|
||||
return _rest.Get<Responses.Status>(Endpoints.StatusActiveMaintenance);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
namespace Discord.Net.API
|
||||
{
|
||||
internal static class Endpoints
|
||||
{
|
||||
{
|
||||
public const string BaseStatusApi = "https://status.discordapp.com/api/v2/";
|
||||
public const string BaseApi = "https://discordapp.com/api/";
|
||||
//public const string Track = "track";
|
||||
public const string Gateway = "gateway";
|
||||
@@ -41,5 +42,8 @@
|
||||
public const string Voice = "voice";
|
||||
public const string VoiceRegions = "voice/regions";
|
||||
public const string VoiceIce = "voice/ice";
|
||||
}
|
||||
|
||||
public const string StatusActiveMaintenance = "scheduled-maintenances/active.json";
|
||||
public const string StatusUnresolvedMaintenance = "scheduled-maintenances/unresolved.json";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Discord.Net
|
||||
namespace Discord.Net.API
|
||||
{
|
||||
public class HttpException : Exception
|
||||
{
|
||||
@@ -7,8 +7,7 @@ using System.Threading.Tasks;
|
||||
namespace Discord.Net.WebSockets
|
||||
{
|
||||
internal partial class DataWebSocket : WebSocket
|
||||
{
|
||||
private string _redirectServer;
|
||||
{
|
||||
private int _lastSeq;
|
||||
|
||||
public string SessionId => _sessionId;
|
||||
@@ -18,29 +17,25 @@ namespace Discord.Net.WebSockets
|
||||
: base(client)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task Login(string host, string token, CancellationToken cancelToken)
|
||||
|
||||
public async Task Login(string token)
|
||||
{
|
||||
await base.Connect(host, cancelToken);
|
||||
await Connect();
|
||||
|
||||
Commands.Login msg = new Commands.Login();
|
||||
msg.Payload.Token = token;
|
||||
msg.Payload.Properties["$device"] = "Discord.Net";
|
||||
QueueMessage(msg);
|
||||
}
|
||||
|
||||
protected override Task[] Run()
|
||||
private async Task Redirect(string server)
|
||||
{
|
||||
//Send resume session if we were transferred
|
||||
if (_redirectServer != null)
|
||||
{
|
||||
var resumeMsg = new Commands.Resume();
|
||||
resumeMsg.Payload.SessionId = _sessionId;
|
||||
resumeMsg.Payload.Sequence = _lastSeq;
|
||||
QueueMessage(resumeMsg);
|
||||
_redirectServer = null;
|
||||
}
|
||||
return base.Run();
|
||||
await DisconnectInternal(isUnexpected: false);
|
||||
await Connect();
|
||||
|
||||
var resumeMsg = new Commands.Resume();
|
||||
resumeMsg.Payload.SessionId = _sessionId;
|
||||
resumeMsg.Payload.Sequence = _lastSeq;
|
||||
QueueMessage(resumeMsg);
|
||||
}
|
||||
|
||||
protected override async Task ProcessMessage(string json)
|
||||
@@ -54,27 +49,31 @@ namespace Discord.Net.WebSockets
|
||||
case 0:
|
||||
{
|
||||
JToken token = msg.Payload as JToken;
|
||||
if (msg.Type == "READY")
|
||||
if (msg.Type == "READY")
|
||||
{
|
||||
var payload = token.ToObject<Events.Ready>();
|
||||
_sessionId = payload.SessionId;
|
||||
_heartbeatInterval = payload.HeartbeatInterval;
|
||||
QueueMessage(new Commands.UpdateStatus());
|
||||
}
|
||||
else if (msg.Type == "RESUMED")
|
||||
{
|
||||
var payload = token.ToObject<Events.Resumed>();
|
||||
_heartbeatInterval = payload.HeartbeatInterval;
|
||||
QueueMessage(new Commands.UpdateStatus());
|
||||
}
|
||||
RaiseReceivedEvent(msg.Type, token);
|
||||
if (msg.Type == "READY")
|
||||
if (msg.Type == "READY" || msg.Type == "RESUMED")
|
||||
CompleteConnect();
|
||||
/*if (_logLevel >= LogMessageSeverity.Info)
|
||||
RaiseOnLog(LogMessageSeverity.Info, "Got Event: " + msg.Type);*/
|
||||
}
|
||||
break;
|
||||
case 7: //Redirect
|
||||
{
|
||||
var payload = (msg.Payload as JToken).ToObject<Events.Redirect>();
|
||||
_host = payload.Url;
|
||||
Host = payload.Url;
|
||||
if (_logLevel >= LogMessageSeverity.Info)
|
||||
RaiseOnLog(LogMessageSeverity.Info, "Redirected to " + payload.Url);
|
||||
await DisconnectInternal(new Exception("Server is redirecting."), true);
|
||||
await Redirect(payload.Url);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -36,6 +36,11 @@ namespace Discord.Net.WebSockets
|
||||
[JsonProperty(PropertyName = "heartbeat_interval")]
|
||||
public int HeartbeatInterval;
|
||||
}
|
||||
public sealed class Resumed
|
||||
{
|
||||
[JsonProperty(PropertyName = "heartbeat_interval")]
|
||||
public int HeartbeatInterval;
|
||||
}
|
||||
|
||||
public sealed class Redirect
|
||||
{
|
||||
|
||||
@@ -53,13 +53,12 @@ namespace Discord.Net.WebSockets
|
||||
_targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames
|
||||
}
|
||||
|
||||
public async Task Login(string host, string serverId, string userId, string sessionId, string token, CancellationToken cancelToken)
|
||||
public async Task Login(string serverId, string userId, string sessionId, string token, CancellationToken cancelToken)
|
||||
{
|
||||
if (_serverId == serverId && _userId == userId && _sessionId == sessionId && _token == token)
|
||||
{
|
||||
//Adjust the host and tell the system to reconnect
|
||||
_host = host;
|
||||
await DisconnectInternal(new Exception("Server transfer occurred."));
|
||||
await DisconnectInternal(new Exception("Server transfer occurred."), isUnexpected: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,18 +67,19 @@ namespace Discord.Net.WebSockets
|
||||
_sessionId = sessionId;
|
||||
_token = token;
|
||||
|
||||
await Connect(host, cancelToken);
|
||||
await Connect();
|
||||
}
|
||||
public async Task Reconnect(CancellationToken cancelToken)
|
||||
public async Task Reconnect()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false);
|
||||
var cancelToken = ParentCancelToken;
|
||||
await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false);
|
||||
while (!cancelToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Connect(_host, cancelToken).ConfigureAwait(false);
|
||||
await Connect().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
@@ -295,7 +295,7 @@ namespace Discord.Net.WebSockets
|
||||
var payload = (msg.Payload as JToken).ToObject<VoiceEvents.Ready>();
|
||||
_heartbeatInterval = payload.HeartbeatInterval;
|
||||
_ssrc = payload.SSRC;
|
||||
_endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(_host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port);
|
||||
_endpoint = new IPEndPoint((await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(), payload.Port);
|
||||
//_mode = payload.Modes.LastOrDefault();
|
||||
_isEncrypted = !payload.Modes.Contains("plain");
|
||||
_udp.Connect(_endpoint);
|
||||
|
||||
@@ -38,7 +38,8 @@ namespace Discord.Net.WebSockets
|
||||
protected readonly DiscordClient _client;
|
||||
protected readonly LogMessageSeverity _logLevel;
|
||||
|
||||
protected string _host;
|
||||
public string Host { get; set; }
|
||||
|
||||
protected int _loginTimeout, _heartbeatInterval;
|
||||
private DateTime _lastHeartbeat;
|
||||
private Task _runTask;
|
||||
@@ -49,6 +50,7 @@ namespace Discord.Net.WebSockets
|
||||
protected ExceptionDispatchInfo _disconnectReason;
|
||||
private bool _wasDisconnectUnexpected;
|
||||
|
||||
public CancellationToken ParentCancelToken { get; set; }
|
||||
public CancellationToken CancelToken => _cancelToken;
|
||||
private CancellationTokenSource _cancelTokenSource;
|
||||
protected CancellationToken _cancelToken;
|
||||
@@ -69,22 +71,24 @@ namespace Discord.Net.WebSockets
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual async Task Connect(string host, CancellationToken cancelToken)
|
||||
protected virtual async Task Connect()
|
||||
{
|
||||
if (_state != (int)WebSocketState.Disconnected)
|
||||
throw new InvalidOperationException("Client is already connected or connecting to the server.");
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
await Disconnect().ConfigureAwait(false);
|
||||
|
||||
_state = (int)WebSocketState.Connecting;
|
||||
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token;
|
||||
if (ParentCancelToken != null)
|
||||
_cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken).Token;
|
||||
else
|
||||
_cancelToken = _cancelTokenSource.Token;
|
||||
|
||||
await _engine.Connect(host, _cancelToken).ConfigureAwait(false);
|
||||
_host = host;
|
||||
await _engine.Connect(Host, _cancelToken).ConfigureAwait(false);
|
||||
_lastHeartbeat = DateTime.UtcNow;
|
||||
|
||||
_runTask = RunTasks();
|
||||
|
||||
16
src/Discord.Net/TimeoutException.cs
Normal file
16
src/Discord.Net/TimeoutException.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
public sealed class TimeoutException : Exception
|
||||
{
|
||||
internal TimeoutException()
|
||||
: base("An operation has timed out.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user