@@ -22,6 +22,12 @@ namespace Discord | |||||
VoiceWebSocket, | VoiceWebSocket, | ||||
} | } | ||||
public class DisconnectedEventArgs : EventArgs | |||||
{ | |||||
public readonly bool WasUnexpected; | |||||
public readonly Exception Error; | |||||
internal DisconnectedEventArgs(bool wasUnexpected, Exception error) { WasUnexpected = wasUnexpected; Error = error; } | |||||
} | |||||
public sealed class LogMessageEventArgs : EventArgs | public sealed class LogMessageEventArgs : EventArgs | ||||
{ | { | ||||
public LogMessageSeverity Severity { get; } | public LogMessageSeverity Severity { get; } | ||||
@@ -30,6 +36,7 @@ namespace Discord | |||||
internal LogMessageEventArgs(LogMessageSeverity severity, LogMessageSource source, string msg) { Severity = severity; Source = source; Message = msg; } | internal LogMessageEventArgs(LogMessageSeverity severity, LogMessageSource source, string msg) { Severity = severity; Source = source; Message = msg; } | ||||
} | } | ||||
public sealed class ServerEventArgs : EventArgs | public sealed class ServerEventArgs : EventArgs | ||||
{ | { | ||||
public Server Server { get; } | public Server Server { get; } | ||||
@@ -136,11 +143,11 @@ namespace Discord | |||||
if (Connected != null) | if (Connected != null) | ||||
Connected(this, EventArgs.Empty); | Connected(this, EventArgs.Empty); | ||||
} | } | ||||
public event EventHandler Disconnected; | |||||
private void RaiseDisconnected() | |||||
public event EventHandler<DisconnectedEventArgs> Disconnected; | |||||
private void RaiseDisconnected(DisconnectedEventArgs e) | |||||
{ | { | ||||
if (Disconnected != null) | if (Disconnected != null) | ||||
Disconnected(this, EventArgs.Empty); | |||||
Disconnected(this, e); | |||||
} | } | ||||
public event EventHandler<LogMessageEventArgs> LogMessage; | public event EventHandler<LogMessageEventArgs> LogMessage; | ||||
internal void RaiseOnLog(LogMessageSeverity severity, LogMessageSource source, string message) | internal void RaiseOnLog(LogMessageSeverity severity, LogMessageSource source, string message) | ||||
@@ -308,11 +315,11 @@ namespace Discord | |||||
if (VoiceConnected != null) | if (VoiceConnected != null) | ||||
VoiceConnected(this, EventArgs.Empty); | VoiceConnected(this, EventArgs.Empty); | ||||
} | } | ||||
public event EventHandler VoiceDisconnected; | |||||
private void RaiseVoiceDisconnected() | |||||
public event EventHandler<DisconnectedEventArgs> VoiceDisconnected; | |||||
private void RaiseVoiceDisconnected(DisconnectedEventArgs e) | |||||
{ | { | ||||
if (VoiceDisconnected != null) | if (VoiceDisconnected != null) | ||||
VoiceDisconnected(this, EventArgs.Empty); | |||||
VoiceDisconnected(this, e); | |||||
} | } | ||||
/*public event EventHandler<VoiceServerUpdatedEventArgs> VoiceServerChanged; | /*public event EventHandler<VoiceServerUpdatedEventArgs> VoiceServerChanged; | ||||
private void RaiseVoiceServerUpdated(Server server, string endpoint) | private void RaiseVoiceServerUpdated(Server server, string endpoint) | ||||
@@ -38,7 +38,10 @@ namespace Discord | |||||
public void SendVoicePCM(byte[] data, int count) | public void SendVoicePCM(byte[] data, int count) | ||||
{ | { | ||||
CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
if (data == null) throw new ArgumentException(nameof(data)); | |||||
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); | |||||
if (count == 0) return; | if (count == 0) return; | ||||
_voiceSocket.SendPCMFrames(data, count); | _voiceSocket.SendPCMFrames(data, count); | ||||
} | } | ||||
@@ -46,6 +49,7 @@ namespace Discord | |||||
public void ClearVoicePCM() | public void ClearVoicePCM() | ||||
{ | { | ||||
CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
_voiceSocket.ClearPCMFrames(); | _voiceSocket.ClearPCMFrames(); | ||||
} | } | ||||
@@ -53,6 +57,7 @@ namespace Discord | |||||
public async Task WaitVoice() | public async Task WaitVoice() | ||||
{ | { | ||||
CheckReady(checkVoice: true); | CheckReady(checkVoice: true); | ||||
_voiceSocket.Wait(); | _voiceSocket.Wait(); | ||||
await TaskHelper.CompletedTask.ConfigureAwait(false); | await TaskHelper.CompletedTask.ConfigureAwait(false); | ||||
} | } | ||||
@@ -35,6 +35,7 @@ namespace Discord | |||||
private Task _runTask; | private Task _runTask; | ||||
protected ExceptionDispatchInfo _disconnectReason; | protected ExceptionDispatchInfo _disconnectReason; | ||||
private bool _wasDisconnectUnexpected; | private bool _wasDisconnectUnexpected; | ||||
private string _token; | |||||
/// <summary> Returns the id of the current logged-in user. </summary> | /// <summary> Returns the id of the current logged-in user. </summary> | ||||
public string CurrentUserId => _currentUserId; | public string CurrentUserId => _currentUserId; | ||||
@@ -86,6 +87,7 @@ namespace Discord | |||||
_config.Lock(); | _config.Lock(); | ||||
_state = (int)DiscordClientState.Disconnected; | _state = (int)DiscordClientState.Disconnected; | ||||
_cancelToken = new CancellationToken(true); | |||||
_disconnectedEvent = new ManualResetEvent(true); | _disconnectedEvent = new ManualResetEvent(true); | ||||
_connectedEvent = new ManualResetEventSlim(false); | _connectedEvent = new ManualResetEventSlim(false); | ||||
_rand = new Random(); | _rand = new Random(); | ||||
@@ -93,8 +95,13 @@ namespace Discord | |||||
_api = new DiscordAPIClient(_config.LogLevel); | _api = new DiscordAPIClient(_config.LogLevel); | ||||
_dataSocket = new DataWebSocket(this); | _dataSocket = new DataWebSocket(this); | ||||
_dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); }; | _dataSocket.Connected += (s, e) => { if (_state == (int)DiscordClientState.Connecting) CompleteConnect(); }; | ||||
_dataSocket.Disconnected += async (s, e) => { RaiseDisconnected(e); if (e.WasUnexpected) await Connect(_token); /*await _dataSocket.Reconnect(_cancelToken);*/ }; | |||||
if (_config.EnableVoice) | if (_config.EnableVoice) | ||||
{ | |||||
_voiceSocket = new VoiceWebSocket(this); | _voiceSocket = new VoiceWebSocket(this); | ||||
_voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); | |||||
_voiceSocket.Disconnected += async (s, e) => { RaiseVoiceDisconnected(e); if (e.WasUnexpected) await _voiceSocket.Reconnect(_cancelToken); }; | |||||
} | |||||
_channels = new Channels(this); | _channels = new Channels(this); | ||||
_members = new Members(this); | _members = new Members(this); | ||||
@@ -108,10 +115,6 @@ namespace Discord | |||||
_voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.VoiceWebSocket, e.Message); | _voiceSocket.LogMessage += (s, e) => RaiseOnLog(e.Severity, LogMessageSource.VoiceWebSocket, e.Message); | ||||
if (_config.LogLevel >= LogMessageSeverity.Info) | if (_config.LogLevel >= LogMessageSeverity.Info) | ||||
{ | { | ||||
Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, "Connected"); | |||||
Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, "Disconnected"); | |||||
VoiceConnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, $"Voice Connected"); | |||||
VoiceDisconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.Client, $"Voice Disconnected"); | |||||
_dataSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected"); | _dataSocket.Connected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Connected"); | ||||
_dataSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected"); | _dataSocket.Disconnected += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, "Disconnected"); | ||||
//_dataSocket.ReceivedEvent += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, $"Received {e.Type}"); | //_dataSocket.ReceivedEvent += (s, e) => RaiseOnLog(LogMessageSeverity.Info, LogMessageSource.DataWebSocket, $"Received {e.Type}"); | ||||
@@ -604,6 +607,7 @@ namespace Discord | |||||
} | } | ||||
//_state = (int)DiscordClientState.Connected; | //_state = (int)DiscordClientState.Connected; | ||||
_token = token; | |||||
return token; | return token; | ||||
} | } | ||||
catch | catch | ||||
@@ -616,23 +620,24 @@ namespace Discord | |||||
{ | { | ||||
_state = (int)WebSocketState.Connected; | _state = (int)WebSocketState.Connected; | ||||
_connectedEvent.Set(); | _connectedEvent.Set(); | ||||
RaiseConnected(); | |||||
} | } | ||||
/// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> | /// <summary> Disconnects from the Discord server, canceling any pending requests. </summary> | ||||
public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | ||||
protected async Task DisconnectInternal(Exception ex, bool isUnexpected = true, bool skipAwait = false) | |||||
protected Task DisconnectInternal(Exception ex, bool isUnexpected = true, bool skipAwait = false) | |||||
{ | { | ||||
int oldState; | int oldState; | ||||
bool hasWriterLock; | bool hasWriterLock; | ||||
//If in either connecting or connected state, get a lock by being the first to switch to disconnecting | //If in either connecting or connected state, get a lock by being the first to switch to disconnecting | ||||
oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connecting); | oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connecting); | ||||
if (oldState == (int)DiscordClientState.Disconnected) return; //Already disconnected | |||||
if (oldState == (int)DiscordClientState.Disconnected) return TaskHelper.CompletedTask; //Already disconnected | |||||
hasWriterLock = oldState == (int)DiscordClientState.Connecting; //Caused state change | hasWriterLock = oldState == (int)DiscordClientState.Connecting; //Caused state change | ||||
if (!hasWriterLock) | if (!hasWriterLock) | ||||
{ | { | ||||
oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connected); | oldState = Interlocked.CompareExchange(ref _state, (int)DiscordClientState.Disconnecting, (int)DiscordClientState.Connected); | ||||
if (oldState == (int)DiscordClientState.Disconnected) return; //Already disconnected | |||||
if (oldState == (int)DiscordClientState.Disconnected) return TaskHelper.CompletedTask; //Already disconnected | |||||
hasWriterLock = oldState == (int)DiscordClientState.Connected; //Caused state change | hasWriterLock = oldState == (int)DiscordClientState.Connected; //Caused state change | ||||
} | } | ||||
@@ -644,18 +649,9 @@ namespace Discord | |||||
} | } | ||||
if (!skipAwait) | if (!skipAwait) | ||||
{ | |||||
Task task = _runTask; | |||||
if (task != null) | |||||
await task.ConfigureAwait(false); | |||||
} | |||||
if (hasWriterLock) | |||||
{ | |||||
_state = (int)DiscordClientState.Disconnected; | |||||
_disconnectedEvent.Set(); | |||||
_connectedEvent.Reset(); | |||||
} | |||||
return _runTask ?? TaskHelper.CompletedTask; | |||||
else | |||||
return TaskHelper.CompletedTask; | |||||
} | } | ||||
private async Task RunTasks() | private async Task RunTasks() | ||||
@@ -672,13 +668,14 @@ namespace Discord | |||||
} | } | ||||
catch (Exception ex) { await DisconnectInternal(ex, skipAwait: true).ConfigureAwait(false); } | catch (Exception ex) { await DisconnectInternal(ex, skipAwait: true).ConfigureAwait(false); } | ||||
await Cleanup().ConfigureAwait(false); | |||||
bool wasUnexpected = _wasDisconnectUnexpected; | |||||
_wasDisconnectUnexpected = false; | |||||
await Cleanup(wasUnexpected).ConfigureAwait(false); | |||||
_runTask = null; | _runTask = null; | ||||
} | } | ||||
private async Task Cleanup() | |||||
private async Task Cleanup(bool wasUnexpected) | |||||
{ | { | ||||
_disconnectedEvent.Set(); | |||||
await _dataSocket.Disconnect().ConfigureAwait(false); | await _dataSocket.Disconnect().ConfigureAwait(false); | ||||
if (_config.EnableVoice) | if (_config.EnableVoice) | ||||
await _voiceSocket.Disconnect().ConfigureAwait(false); | await _voiceSocket.Disconnect().ConfigureAwait(false); | ||||
@@ -695,6 +692,14 @@ namespace Discord | |||||
_currentUser = null; | _currentUser = null; | ||||
_currentUserId = null; | _currentUserId = null; | ||||
_token = null; | |||||
if (!wasUnexpected) | |||||
{ | |||||
_state = (int)DiscordClientState.Disconnected; | |||||
_disconnectedEvent.Set(); | |||||
} | |||||
_connectedEvent.Reset(); | |||||
} | } | ||||
//Helpers | //Helpers | ||||
@@ -16,7 +16,10 @@ namespace Discord.Net.WebSockets | |||||
{ | { | ||||
internal partial class VoiceWebSocket : WebSocket | internal partial class VoiceWebSocket : WebSocket | ||||
{ | { | ||||
private readonly int _targetAudioBufferLength; | |||||
private const string EncryptedMode = "xsalsa20_poly1305"; | |||||
private const string UnencryptedMode = "plain"; | |||||
private readonly int _targetAudioBufferLength; | |||||
private ManualResetEventSlim _connectWaitOnLogin; | private ManualResetEventSlim _connectWaitOnLogin; | ||||
private uint _ssrc; | private uint _ssrc; | ||||
private readonly Random _rand = new Random(); | private readonly Random _rand = new Random(); | ||||
@@ -26,11 +29,9 @@ namespace Discord.Net.WebSockets | |||||
private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait; | private ManualResetEventSlim _sendQueueWait, _sendQueueEmptyWait; | ||||
private UdpClient _udp; | private UdpClient _udp; | ||||
private IPEndPoint _endpoint; | private IPEndPoint _endpoint; | ||||
private bool _isReady, _isClearing; | |||||
private bool _isClearing, _isEncrypted; | |||||
private byte[] _secretKey; | private byte[] _secretKey; | ||||
private string _myIp; | |||||
private ushort _sequence; | private ushort _sequence; | ||||
private string _mode; | |||||
private byte[] _encodingBuffer; | private byte[] _encodingBuffer; | ||||
private string _serverId, _userId, _sessionId, _token; | private string _serverId, _userId, _sessionId, _token; | ||||
@@ -52,24 +53,32 @@ namespace Discord.Net.WebSockets | |||||
_targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | _targetAudioBufferLength = client.Config.VoiceBufferLength / 20; //20 ms frames | ||||
} | } | ||||
public Task Login(string host, string serverId, string userId, string sessionId, string token, CancellationToken cancelToken) | |||||
public async Task Login(string host, 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.")); | |||||
return; | |||||
} | |||||
_serverId = serverId; | _serverId = serverId; | ||||
_userId = userId; | _userId = userId; | ||||
_sessionId = sessionId; | _sessionId = sessionId; | ||||
_token = token; | _token = token; | ||||
return base.Connect(host, cancelToken); | |||||
await Connect(host, cancelToken); | |||||
} | } | ||||
protected override Task[] Run() | protected override Task[] Run() | ||||
{ | { | ||||
_isClearing = false; | |||||
_udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); | ||||
#if !DNX451 | #if !DNX451 | ||||
_udp.AllowNatTraversal(true); | _udp.AllowNatTraversal(true); | ||||
#endif | #endif | ||||
_isReady = false; | |||||
_isClearing = false; | |||||
VoiceCommands.Login msg = new VoiceCommands.Login(); | VoiceCommands.Login msg = new VoiceCommands.Login(); | ||||
msg.Payload.ServerId = _serverId; | msg.Payload.ServerId = _serverId; | ||||
@@ -93,19 +102,24 @@ namespace Discord.Net.WebSockets | |||||
#endif | #endif | ||||
}.Concat(base.Run()).ToArray(); | }.Concat(base.Run()).ToArray(); | ||||
} | } | ||||
protected override Task Cleanup() | |||||
protected override Task Cleanup(bool wasUnexpected) | |||||
{ | { | ||||
ClearPCMFrames(); | |||||
_udp = null; | |||||
_serverId = null; | |||||
_userId = null; | |||||
_sessionId = null; | |||||
_token = null; | |||||
#if USE_THREAD | #if USE_THREAD | ||||
_sendThread.Join(); | _sendThread.Join(); | ||||
_sendThread = null; | _sendThread = null; | ||||
#endif | #endif | ||||
return base.Cleanup(); | |||||
ClearPCMFrames(); | |||||
if (!wasUnexpected) | |||||
{ | |||||
_serverId = null; | |||||
_userId = null; | |||||
_sessionId = null; | |||||
_token = null; | |||||
} | |||||
_udp = null; | |||||
return base.Cleanup(wasUnexpected); | |||||
} | } | ||||
private async Task ReceiveVoiceAsync() | private async Task ReceiveVoiceAsync() | ||||
@@ -153,14 +167,16 @@ namespace Discord.Net.WebSockets | |||||
byte[] packet; | byte[] packet; | ||||
try | try | ||||
{ | { | ||||
while (!cancelToken.IsCancellationRequested && !_isReady) | |||||
while (!cancelToken.IsCancellationRequested && _state != (int)WebSocketState.Connected) | |||||
{ | |||||
#if USE_THREAD | #if USE_THREAD | ||||
Thread.Sleep(1); | Thread.Sleep(1); | ||||
#else | #else | ||||
await Task.Delay(1); | await Task.Delay(1); | ||||
#endif | #endif | ||||
} | |||||
if (cancelToken.IsCancellationRequested) | |||||
if (cancelToken.IsCancellationRequested) | |||||
return; | return; | ||||
uint timestamp = 0; | uint timestamp = 0; | ||||
@@ -251,15 +267,15 @@ namespace Discord.Net.WebSockets | |||||
{ | { | ||||
case 2: //READY | case 2: //READY | ||||
{ | { | ||||
if (!_isReady) | |||||
if (_state != (int)WebSocketState.Connected) | |||||
{ | { | ||||
var payload = (msg.Payload as JToken).ToObject<VoiceEvents.Ready>(); | var payload = (msg.Payload as JToken).ToObject<VoiceEvents.Ready>(); | ||||
_heartbeatInterval = payload.HeartbeatInterval; | _heartbeatInterval = payload.HeartbeatInterval; | ||||
_ssrc = payload.SSRC; | _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(); | //_mode = payload.Modes.LastOrDefault(); | ||||
_mode = "plain"; | |||||
_udp.Connect(_endpoint); | |||||
_isEncrypted = !payload.Modes.Contains("plain"); | |||||
_udp.Connect(_endpoint); | |||||
_sequence = (ushort)_rand.Next(0, ushort.MaxValue); | _sequence = (ushort)_rand.Next(0, ushort.MaxValue); | ||||
//No thread issue here because SendAsync doesn't start until _isReady is true | //No thread issue here because SendAsync doesn't start until _isReady is true | ||||
@@ -297,9 +313,8 @@ namespace Discord.Net.WebSockets | |||||
{ | { | ||||
byte[] buffer = msg.Buffer; | byte[] buffer = msg.Buffer; | ||||
int length = msg.Buffer.Length; | int length = msg.Buffer.Length; | ||||
if (!_isReady) | |||||
if (_state != (int)WebSocketState.Connected) | |||||
{ | { | ||||
_isReady = true; | |||||
if (length != 70) | if (length != 70) | ||||
{ | { | ||||
if (_logLevel >= LogMessageSeverity.Warning) | if (_logLevel >= LogMessageSeverity.Warning) | ||||
@@ -308,15 +323,15 @@ namespace Discord.Net.WebSockets | |||||
} | } | ||||
int port = buffer[68] | buffer[69] << 8; | int port = buffer[68] | buffer[69] << 8; | ||||
string ip = Encoding.ASCII.GetString(buffer, 4, 70 - 6).TrimEnd('\0'); | |||||
_myIp = Encoding.ASCII.GetString(buffer, 4, 70 - 6).TrimEnd('\0'); | |||||
CompleteConnect(); | |||||
_isReady = true; | |||||
var login2 = new VoiceCommands.Login2(); | var login2 = new VoiceCommands.Login2(); | ||||
login2.Payload.Protocol = "udp"; | login2.Payload.Protocol = "udp"; | ||||
login2.Payload.SocketData.Address = _myIp; | |||||
login2.Payload.SocketData.Mode = _mode; | |||||
login2.Payload.SocketData.Port = port; | |||||
login2.Payload.SocketData.Address = ip; | |||||
login2.Payload.SocketData.Mode = _isEncrypted ? EncryptedMode : UnencryptedMode; | |||||
login2.Payload.SocketData.Port = port; | |||||
QueueMessage(login2); | QueueMessage(login2); | ||||
} | } | ||||
else | else | ||||
@@ -377,8 +392,8 @@ namespace Discord.Net.WebSockets | |||||
buffer = newBuffer; | buffer = newBuffer; | ||||
}*/ | }*/ | ||||
if (_logLevel >= LogMessageSeverity.Verbose) | |||||
RaiseOnLog(LogMessageSeverity.Verbose, $"Received {buffer.Length - 12} bytes."); | |||||
if (_logLevel >= LogMessageSeverity.Debug) | |||||
RaiseOnLog(LogMessageSeverity.Debug, $"Received {buffer.Length - 12} bytes."); | |||||
//TODO: Use Voice Data | //TODO: Use Voice Data | ||||
} | } | ||||
} | } | ||||
@@ -386,12 +401,6 @@ namespace Discord.Net.WebSockets | |||||
public void SendPCMFrames(byte[] data, int bytes) | public void SendPCMFrames(byte[] data, int bytes) | ||||
{ | { | ||||
var cancelToken = _cancelToken; | |||||
if (!_isReady || cancelToken == null) | |||||
throw new InvalidOperationException("Not connected to a voice server."); | |||||
if (bytes == 0) | |||||
return; | |||||
int frameSize = _encoder.FrameSize; | int frameSize = _encoder.FrameSize; | ||||
int frames = bytes / frameSize; | int frames = bytes / frameSize; | ||||
int expectedBytes = frames * frameSize; | int expectedBytes = frames * frameSize; | ||||
@@ -431,7 +440,7 @@ namespace Discord.Net.WebSockets | |||||
int encodedLength = _encoder.EncodeFrame(data, pos, _encodingBuffer); | int encodedLength = _encoder.EncodeFrame(data, pos, _encodingBuffer); | ||||
//TODO: Handle Encryption | //TODO: Handle Encryption | ||||
if (_mode == "xsalsa20_poly1305") | |||||
if (_isEncrypted) | |||||
{ | { | ||||
} | } | ||||
@@ -448,16 +457,16 @@ namespace Discord.Net.WebSockets | |||||
} | } | ||||
} | } | ||||
if (_logLevel >= LogMessageSeverity.Verbose) | |||||
RaiseOnLog(LogMessageSeverity.Verbose, $"Queued {bytes} bytes for voice output."); | |||||
if (_logLevel >= LogMessageSeverity.Debug) | |||||
RaiseOnLog(LogMessageSeverity.Debug, $"Queued {bytes} bytes for voice output."); | |||||
} | } | ||||
public void ClearPCMFrames() | public void ClearPCMFrames() | ||||
{ | { | ||||
_isClearing = true; | _isClearing = true; | ||||
byte[] ignored; | byte[] ignored; | ||||
while (_sendQueue.TryDequeue(out ignored)) { } | while (_sendQueue.TryDequeue(out ignored)) { } | ||||
if (_logLevel >= LogMessageSeverity.Verbose) | |||||
RaiseOnLog(LogMessageSeverity.Verbose, "Cleared the voice buffer."); | |||||
if (_logLevel >= LogMessageSeverity.Debug) | |||||
RaiseOnLog(LogMessageSeverity.Debug, "Cleared the voice buffer."); | |||||
_isClearing = false; | _isClearing = false; | ||||
} | } | ||||
@@ -2,13 +2,6 @@ | |||||
namespace Discord.Net.WebSockets | namespace Discord.Net.WebSockets | ||||
{ | { | ||||
public class DisconnectedEventArgs : EventArgs | |||||
{ | |||||
public readonly bool WasUnexpected; | |||||
public readonly Exception Error; | |||||
internal DisconnectedEventArgs(bool wasUnexpected, Exception error) { WasUnexpected = wasUnexpected; Error = error; } | |||||
} | |||||
internal partial class WebSocket | internal partial class WebSocket | ||||
{ | { | ||||
public event EventHandler Connected; | public event EventHandler Connected; | ||||
@@ -38,12 +38,14 @@ namespace Discord.Net.WebSockets | |||||
protected readonly DiscordClient _client; | protected readonly DiscordClient _client; | ||||
protected readonly LogMessageSeverity _logLevel; | protected readonly LogMessageSeverity _logLevel; | ||||
protected int _state; | |||||
protected string _host; | protected string _host; | ||||
protected int _loginTimeout, _heartbeatInterval; | protected int _loginTimeout, _heartbeatInterval; | ||||
private DateTime _lastHeartbeat; | private DateTime _lastHeartbeat; | ||||
private Task _runTask; | private Task _runTask; | ||||
public WebSocketState State => (WebSocketState)_state; | |||||
protected int _state; | |||||
protected ExceptionDispatchInfo _disconnectReason; | protected ExceptionDispatchInfo _disconnectReason; | ||||
private bool _wasDisconnectUnexpected; | private bool _wasDisconnectUnexpected; | ||||
@@ -56,17 +58,45 @@ namespace Discord.Net.WebSockets | |||||
_client = client; | _client = client; | ||||
_logLevel = client.Config.LogLevel; | _logLevel = client.Config.LogLevel; | ||||
_loginTimeout = client.Config.ConnectionTimeout; | _loginTimeout = client.Config.ConnectionTimeout; | ||||
_cancelToken = new CancellationToken(true); | |||||
_engine = new BuiltInWebSocketEngine(client.Config.WebSocketInterval); | _engine = new BuiltInWebSocketEngine(client.Config.WebSocketInterval); | ||||
_engine.ProcessMessage += (s, e) => | |||||
_engine.ProcessMessage += async (s, e) => | |||||
{ | { | ||||
if (_logLevel >= LogMessageSeverity.Debug) | if (_logLevel >= LogMessageSeverity.Debug) | ||||
RaiseOnLog(LogMessageSeverity.Debug, $"In: " + e.Message); | RaiseOnLog(LogMessageSeverity.Debug, $"In: " + e.Message); | ||||
ProcessMessage(e.Message); | |||||
await ProcessMessage(e.Message); | |||||
}; | }; | ||||
} | } | ||||
public async Task Reconnect(CancellationToken cancelToken) | |||||
{ | |||||
try | |||||
{ | |||||
await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
while (!cancelToken.IsCancellationRequested) | |||||
{ | |||||
try | |||||
{ | |||||
await Connect(_host, cancelToken).ConfigureAwait(false); | |||||
break; | |||||
} | |||||
catch (OperationCanceledException) { throw; } | |||||
catch (Exception ex) | |||||
{ | |||||
RaiseOnLog(LogMessageSeverity.Error, $"DataSocket reconnect failed: {ex.GetBaseException().Message}"); | |||||
//Net is down? We can keep trying to reconnect until the user runs Disconnect() | |||||
await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); | |||||
} | |||||
} | |||||
} | |||||
catch (OperationCanceledException) { } | |||||
} | |||||
protected virtual async Task Connect(string host, CancellationToken cancelToken) | protected virtual async Task Connect(string host, CancellationToken cancelToken) | ||||
{ | { | ||||
if (_state != (int)WebSocketState.Disconnected) | |||||
throw new InvalidOperationException("Client is already connected or connecting to the server."); | |||||
try | try | ||||
{ | { | ||||
await Disconnect().ConfigureAwait(false); | await Disconnect().ConfigureAwait(false); | ||||
@@ -97,19 +127,19 @@ namespace Discord.Net.WebSockets | |||||
=> Connect(_host, _cancelToken);*/ | => Connect(_host, _cancelToken);*/ | ||||
public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | public Task Disconnect() => DisconnectInternal(new Exception("Disconnect was requested by user."), isUnexpected: false); | ||||
protected async Task DisconnectInternal(Exception ex, bool isUnexpected = true, bool skipAwait = false) | |||||
protected Task DisconnectInternal(Exception ex, bool isUnexpected = true, bool skipAwait = false) | |||||
{ | { | ||||
int oldState; | int oldState; | ||||
bool hasWriterLock; | bool hasWriterLock; | ||||
//If in either connecting or connected state, get a lock by being the first to switch to disconnecting | //If in either connecting or connected state, get a lock by being the first to switch to disconnecting | ||||
oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connecting); | oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connecting); | ||||
if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected | |||||
if (oldState == (int)WebSocketState.Disconnected) return TaskHelper.CompletedTask; //Already disconnected | |||||
hasWriterLock = oldState == (int)WebSocketState.Connecting; //Caused state change | hasWriterLock = oldState == (int)WebSocketState.Connecting; //Caused state change | ||||
if (!hasWriterLock) | if (!hasWriterLock) | ||||
{ | { | ||||
oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connected); | oldState = Interlocked.CompareExchange(ref _state, (int)WebSocketState.Disconnecting, (int)WebSocketState.Connected); | ||||
if (oldState == (int)WebSocketState.Disconnected) return; //Already disconnected | |||||
if (oldState == (int)WebSocketState.Disconnected) return TaskHelper.CompletedTask; //Already disconnected | |||||
hasWriterLock = oldState == (int)WebSocketState.Connected; //Caused state change | hasWriterLock = oldState == (int)WebSocketState.Connected; //Caused state change | ||||
} | } | ||||
@@ -121,17 +151,9 @@ namespace Discord.Net.WebSockets | |||||
} | } | ||||
if (!skipAwait) | if (!skipAwait) | ||||
{ | |||||
Task task = _runTask; | |||||
if (task != null) | |||||
await task.ConfigureAwait(false); | |||||
} | |||||
if (hasWriterLock) | |||||
{ | |||||
_state = (int)WebSocketState.Disconnected; | |||||
RaiseDisconnected(isUnexpected, ex); | |||||
} | |||||
return _runTask ?? TaskHelper.CompletedTask; | |||||
else | |||||
return TaskHelper.CompletedTask; | |||||
} | } | ||||
protected virtual async Task RunTasks() | protected virtual async Task RunTasks() | ||||
@@ -146,9 +168,8 @@ namespace Discord.Net.WebSockets | |||||
bool wasUnexpected = _wasDisconnectUnexpected; | bool wasUnexpected = _wasDisconnectUnexpected; | ||||
_wasDisconnectUnexpected = false; | _wasDisconnectUnexpected = false; | ||||
await _engine.Disconnect().ConfigureAwait(false); | |||||
await Cleanup().ConfigureAwait(false); | |||||
await Cleanup(wasUnexpected).ConfigureAwait(false); | |||||
_runTask = null; | _runTask = null; | ||||
} | } | ||||
protected virtual Task[] Run() | protected virtual Task[] Run() | ||||
@@ -158,10 +179,12 @@ namespace Discord.Net.WebSockets | |||||
.Concat(new Task[] { HeartbeatAsync(cancelToken) }) | .Concat(new Task[] { HeartbeatAsync(cancelToken) }) | ||||
.ToArray(); | .ToArray(); | ||||
} | } | ||||
protected virtual Task Cleanup() | |||||
protected virtual Task Cleanup(bool wasUnexpected) | |||||
{ | { | ||||
_cancelTokenSource = null; | _cancelTokenSource = null; | ||||
return TaskHelper.CompletedTask; | |||||
_state = (int)WebSocketState.Disconnected; | |||||
RaiseDisconnected(wasUnexpected, _disconnectReason?.SourceException); | |||||
return _engine.Disconnect(); | |||||
} | } | ||||
protected abstract Task ProcessMessage(string json); | protected abstract Task ProcessMessage(string json); | ||||