| @@ -38,7 +38,7 @@ | |||||
| <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||
| <LangVersion>6</LangVersion> | <LangVersion>6</LangVersion> | ||||
| </PropertyGroup> | </PropertyGroup> | ||||
| <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'FullDebug|AnyCPU'"> | |||||
| <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'TestResponses|AnyCPU' "> | |||||
| <DebugSymbols>true</DebugSymbols> | <DebugSymbols>true</DebugSymbols> | ||||
| <OutputPath>bin\FullDebug\</OutputPath> | <OutputPath>bin\FullDebug\</OutputPath> | ||||
| <DefineConstants>TRACE;DEBUG;NET45,TEST_RESPONSES</DefineConstants> | <DefineConstants>TRACE;DEBUG;NET45,TEST_RESPONSES</DefineConstants> | ||||
| @@ -515,6 +515,9 @@ | |||||
| <Compile Include="..\Discord.Net\Net\WebSocketException.cs"> | <Compile Include="..\Discord.Net\Net\WebSocketException.cs"> | ||||
| <Link>Net\WebSockets\WebSocketException.cs</Link> | <Link>Net\WebSockets\WebSocketException.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\Net\WebSockets\BuiltInEngine.cs"> | |||||
| <Link>Net\WebSockets\BuiltInEngine.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\Net\WebSockets\GatewaySocket.cs"> | <Compile Include="..\Discord.Net\Net\WebSockets\GatewaySocket.cs"> | ||||
| <Link>Net\WebSockets\GatewaySocket.cs</Link> | <Link>Net\WebSockets\GatewaySocket.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -0,0 +1,137 @@ | |||||
| #if DOTNET5_4 | |||||
| using Discord.Logging; | |||||
| using System; | |||||
| using System.IO; | |||||
| using System.Linq; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| using System.Net.Http; | |||||
| using System.Net; | |||||
| using System.Text; | |||||
| using System.Globalization; | |||||
| namespace Discord.Net.Rest | |||||
| { | |||||
| internal sealed class BuiltInEngine : IRestEngine | |||||
| { | |||||
| private readonly DiscordConfig _config; | |||||
| private readonly HttpClient _client; | |||||
| private readonly string _baseUrl; | |||||
| private readonly object _rateLimitLock; | |||||
| private DateTime _rateLimitTime; | |||||
| internal Logger Logger { get; } | |||||
| public BuiltInEngine(DiscordConfig config, string baseUrl, Logger logger) | |||||
| { | |||||
| _config = config; | |||||
| _baseUrl = baseUrl; | |||||
| _rateLimitLock = new object(); | |||||
| _client = new HttpClient(new HttpClientHandler | |||||
| { | |||||
| AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, | |||||
| UseCookies = false, | |||||
| UseProxy = false, | |||||
| PreAuthenticate = false //We do auth ourselves | |||||
| }); | |||||
| _client.DefaultRequestHeaders.Add("accept", "*/*"); | |||||
| _client.DefaultRequestHeaders.Add("accept-encoding", "gzip,deflate"); | |||||
| _client.DefaultRequestHeaders.Add("user-agent", config.UserAgent); | |||||
| } | |||||
| public void SetToken(string token) | |||||
| { | |||||
| _client.DefaultRequestHeaders.Remove("authorization"); | |||||
| if (token != null) | |||||
| _client.DefaultRequestHeaders.Add("authorization", token); | |||||
| } | |||||
| public async Task<string> Send(string method, string path, string json, CancellationToken cancelToken) | |||||
| { | |||||
| using (var request = new HttpRequestMessage(GetMethod(method), _baseUrl + path)) | |||||
| { | |||||
| if (json != null) | |||||
| request.Content = new StringContent(json, Encoding.UTF8, "application/json"); | |||||
| return await Send(request, cancelToken); | |||||
| } | |||||
| } | |||||
| public async Task<string> SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken) | |||||
| { | |||||
| using (var request = new HttpRequestMessage(GetMethod(method), _baseUrl + path)) | |||||
| { | |||||
| var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | |||||
| content.Add(new StreamContent(File.OpenRead(path)), "file", filename); | |||||
| request.Content = content; | |||||
| return await Send(request, cancelToken); | |||||
| } | |||||
| } | |||||
| private async Task<string> Send(HttpRequestMessage request, CancellationToken cancelToken) | |||||
| { | |||||
| int retryCount = 0; | |||||
| while (true) | |||||
| { | |||||
| HttpResponseMessage response; | |||||
| try | |||||
| { | |||||
| response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); | |||||
| } | |||||
| catch (WebException ex) | |||||
| { | |||||
| //The request was aborted: Could not create SSL/TLS secure channel. | |||||
| if (ex.HResult == -2146233079 && retryCount++ < 5) | |||||
| continue; //Retrying seems to fix this somehow? | |||||
| throw; | |||||
| } | |||||
| int statusCode = (int)response.StatusCode; | |||||
| if (statusCode == 429) //Rate limit | |||||
| { | |||||
| var retryAfter = response.Headers | |||||
| .Where(x => x.Key.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)) | |||||
| .Select(x => x.Value.FirstOrDefault()) | |||||
| .FirstOrDefault(); | |||||
| int milliseconds; | |||||
| if (retryAfter != null && int.TryParse(retryAfter, out milliseconds)) | |||||
| { | |||||
| var now = DateTime.UtcNow; | |||||
| if (now >= _rateLimitTime) | |||||
| { | |||||
| lock (_rateLimitLock) | |||||
| { | |||||
| if (now >= _rateLimitTime) | |||||
| { | |||||
| _rateLimitTime = now.AddMilliseconds(milliseconds); | |||||
| Logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); | |||||
| } | |||||
| } | |||||
| } | |||||
| await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false); | |||||
| continue; | |||||
| } | |||||
| throw new HttpException(response.StatusCode); | |||||
| } | |||||
| else if (statusCode < 200 || statusCode >= 300) //2xx = Success | |||||
| throw new HttpException(response.StatusCode); | |||||
| else | |||||
| return await response.Content.ReadAsStringAsync(); | |||||
| } | |||||
| } | |||||
| private static readonly HttpMethod _patch = new HttpMethod("PATCH"); | |||||
| private HttpMethod GetMethod(string method) | |||||
| { | |||||
| switch (method) | |||||
| { | |||||
| case "DELETE": return HttpMethod.Delete; | |||||
| case "GET": return HttpMethod.Get; | |||||
| case "PATCH": return _patch; | |||||
| case "POST": return HttpMethod.Post; | |||||
| case "PUT": return HttpMethod.Put; | |||||
| default: throw new InvalidOperationException($"Unknown HttpMethod: {method}"); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| #endif | |||||
| @@ -51,7 +51,7 @@ namespace Discord.Net.Rest | |||||
| #if !DOTNET5_4 | #if !DOTNET5_4 | ||||
| _engine = new RestSharpEngine(config, baseUrl, logger); | _engine = new RestSharpEngine(config, baseUrl, logger); | ||||
| #else | #else | ||||
| //_engine = new BuiltInRestEngine(config, baseUrl, logger); | |||||
| _engine = new BuiltInEngine(config, baseUrl, logger); | |||||
| #endif | #endif | ||||
| } | } | ||||
| @@ -51,7 +51,7 @@ namespace Discord.Net.Rest | |||||
| } | } | ||||
| public Task<string> SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken) | public Task<string> SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken) | ||||
| { | { | ||||
| var request = new RestRequest(path, Method.POST); | |||||
| var request = new RestRequest(path, GetMethod(method)); | |||||
| request.AddHeader("content-length", (stream.Length - stream.Position).ToString()); | request.AddHeader("content-length", (stream.Length - stream.Position).ToString()); | ||||
| byte[] bytes = new byte[stream.Length - stream.Position]; | byte[] bytes = new byte[stream.Length - stream.Position]; | ||||
| @@ -79,6 +79,7 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| var retryAfter = response.Headers | var retryAfter = response.Headers | ||||
| .FirstOrDefault(x => x.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)); | .FirstOrDefault(x => x.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)); | ||||
| int milliseconds; | int milliseconds; | ||||
| if (retryAfter != null && int.TryParse((string)retryAfter.Value, out milliseconds)) | if (retryAfter != null && int.TryParse((string)retryAfter.Value, out milliseconds)) | ||||
| { | { | ||||
| @@ -0,0 +1,163 @@ | |||||
| #if DOTNET5_4 | |||||
| using System; | |||||
| using System.Collections.Concurrent; | |||||
| using System.Collections.Generic; | |||||
| using System.ComponentModel; | |||||
| using System.IO; | |||||
| using System.Net.WebSockets; | |||||
| using System.Text; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| using WebSocketClient = System.Net.WebSockets.ClientWebSocket; | |||||
| namespace Discord.Net.WebSockets | |||||
| { | |||||
| internal class BuiltInEngine : IWebSocketEngine | |||||
| { | |||||
| private const int ReceiveChunkSize = 4096; | |||||
| private const int SendChunkSize = 4096; | |||||
| private const int HR_TIMEOUT = -2147012894; | |||||
| private readonly DiscordConfig _config; | |||||
| private readonly ConcurrentQueue<string> _sendQueue; | |||||
| private WebSocketClient _webSocket; | |||||
| public event EventHandler<WebSocketBinaryMessageEventArgs> BinaryMessage = delegate { }; | |||||
| public event EventHandler<WebSocketTextMessageEventArgs> TextMessage = delegate { }; | |||||
| private void OnBinaryMessage(byte[] data) | |||||
| => BinaryMessage(this, new WebSocketBinaryMessageEventArgs(data)); | |||||
| private void OnTextMessage(string msg) | |||||
| => TextMessage(this, new WebSocketTextMessageEventArgs(msg)); | |||||
| internal BuiltInEngine(DiscordConfig config) | |||||
| { | |||||
| _config = config; | |||||
| _sendQueue = new ConcurrentQueue<string>(); | |||||
| } | |||||
| public Task Connect(string host, CancellationToken cancelToken) | |||||
| { | |||||
| return Task.Run(async () => | |||||
| { | |||||
| _webSocket = new WebSocketClient(); | |||||
| _webSocket.Options.Proxy = null; | |||||
| _webSocket.Options.SetRequestHeader("User-Agent", _config.UserAgent); | |||||
| _webSocket.Options.KeepAliveInterval = TimeSpan.Zero; | |||||
| await _webSocket.ConnectAsync(new Uri(host), cancelToken)//.ConfigureAwait(false); | |||||
| .ContinueWith(t => ReceiveAsync(cancelToken)).ConfigureAwait(false); | |||||
| //TODO: ContinueWith is a temporary hack, may be a bug related to https://github.com/dotnet/corefx/issues/4429 | |||||
| }); | |||||
| } | |||||
| public Task Disconnect() | |||||
| { | |||||
| string ignored; | |||||
| while (_sendQueue.TryDequeue(out ignored)) { } | |||||
| var socket = _webSocket; | |||||
| _webSocket = null; | |||||
| return TaskHelper.CompletedTask; | |||||
| } | |||||
| public IEnumerable<Task> GetTasks(CancellationToken cancelToken) | |||||
| => new Task[] { /*ReceiveAsync(cancelToken),*/ SendAsync(cancelToken) }; | |||||
| private Task ReceiveAsync(CancellationToken cancelToken) | |||||
| { | |||||
| return Task.Run(async () => | |||||
| { | |||||
| var sendInterval = _config.WebSocketInterval; | |||||
| //var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]); | |||||
| var buffer = new byte[ReceiveChunkSize]; | |||||
| var stream = new MemoryStream(); | |||||
| try | |||||
| { | |||||
| while (!cancelToken.IsCancellationRequested) | |||||
| { | |||||
| WebSocketReceiveResult result = null; | |||||
| do | |||||
| { | |||||
| if (cancelToken.IsCancellationRequested) return; | |||||
| try | |||||
| { | |||||
| result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancelToken);//.ConfigureAwait(false); | |||||
| } | |||||
| catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) | |||||
| { | |||||
| throw new Exception($"Connection timed out."); | |||||
| } | |||||
| if (result.MessageType == WebSocketMessageType.Close) | |||||
| throw new WebSocketException((int)result.CloseStatus.Value, result.CloseStatusDescription); | |||||
| else | |||||
| stream.Write(buffer, 0, result.Count); | |||||
| } | |||||
| while (result == null || !result.EndOfMessage); | |||||
| var array = stream.ToArray(); | |||||
| if (result.MessageType == WebSocketMessageType.Binary) | |||||
| OnBinaryMessage(array); | |||||
| else if (result.MessageType == WebSocketMessageType.Text) | |||||
| OnTextMessage(Encoding.UTF8.GetString(array, 0, array.Length)); | |||||
| stream.Position = 0; | |||||
| stream.SetLength(0); | |||||
| } | |||||
| } | |||||
| catch (OperationCanceledException) { } | |||||
| }); | |||||
| } | |||||
| private Task SendAsync(CancellationToken cancelToken) | |||||
| { | |||||
| return Task.Run(async () => | |||||
| { | |||||
| byte[] bytes = new byte[SendChunkSize]; | |||||
| var sendInterval = _config.WebSocketInterval; | |||||
| try | |||||
| { | |||||
| while (!cancelToken.IsCancellationRequested) | |||||
| { | |||||
| string json; | |||||
| while (_sendQueue.TryDequeue(out json)) | |||||
| { | |||||
| int byteCount = Encoding.UTF8.GetBytes(json, 0, json.Length, bytes, 0); | |||||
| int frameCount = (int)Math.Ceiling((double)byteCount / SendChunkSize); | |||||
| int offset = 0; | |||||
| for (var i = 0; i < frameCount; i++, offset += SendChunkSize) | |||||
| { | |||||
| bool isLast = i == (frameCount - 1); | |||||
| int count; | |||||
| if (isLast) | |||||
| count = byteCount - (i * SendChunkSize); | |||||
| else | |||||
| count = SendChunkSize; | |||||
| try | |||||
| { | |||||
| await _webSocket.SendAsync(new ArraySegment<byte>(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false); | |||||
| } | |||||
| catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) | |||||
| { | |||||
| return; | |||||
| } | |||||
| } | |||||
| } | |||||
| await Task.Delay(sendInterval, cancelToken).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| catch (OperationCanceledException) { } | |||||
| }); | |||||
| } | |||||
| public void QueueMessage(string message) | |||||
| => _sendQueue.Enqueue(message); | |||||
| } | |||||
| } | |||||
| #endif | |||||
| @@ -6,7 +6,7 @@ using System.Collections.Generic; | |||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| using WebSocket4Net; | using WebSocket4Net; | ||||
| using WS4NetWebSocket = WebSocket4Net.WebSocket; | |||||
| using WebSocketClient = WebSocket4Net.WebSocket; | |||||
| namespace Discord.Net.WebSockets | namespace Discord.Net.WebSockets | ||||
| { | { | ||||
| @@ -15,7 +15,7 @@ namespace Discord.Net.WebSockets | |||||
| private readonly DiscordConfig _config; | private readonly DiscordConfig _config; | ||||
| private readonly ConcurrentQueue<string> _sendQueue; | private readonly ConcurrentQueue<string> _sendQueue; | ||||
| private readonly TaskManager _taskManager; | private readonly TaskManager _taskManager; | ||||
| private WS4NetWebSocket _webSocket; | |||||
| private WebSocketClient _webSocket; | |||||
| private ManualResetEventSlim _waitUntilConnect; | private ManualResetEventSlim _waitUntilConnect; | ||||
| public event EventHandler<WebSocketBinaryMessageEventArgs> BinaryMessage = delegate { }; | public event EventHandler<WebSocketBinaryMessageEventArgs> BinaryMessage = delegate { }; | ||||
| @@ -35,7 +35,7 @@ namespace Discord.Net.WebSockets | |||||
| public Task Connect(string host, CancellationToken cancelToken) | public Task Connect(string host, CancellationToken cancelToken) | ||||
| { | { | ||||
| _webSocket = new WS4NetWebSocket(host); | |||||
| _webSocket = new WebSocketClient(host); | |||||
| _webSocket.EnableAutoSendPing = false; | _webSocket.EnableAutoSendPing = false; | ||||
| _webSocket.NoDelay = true; | _webSocket.NoDelay = true; | ||||
| _webSocket.Proxy = null; | _webSocket.Proxy = null; | ||||
| @@ -96,7 +96,8 @@ namespace Discord.Net.WebSockets | |||||
| private void OnWebSocketBinary(object sender, DataReceivedEventArgs e) | private void OnWebSocketBinary(object sender, DataReceivedEventArgs e) | ||||
| => OnBinaryMessage(e.Data); | => OnBinaryMessage(e.Data); | ||||
| public IEnumerable<Task> GetTasks(CancellationToken cancelToken) => new Task[] { SendAsync(cancelToken) }; | |||||
| public IEnumerable<Task> GetTasks(CancellationToken cancelToken) | |||||
| => new Task[] { SendAsync(cancelToken) }; | |||||
| private Task SendAsync(CancellationToken cancelToken) | private Task SendAsync(CancellationToken cancelToken) | ||||
| { | { | ||||
| @@ -53,7 +53,7 @@ namespace Discord.Net.WebSockets | |||||
| #if !DOTNET5_4 | #if !DOTNET5_4 | ||||
| _engine = new WS4NetEngine(client.Config, _taskManager); | _engine = new WS4NetEngine(client.Config, _taskManager); | ||||
| #else | #else | ||||
| //_engine = new BuiltInWebSocketEngine(this, client.Config); | |||||
| _engine = new BuiltInEngine(client.Config); | |||||
| #endif | #endif | ||||
| _engine.BinaryMessage += (s, e) => | _engine.BinaryMessage += (s, e) => | ||||
| { | { | ||||
| @@ -179,7 +179,8 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| //Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached | //Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached | ||||
| cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token; | cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token; | ||||
| _connectedEvent.Wait(cancelToken); | |||||
| if (!_connectedEvent.Wait(_client.Config.ConnectionTimeout, cancelToken)) | |||||
| throw new TimeoutException(); | |||||
| } | } | ||||
| catch (OperationCanceledException) | catch (OperationCanceledException) | ||||
| { | { | ||||
| @@ -32,14 +32,8 @@ | |||||
| "Newtonsoft.Json": "7.0.1" | "Newtonsoft.Json": "7.0.1" | ||||
| }, | }, | ||||
| "frameworks": { | |||||
| "net45": { | |||||
| "dependencies": { | |||||
| "WebSocket4Net": "0.14.1", | |||||
| "RestSharp": "105.2.3" | |||||
| } | |||||
| }, | |||||
| "dotnet5.4": { | |||||
| "frameworks": { | |||||
| "dotnet5.4": { | |||||
| "dependencies": { | "dependencies": { | ||||
| "System.Collections": "4.0.11-beta-23516", | "System.Collections": "4.0.11-beta-23516", | ||||
| "System.Collections.Concurrent": "4.0.11-beta-23516", | "System.Collections.Concurrent": "4.0.11-beta-23516", | ||||
| @@ -47,6 +41,7 @@ | |||||
| "System.IO.FileSystem": "4.0.1-beta-23516", | "System.IO.FileSystem": "4.0.1-beta-23516", | ||||
| "System.IO.Compression": "4.1.0-beta-23516", | "System.IO.Compression": "4.1.0-beta-23516", | ||||
| "System.Linq": "4.0.1-beta-23516", | "System.Linq": "4.0.1-beta-23516", | ||||
| "System.Net.Http": "4.0.1-beta-23516", | |||||
| "System.Net.NameResolution": "4.0.0-beta-23516", | "System.Net.NameResolution": "4.0.0-beta-23516", | ||||
| "System.Net.Sockets": "4.1.0-beta-23409", | "System.Net.Sockets": "4.1.0-beta-23409", | ||||
| "System.Net.Requests": "4.0.11-beta-23516", | "System.Net.Requests": "4.0.11-beta-23516", | ||||
| @@ -57,6 +52,12 @@ | |||||
| "System.Threading": "4.0.11-beta-23516", | "System.Threading": "4.0.11-beta-23516", | ||||
| "System.Threading.Thread": "4.0.0-beta-23516" | "System.Threading.Thread": "4.0.0-beta-23516" | ||||
| } | } | ||||
| } | |||||
| } | |||||
| }, | |||||
| "net45": { | |||||
| "dependencies": { | |||||
| "WebSocket4Net": "0.14.1", | |||||
| "RestSharp": "105.2.3" | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||