* Initial set of dispose implementations Not handled yet: - Discord.Net.Websocket/Entities/SocketGuild - Discord.Net.Tests * Refactor DiscordSocketClient init into ctor This way we remove an IDisposableAnalyzer warning for not disposing the client when we set the client variable. * Dispose of clients when disposing sharded client * Finish implementing IDisposable where appropriate I opted to use NoWarn in the Tests project as it wasn't really necessary considering that our tests only run once * Tweak samples after feedbacktags/2.0
| @@ -16,21 +16,28 @@ namespace _01_basic_ping_bot | |||
| // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library | |||
| class Program | |||
| { | |||
| private DiscordSocketClient _client; | |||
| private readonly DiscordSocketClient _client; | |||
| // Discord.Net heavily utilizes TAP for async, so we create | |||
| // an asynchronous context from the beginning. | |||
| static void Main(string[] args) | |||
| => new Program().MainAsync().GetAwaiter().GetResult(); | |||
| { | |||
| new Program().MainAsync().GetAwaiter().GetResult(); | |||
| } | |||
| public async Task MainAsync() | |||
| public Program() | |||
| { | |||
| // It is recommended to Dispose of a client when you are finished | |||
| // using it, at the end of your app's lifetime. | |||
| _client = new DiscordSocketClient(); | |||
| _client.Log += LogAsync; | |||
| _client.Ready += ReadyAsync; | |||
| _client.MessageReceived += MessageReceivedAsync; | |||
| } | |||
| public async Task MainAsync() | |||
| { | |||
| // Tokens should be considered secret data, and never hard-coded. | |||
| await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); | |||
| await _client.StartAsync(); | |||
| @@ -19,24 +19,32 @@ namespace _02_commands_framework | |||
| // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library | |||
| class Program | |||
| { | |||
| // There is no need to implement IDisposable like before as we are | |||
| // using dependency injection, which handles calling Dispose for us. | |||
| static void Main(string[] args) | |||
| => new Program().MainAsync().GetAwaiter().GetResult(); | |||
| public async Task MainAsync() | |||
| { | |||
| var services = ConfigureServices(); | |||
| // You should dispose a service provider created using ASP.NET | |||
| // when you are finished using it, at the end of your app's lifetime. | |||
| // If you use another dependency injection framework, you should inspect | |||
| // its documentation for the best way to do this. | |||
| using (var services = ConfigureServices()) | |||
| { | |||
| var client = services.GetRequiredService<DiscordSocketClient>(); | |||
| var client = services.GetRequiredService<DiscordSocketClient>(); | |||
| client.Log += LogAsync; | |||
| services.GetRequiredService<CommandService>().Log += LogAsync; | |||
| client.Log += LogAsync; | |||
| services.GetRequiredService<CommandService>().Log += LogAsync; | |||
| // Tokens should be considered secret data, and never hard-coded. | |||
| await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); | |||
| await client.StartAsync(); | |||
| await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); | |||
| await client.StartAsync(); | |||
| await services.GetRequiredService<CommandHandlingService>().InitializeAsync(); | |||
| await services.GetRequiredService<CommandHandlingService>().InitializeAsync(); | |||
| await Task.Delay(-1); | |||
| await Task.Delay(-1); | |||
| } | |||
| } | |||
| private Task LogAsync(LogMessage log) | |||
| @@ -46,7 +54,7 @@ namespace _02_commands_framework | |||
| return Task.CompletedTask; | |||
| } | |||
| private IServiceProvider ConfigureServices() | |||
| private ServiceProvider ConfigureServices() | |||
| { | |||
| return new ServiceCollection() | |||
| .AddSingleton<DiscordSocketClient>() | |||
| @@ -13,41 +13,46 @@ namespace _03_sharded_client | |||
| // DiscordSocketClient instances (or shards) to serve a large number of guilds. | |||
| class Program | |||
| { | |||
| private DiscordShardedClient _client; | |||
| static void Main(string[] args) | |||
| => new Program().MainAsync().GetAwaiter().GetResult(); | |||
| public async Task MainAsync() | |||
| { | |||
| // You specify the amount of shards you'd like to have with the | |||
| // DiscordSocketConfig. Generally, it's recommended to | |||
| // DiscordSocketConfig. Generally, it's recommended to | |||
| // have 1 shard per 1500-2000 guilds your bot is in. | |||
| var config = new DiscordSocketConfig | |||
| { | |||
| TotalShards = 2 | |||
| }; | |||
| _client = new DiscordShardedClient(config); | |||
| var services = ConfigureServices(); | |||
| // You should dispose a service provider created using ASP.NET | |||
| // when you are finished using it, at the end of your app's lifetime. | |||
| // If you use another dependency injection framework, you should inspect | |||
| // its documentation for the best way to do this. | |||
| using (var services = ConfigureServices(config)) | |||
| { | |||
| var client = services.GetRequiredService<DiscordShardedClient>(); | |||
| // The Sharded Client does not have a Ready event. | |||
| // The ShardReady event is used instead, allowing for individual | |||
| // control per shard. | |||
| _client.ShardReady += ReadyAsync; | |||
| _client.Log += LogAsync; | |||
| // The Sharded Client does not have a Ready event. | |||
| // The ShardReady event is used instead, allowing for individual | |||
| // control per shard. | |||
| client.ShardReady += ReadyAsync; | |||
| client.Log += LogAsync; | |||
| await services.GetRequiredService<CommandHandlingService>().InitializeAsync(); | |||
| await services.GetRequiredService<CommandHandlingService>().InitializeAsync(); | |||
| await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); | |||
| await _client.StartAsync(); | |||
| // Tokens should be considered secret data, and never hard-coded. | |||
| await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); | |||
| await client.StartAsync(); | |||
| await Task.Delay(-1); | |||
| await Task.Delay(-1); | |||
| } | |||
| } | |||
| private IServiceProvider ConfigureServices() | |||
| private ServiceProvider ConfigureServices(DiscordSocketConfig config) | |||
| { | |||
| return new ServiceCollection() | |||
| .AddSingleton(_client) | |||
| .AddSingleton(new DiscordShardedClient(config)) | |||
| .AddSingleton<CommandService>() | |||
| .AddSingleton<CommandHandlingService>() | |||
| .BuildServiceProvider(); | |||
| @@ -27,7 +27,7 @@ namespace Discord.Commands | |||
| /// been successfully executed. | |||
| /// </para> | |||
| /// </remarks> | |||
| public class CommandService | |||
| public class CommandService : IDisposable | |||
| { | |||
| /// <summary> | |||
| /// Occurs when a command-related information is received. | |||
| @@ -67,6 +67,8 @@ namespace Discord.Commands | |||
| internal readonly LogManager _logManager; | |||
| internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap; | |||
| internal bool _isDisposed; | |||
| /// <summary> | |||
| /// Represents all modules loaded within <see cref="CommandService"/>. | |||
| /// </summary> | |||
| @@ -330,9 +332,9 @@ namespace Discord.Commands | |||
| //Type Readers | |||
| /// <summary> | |||
| /// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object | |||
| /// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object | |||
| /// type. | |||
| /// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will | |||
| /// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will | |||
| /// also be added. | |||
| /// If a default <see cref="TypeReader" /> exists for <typeparamref name="T" />, a warning will be logged | |||
| /// and the default <see cref="TypeReader" /> will be replaced. | |||
| @@ -607,5 +609,23 @@ namespace Discord.Commands | |||
| await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); | |||
| return result; | |||
| } | |||
| protected virtual void Dispose(bool disposing) | |||
| { | |||
| if (!_isDisposed) | |||
| { | |||
| if (disposing) | |||
| { | |||
| _moduleLock?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| void IDisposable.Dispose() | |||
| { | |||
| Dispose(true); | |||
| } | |||
| } | |||
| } | |||
| @@ -16,4 +16,4 @@ | |||
| <ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' "> | |||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| </Project> | |||
| @@ -12,4 +12,7 @@ | |||
| <PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> | |||
| <PackageReference Include="System.Interactive.Async" Version="3.1.1" /> | |||
| </ItemGroup> | |||
| <ItemGroup Condition=" '$(Configuration)' != 'Release' "> | |||
| <PackageReference Include="IDisposableAnalyzers" Version="2.0.3.3" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -1,15 +1,21 @@ | |||
| using System; | |||
| using System.IO; | |||
| namespace Discord | |||
| { | |||
| /// <summary> | |||
| /// An image that will be uploaded to Discord. | |||
| /// </summary> | |||
| public struct Image | |||
| public struct Image : IDisposable | |||
| { | |||
| private bool _isDisposed; | |||
| /// <summary> | |||
| /// Gets the stream to be uploaded to Discord. | |||
| /// </summary> | |||
| #pragma warning disable IDISP008 | |||
| public Stream Stream { get; } | |||
| #pragma warning restore IDISP008 | |||
| /// <summary> | |||
| /// Create the image with a <see cref="System.IO.Stream"/>. | |||
| /// </summary> | |||
| @@ -19,6 +25,7 @@ namespace Discord | |||
| /// </param> | |||
| public Image(Stream stream) | |||
| { | |||
| _isDisposed = false; | |||
| Stream = stream; | |||
| } | |||
| @@ -45,15 +52,28 @@ namespace Discord | |||
| /// The specified <paramref name="path"/> is invalid, (for example, it is on an unmapped drive). | |||
| /// </exception> | |||
| /// <exception cref="System.UnauthorizedAccessException"> | |||
| /// <paramref name="path" /> specified a directory.-or- The caller does not have the required permission. | |||
| /// <paramref name="path" /> specified a directory.-or- The caller does not have the required permission. | |||
| /// </exception> | |||
| /// <exception cref="FileNotFoundException">The file specified in <paramref name="path" /> was not found. | |||
| /// <exception cref="FileNotFoundException">The file specified in <paramref name="path" /> was not found. | |||
| /// </exception> | |||
| /// <exception cref="IOException">An I/O error occurred while opening the file. </exception> | |||
| public Image(string path) | |||
| { | |||
| _isDisposed = false; | |||
| Stream = File.OpenRead(path); | |||
| } | |||
| /// <inheritdoc/> | |||
| public void Dispose() | |||
| { | |||
| if (!_isDisposed) | |||
| { | |||
| #pragma warning disable IDISP007 | |||
| Stream?.Dispose(); | |||
| #pragma warning restore IDISP007 | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| @@ -7,7 +8,7 @@ namespace Discord.Net.Rest | |||
| /// <summary> | |||
| /// Represents a generic REST-based client. | |||
| /// </summary> | |||
| public interface IRestClient | |||
| public interface IRestClient : IDisposable | |||
| { | |||
| /// <summary> | |||
| /// Sets the HTTP header of this client for all requests. | |||
| @@ -4,7 +4,7 @@ using System.Threading.Tasks; | |||
| namespace Discord.Net.Udp | |||
| { | |||
| public interface IUdpSocket | |||
| public interface IUdpSocket : IDisposable | |||
| { | |||
| event Func<byte[], int, int, Task> ReceivedDatagram; | |||
| @@ -4,7 +4,7 @@ using System.Threading.Tasks; | |||
| namespace Discord.Net.WebSockets | |||
| { | |||
| public interface IWebSocketClient | |||
| public interface IWebSocketClient : IDisposable | |||
| { | |||
| event Func<byte[], int, int, Task> BinaryMessage; | |||
| event Func<string, Task> TextMessage; | |||
| @@ -19,6 +19,7 @@ namespace Discord.Net.Providers.WS4Net | |||
| private readonly SemaphoreSlim _lock; | |||
| private readonly Dictionary<string, string> _headers; | |||
| private WS4NetSocket _client; | |||
| private CancellationTokenSource _disconnectCancelTokenSource; | |||
| private CancellationTokenSource _cancelTokenSource; | |||
| private CancellationToken _cancelToken, _parentToken; | |||
| private ManualResetEventSlim _waitUntilConnect; | |||
| @@ -28,7 +29,7 @@ namespace Discord.Net.Providers.WS4Net | |||
| { | |||
| _headers = new Dictionary<string, string>(); | |||
| _lock = new SemaphoreSlim(1, 1); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _disconnectCancelTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = CancellationToken.None; | |||
| _parentToken = CancellationToken.None; | |||
| _waitUntilConnect = new ManualResetEventSlim(); | |||
| @@ -38,7 +39,11 @@ namespace Discord.Net.Providers.WS4Net | |||
| if (!_isDisposed) | |||
| { | |||
| if (disposing) | |||
| { | |||
| DisconnectInternalAsync(true).GetAwaiter().GetResult(); | |||
| _lock?.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| @@ -63,8 +68,13 @@ namespace Discord.Net.Providers.WS4Net | |||
| { | |||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | |||
| _disconnectCancelTokenSource?.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| _client?.Dispose(); | |||
| _disconnectCancelTokenSource = new CancellationTokenSource(); | |||
| _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectCancelTokenSource.Token); | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| _client = new WS4NetSocket(host, "", customHeaderItems: _headers.ToList()) | |||
| { | |||
| @@ -96,7 +106,7 @@ namespace Discord.Net.Providers.WS4Net | |||
| } | |||
| private Task DisconnectInternalAsync(bool isDisposing = false) | |||
| { | |||
| _cancelTokenSource.Cancel(); | |||
| _disconnectCancelTokenSource.Cancel(); | |||
| if (_client == null) | |||
| return Task.Delay(0); | |||
| @@ -125,8 +135,10 @@ namespace Discord.Net.Providers.WS4Net | |||
| } | |||
| public void SetCancelToken(CancellationToken cancelToken) | |||
| { | |||
| _cancelTokenSource?.Dispose(); | |||
| _parentToken = cancelToken; | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | |||
| _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectCancelTokenSource.Token); | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| } | |||
| public async Task SendAsync(byte[] data, int index, int count, bool isText) | |||
| @@ -34,7 +34,7 @@ namespace Discord.Rest | |||
| public ISelfUser CurrentUser { get; protected set; } | |||
| /// <inheritdoc /> | |||
| public TokenType TokenType => ApiClient.AuthTokenType; | |||
| /// <summary> Creates a new REST-only Discord client. </summary> | |||
| internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) | |||
| { | |||
| @@ -106,9 +106,9 @@ namespace Discord.Rest | |||
| await _loggedInEvent.InvokeAsync().ConfigureAwait(false); | |||
| } | |||
| internal virtual Task OnLoginAsync(TokenType tokenType, string token) | |||
| internal virtual Task OnLoginAsync(TokenType tokenType, string token) | |||
| => Task.Delay(0); | |||
| public async Task LogoutAsync() | |||
| { | |||
| await _stateLock.WaitAsync().ConfigureAwait(false); | |||
| @@ -131,14 +131,17 @@ namespace Discord.Rest | |||
| await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); | |||
| } | |||
| internal virtual Task OnLogoutAsync() | |||
| internal virtual Task OnLogoutAsync() | |||
| => Task.Delay(0); | |||
| internal virtual void Dispose(bool disposing) | |||
| { | |||
| if (!_isDisposed) | |||
| { | |||
| #pragma warning disable IDISP007 | |||
| ApiClient.Dispose(); | |||
| #pragma warning restore IDISP007 | |||
| _stateLock?.Dispose(); | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| @@ -156,7 +159,7 @@ namespace Discord.Rest | |||
| ISelfUser IDiscordClient.CurrentUser => CurrentUser; | |||
| /// <inheritdoc /> | |||
| Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options) | |||
| Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options) | |||
| => throw new NotSupportedException(); | |||
| /// <inheritdoc /> | |||
| @@ -66,6 +66,7 @@ namespace Discord.API | |||
| /// <exception cref="ArgumentException">Unknown OAuth token type.</exception> | |||
| internal void SetBaseUrl(string baseUrl) | |||
| { | |||
| RestClient?.Dispose(); | |||
| RestClient = _restClientProvider(baseUrl); | |||
| RestClient.SetHeader("accept", "*/*"); | |||
| RestClient.SetHeader("user-agent", UserAgent); | |||
| @@ -93,7 +94,9 @@ namespace Discord.API | |||
| if (disposing) | |||
| { | |||
| _loginCancelToken?.Dispose(); | |||
| (RestClient as IDisposable)?.Dispose(); | |||
| RestClient?.Dispose(); | |||
| RequestQueue?.Dispose(); | |||
| _stateLock?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| @@ -117,6 +120,7 @@ namespace Discord.API | |||
| try | |||
| { | |||
| _loginCancelToken?.Dispose(); | |||
| _loginCancelToken = new CancellationTokenSource(); | |||
| AuthToken = null; | |||
| @@ -242,7 +246,7 @@ namespace Discord.API | |||
| internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids, | |||
| ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) | |||
| => SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); | |||
| public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, | |||
| public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, | |||
| string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) | |||
| { | |||
| options = options ?? new RequestOptions(); | |||
| @@ -31,6 +31,8 @@ namespace Discord.Rest | |||
| { | |||
| if (disposing) | |||
| ApiClient.Dispose(); | |||
| base.Dispose(disposing); | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -46,12 +48,12 @@ namespace Discord.Rest | |||
| _applicationInfo = null; | |||
| return Task.Delay(0); | |||
| } | |||
| public async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null) | |||
| { | |||
| return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); | |||
| } | |||
| public Task<RestChannel> GetChannelAsync(ulong id, RequestOptions options = null) | |||
| => ClientHelper.GetChannelAsync(this, id, options); | |||
| public Task<IReadOnlyCollection<IRestPrivateChannel>> GetPrivateChannelsAsync(RequestOptions options = null) | |||
| @@ -60,7 +62,7 @@ namespace Discord.Rest | |||
| => ClientHelper.GetDMChannelsAsync(this, options); | |||
| public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null) | |||
| => ClientHelper.GetGroupChannelsAsync(this, options); | |||
| public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null) | |||
| => ClientHelper.GetConnectionsAsync(this, options); | |||
| @@ -79,12 +81,12 @@ namespace Discord.Rest | |||
| => ClientHelper.GetGuildsAsync(this, options); | |||
| public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) | |||
| => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); | |||
| public Task<RestUser> GetUserAsync(ulong id, RequestOptions options = null) | |||
| => ClientHelper.GetUserAsync(this, id, options); | |||
| public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) | |||
| => ClientHelper.GetGuildUserAsync(this, guildId, id, options); | |||
| public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null) | |||
| => ClientHelper.GetVoiceRegionsAsync(this, options); | |||
| public Task<RestVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null) | |||
| @@ -34,12 +34,14 @@ namespace Discord.Net.Converters | |||
| } | |||
| else | |||
| { | |||
| var cloneStream = new MemoryStream(); | |||
| image.Stream.CopyTo(cloneStream); | |||
| bytes = new byte[cloneStream.Length]; | |||
| cloneStream.Position = 0; | |||
| cloneStream.Read(bytes, 0, bytes.Length); | |||
| length = (int)cloneStream.Length; | |||
| using (var cloneStream = new MemoryStream()) | |||
| { | |||
| image.Stream.CopyTo(cloneStream); | |||
| bytes = new byte[cloneStream.Length]; | |||
| cloneStream.Position = 0; | |||
| cloneStream.Read(bytes, 0, bytes.Length); | |||
| length = (int)cloneStream.Length; | |||
| } | |||
| } | |||
| string base64 = Convert.ToBase64String(bytes, 0, length); | |||
| @@ -27,12 +27,14 @@ namespace Discord.Net.Rest | |||
| { | |||
| _baseUrl = baseUrl; | |||
| #pragma warning disable IDISP014 | |||
| _client = new HttpClient(new HttpClientHandler | |||
| { | |||
| AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, | |||
| UseCookies = false, | |||
| UseProxy = useProxy, | |||
| }); | |||
| #pragma warning restore IDISP014 | |||
| SetHeader("accept-encoding", "gzip, deflate"); | |||
| _cancelToken = CancellationToken.None; | |||
| @@ -91,12 +93,14 @@ namespace Discord.Net.Rest | |||
| { | |||
| if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | |||
| var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | |||
| MemoryStream memoryStream = null; | |||
| if (multipartParams != null) | |||
| { | |||
| foreach (var p in multipartParams) | |||
| { | |||
| switch (p.Value) | |||
| { | |||
| #pragma warning disable IDISP004 | |||
| case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } | |||
| case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } | |||
| case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } | |||
| @@ -105,12 +109,15 @@ namespace Discord.Net.Rest | |||
| var stream = fileValue.Stream; | |||
| if (!stream.CanSeek) | |||
| { | |||
| var memoryStream = new MemoryStream(); | |||
| memoryStream = new MemoryStream(); | |||
| await stream.CopyToAsync(memoryStream).ConfigureAwait(false); | |||
| memoryStream.Position = 0; | |||
| #pragma warning disable IDISP001 | |||
| stream = memoryStream; | |||
| #pragma warning restore IDISP001 | |||
| } | |||
| content.Add(new StreamContent(stream), p.Key, fileValue.Filename); | |||
| #pragma warning restore IDISP004 | |||
| continue; | |||
| } | |||
| default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); | |||
| @@ -118,19 +125,24 @@ namespace Discord.Net.Rest | |||
| } | |||
| } | |||
| restRequest.Content = content; | |||
| return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | |||
| var result = await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); | |||
| memoryStream?.Dispose(); | |||
| return result; | |||
| } | |||
| } | |||
| private async Task<RestResponse> SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) | |||
| { | |||
| cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; | |||
| HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); | |||
| var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); | |||
| var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; | |||
| using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken)) | |||
| { | |||
| cancelToken = cancelTokenSource.Token; | |||
| HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); | |||
| return new RestResponse(response.StatusCode, headers, stream); | |||
| var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); | |||
| var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; | |||
| return new RestResponse(response.StatusCode, headers, stream); | |||
| } | |||
| } | |||
| private static readonly HttpMethod Patch = new HttpMethod("PATCH"); | |||
| @@ -16,23 +16,24 @@ namespace Discord.Net.Queue | |||
| private readonly ConcurrentDictionary<string, RequestBucket> _buckets; | |||
| private readonly SemaphoreSlim _tokenLock; | |||
| private readonly CancellationTokenSource _cancelToken; //Dispose token | |||
| private readonly CancellationTokenSource _cancelTokenSource; //Dispose token | |||
| private CancellationTokenSource _clearToken; | |||
| private CancellationToken _parentToken; | |||
| private CancellationTokenSource _requestCancelTokenSource; | |||
| private CancellationToken _requestCancelToken; //Parent token + Clear token | |||
| private DateTimeOffset _waitUntil; | |||
| private Task _cleanupTask; | |||
| public RequestQueue() | |||
| { | |||
| _tokenLock = new SemaphoreSlim(1, 1); | |||
| _clearToken = new CancellationTokenSource(); | |||
| _cancelToken = new CancellationTokenSource(); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _requestCancelToken = CancellationToken.None; | |||
| _parentToken = CancellationToken.None; | |||
| _buckets = new ConcurrentDictionary<string, RequestBucket>(); | |||
| _cleanupTask = RunCleanup(); | |||
| @@ -44,7 +45,9 @@ namespace Discord.Net.Queue | |||
| try | |||
| { | |||
| _parentToken = cancelToken; | |||
| _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token; | |||
| _requestCancelTokenSource?.Dispose(); | |||
| _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token); | |||
| _requestCancelToken = _requestCancelTokenSource.Token; | |||
| } | |||
| finally { _tokenLock.Release(); } | |||
| } | |||
| @@ -54,9 +57,14 @@ namespace Discord.Net.Queue | |||
| try | |||
| { | |||
| _clearToken?.Cancel(); | |||
| _clearToken?.Dispose(); | |||
| _clearToken = new CancellationTokenSource(); | |||
| if (_parentToken != null) | |||
| _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token; | |||
| { | |||
| _requestCancelTokenSource?.Dispose(); | |||
| _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); | |||
| _requestCancelToken = _requestCancelTokenSource.Token; | |||
| } | |||
| else | |||
| _requestCancelToken = _clearToken.Token; | |||
| } | |||
| @@ -65,13 +73,19 @@ namespace Discord.Net.Queue | |||
| public async Task<Stream> SendAsync(RestRequest request) | |||
| { | |||
| CancellationTokenSource createdTokenSource = null; | |||
| if (request.Options.CancelToken.CanBeCanceled) | |||
| request.Options.CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken).Token; | |||
| { | |||
| createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken); | |||
| request.Options.CancelToken = createdTokenSource.Token; | |||
| } | |||
| else | |||
| request.Options.CancelToken = _requestCancelToken; | |||
| var bucket = GetOrCreateBucket(request.Options.BucketId, request); | |||
| return await bucket.SendAsync(request).ConfigureAwait(false); | |||
| var result = await bucket.SendAsync(request).ConfigureAwait(false); | |||
| createdTokenSource?.Dispose(); | |||
| return result; | |||
| } | |||
| public async Task SendAsync(WebSocketRequest request) | |||
| { | |||
| @@ -109,7 +123,7 @@ namespace Discord.Net.Queue | |||
| { | |||
| try | |||
| { | |||
| while (!_cancelToken.IsCancellationRequested) | |||
| while (!_cancelTokenSource.IsCancellationRequested) | |||
| { | |||
| var now = DateTimeOffset.UtcNow; | |||
| foreach (var bucket in _buckets.Select(x => x.Value)) | |||
| @@ -117,7 +131,7 @@ namespace Discord.Net.Queue | |||
| if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) | |||
| _buckets.TryRemove(bucket.Id, out _); | |||
| } | |||
| await Task.Delay(60000, _cancelToken.Token).ConfigureAwait(false); //Runs each minute | |||
| await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute | |||
| } | |||
| } | |||
| catch (OperationCanceledException) { } | |||
| @@ -126,7 +140,10 @@ namespace Discord.Net.Queue | |||
| public void Dispose() | |||
| { | |||
| _cancelToken.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| _tokenLock?.Dispose(); | |||
| _clearToken?.Dispose(); | |||
| _requestCancelTokenSource?.Dispose(); | |||
| } | |||
| } | |||
| } | |||
| @@ -71,7 +71,7 @@ namespace Discord.Audio | |||
| ApiClient.ReceivedPacket += ProcessPacketAsync; | |||
| _stateLock = new SemaphoreSlim(1, 1); | |||
| _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, | |||
| _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, | |||
| OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); | |||
| _connection.Connected += () => _connectedEvent.InvokeAsync(); | |||
| _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); | |||
| @@ -79,7 +79,7 @@ namespace Discord.Audio | |||
| _keepaliveTimes = new ConcurrentQueue<KeyValuePair<ulong, int>>(); | |||
| _ssrcMap = new ConcurrentDictionary<uint, ulong>(); | |||
| _streams = new ConcurrentDictionary<ulong, StreamPair>(); | |||
| _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
| _serializer.Error += (s, e) => | |||
| { | |||
| @@ -91,7 +91,7 @@ namespace Discord.Audio | |||
| UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); | |||
| } | |||
| internal async Task StartAsync(string url, ulong userId, string sessionId, string token) | |||
| internal async Task StartAsync(string url, ulong userId, string sessionId, string token) | |||
| { | |||
| _url = url; | |||
| _userId = userId; | |||
| @@ -100,7 +100,7 @@ namespace Discord.Audio | |||
| await _connection.StartAsync().ConfigureAwait(false); | |||
| } | |||
| public async Task StopAsync() | |||
| { | |||
| { | |||
| await _connection.StopAsync().ConfigureAwait(false); | |||
| } | |||
| @@ -225,11 +225,11 @@ namespace Discord.Audio | |||
| if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) | |||
| throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); | |||
| ApiClient.SetUdpEndpoint(data.Ip, data.Port); | |||
| await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); | |||
| _heartbeatTask = RunHeartbeatAsync(41250, _connection.CancelToken); | |||
| } | |||
| break; | |||
| @@ -305,9 +305,9 @@ namespace Discord.Audio | |||
| catch (Exception ex) | |||
| { | |||
| await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false); | |||
| return; | |||
| return; | |||
| } | |||
| await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); | |||
| await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); | |||
| } | |||
| @@ -317,7 +317,7 @@ namespace Discord.Audio | |||
| { | |||
| await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); | |||
| ulong value = | |||
| ulong value = | |||
| ((ulong)packet[0] >> 0) | | |||
| ((ulong)packet[1] >> 8) | | |||
| ((ulong)packet[2] >> 16) | | |||
| @@ -341,7 +341,7 @@ namespace Discord.Audio | |||
| } | |||
| } | |||
| else | |||
| { | |||
| { | |||
| if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) | |||
| { | |||
| await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false); | |||
| @@ -388,7 +388,7 @@ namespace Discord.Audio | |||
| var now = Environment.TickCount; | |||
| //Did server respond to our last heartbeat? | |||
| if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && | |||
| if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && | |||
| ConnectionState == ConnectionState.Connected) | |||
| { | |||
| _connection.Error(new Exception("Server missed last heartbeat")); | |||
| @@ -437,7 +437,7 @@ namespace Discord.Audio | |||
| { | |||
| await _audioLogger.WarningAsync("Failed to send keepalive", ex).ConfigureAwait(false); | |||
| } | |||
| await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); | |||
| } | |||
| await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); | |||
| @@ -467,6 +467,7 @@ namespace Discord.Audio | |||
| { | |||
| StopAsync().GetAwaiter().GetResult(); | |||
| ApiClient.Dispose(); | |||
| _stateLock?.Dispose(); | |||
| } | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -27,7 +27,7 @@ namespace Discord.Audio.Streams | |||
| private readonly AudioClient _client; | |||
| private readonly AudioStream _next; | |||
| private readonly CancellationTokenSource _cancelTokenSource; | |||
| private readonly CancellationTokenSource _disposeTokenSource, _cancelTokenSource; | |||
| private readonly CancellationToken _cancelToken; | |||
| private readonly Task _task; | |||
| private readonly ConcurrentQueue<Frame> _queuedFrames; | |||
| @@ -49,12 +49,13 @@ namespace Discord.Audio.Streams | |||
| _logger = logger; | |||
| _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token; | |||
| _disposeTokenSource = new CancellationTokenSource(); | |||
| _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_disposeTokenSource.Token, cancelToken); | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| _queuedFrames = new ConcurrentQueue<Frame>(); | |||
| _bufferPool = new ConcurrentQueue<byte[]>(); | |||
| for (int i = 0; i < _queueLength; i++) | |||
| _bufferPool.Enqueue(new byte[maxFrameSize]); | |||
| _bufferPool.Enqueue(new byte[maxFrameSize]); | |||
| _queueLock = new SemaphoreSlim(_queueLength, _queueLength); | |||
| _silenceFrames = MaxSilenceFrames; | |||
| @@ -63,7 +64,12 @@ namespace Discord.Audio.Streams | |||
| protected override void Dispose(bool disposing) | |||
| { | |||
| if (disposing) | |||
| _cancelTokenSource.Cancel(); | |||
| { | |||
| _disposeTokenSource?.Cancel(); | |||
| _disposeTokenSource?.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| _queueLock?.Dispose(); | |||
| } | |||
| base.Dispose(disposing); | |||
| } | |||
| @@ -131,8 +137,12 @@ namespace Discord.Audio.Streams | |||
| public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore, we use our own timing | |||
| public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) | |||
| { | |||
| CancellationTokenSource writeCancelToken = null; | |||
| if (cancelToken.CanBeCanceled) | |||
| cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token; | |||
| { | |||
| writeCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken); | |||
| cancelToken = writeCancelToken.Token; | |||
| } | |||
| else | |||
| cancelToken = _cancelToken; | |||
| @@ -142,6 +152,9 @@ namespace Discord.Audio.Streams | |||
| #if DEBUG | |||
| var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock | |||
| #endif | |||
| #pragma warning disable IDISP016 | |||
| writeCancelToken?.Dispose(); | |||
| #pragma warning restore IDISP016 | |||
| return; | |||
| } | |||
| Buffer.BlockCopy(data, offset, buffer, 0, count); | |||
| @@ -153,6 +166,7 @@ namespace Discord.Audio.Streams | |||
| #endif | |||
| _isPreloaded = true; | |||
| } | |||
| writeCancelToken?.Dispose(); | |||
| } | |||
| public override async Task FlushAsync(CancellationToken cancelToken) | |||
| @@ -96,7 +96,17 @@ namespace Discord.Audio.Streams | |||
| protected override void Dispose(bool isDisposing) | |||
| { | |||
| _isDisposed = true; | |||
| if (!_isDisposed) | |||
| { | |||
| if (isDisposing) | |||
| { | |||
| _signal?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| base.Dispose(isDisposing); | |||
| } | |||
| } | |||
| } | |||
| @@ -6,7 +6,7 @@ using Discord.Net; | |||
| namespace Discord | |||
| { | |||
| internal class ConnectionManager | |||
| internal class ConnectionManager : IDisposable | |||
| { | |||
| public event Func<Task> Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | |||
| @@ -23,10 +23,12 @@ namespace Discord | |||
| private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken; | |||
| private Task _task; | |||
| private bool _isDisposed; | |||
| public ConnectionState State { get; private set; } | |||
| public CancellationToken CancelToken { get; private set; } | |||
| internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, | |||
| internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, | |||
| Func<Task> onConnecting, Func<Exception, Task> onDisconnecting, Action<Func<Exception, Task>> clientDisconnectHandler) | |||
| { | |||
| _stateLock = stateLock; | |||
| @@ -55,6 +57,7 @@ namespace Discord | |||
| { | |||
| await AcquireConnectionLock().ConfigureAwait(false); | |||
| var reconnectCancelToken = new CancellationTokenSource(); | |||
| _reconnectCancelToken?.Dispose(); | |||
| _reconnectCancelToken = reconnectCancelToken; | |||
| _task = Task.Run(async () => | |||
| { | |||
| @@ -67,16 +70,16 @@ namespace Discord | |||
| try | |||
| { | |||
| await ConnectAsync(reconnectCancelToken).ConfigureAwait(false); | |||
| nextReconnectDelay = 1000; //Reset delay | |||
| nextReconnectDelay = 1000; //Reset delay | |||
| await _connectionPromise.Task.ConfigureAwait(false); | |||
| } | |||
| catch (OperationCanceledException ex) | |||
| { | |||
| 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) | |||
| { | |||
| catch (Exception ex) | |||
| { | |||
| Error(ex); //In case this exception didn't come from another Error call | |||
| if (!reconnectCancelToken.IsCancellationRequested) | |||
| { | |||
| @@ -113,6 +116,8 @@ namespace Discord | |||
| private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) | |||
| { | |||
| _connectionCancelToken?.Dispose(); | |||
| _combinedCancelToken?.Dispose(); | |||
| _connectionCancelToken = new CancellationTokenSource(); | |||
| _combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token); | |||
| CancelToken = _combinedCancelToken.Token; | |||
| @@ -120,7 +125,7 @@ namespace Discord | |||
| _connectionPromise = new TaskCompletionSource<bool>(); | |||
| State = ConnectionState.Connecting; | |||
| await _logger.InfoAsync("Connecting").ConfigureAwait(false); | |||
| try | |||
| { | |||
| var readyPromise = new TaskCompletionSource<bool>(); | |||
| @@ -206,5 +211,25 @@ namespace Discord | |||
| break; | |||
| } | |||
| } | |||
| protected virtual void Dispose(bool disposing) | |||
| { | |||
| if (!_isDisposed) | |||
| { | |||
| if (disposing) | |||
| { | |||
| _combinedCancelToken?.Dispose(); | |||
| _reconnectCancelToken?.Dispose(); | |||
| _connectionCancelToken?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| public void Dispose() | |||
| { | |||
| Dispose(true); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -18,7 +18,9 @@ namespace Discord.WebSocket | |||
| private int[] _shardIds; | |||
| private DiscordSocketClient[] _shards; | |||
| private int _totalShards; | |||
| private bool _isDisposed; | |||
| /// <inheritdoc /> | |||
| public override int Latency { get => GetLatency(); protected set { } } | |||
| /// <inheritdoc /> | |||
| @@ -38,11 +40,15 @@ namespace Discord.WebSocket | |||
| /// <summary> Creates a new REST/WebSocket Discord client. </summary> | |||
| public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } | |||
| /// <summary> Creates a new REST/WebSocket Discord client. </summary> | |||
| #pragma warning disable IDISP004 | |||
| public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } | |||
| #pragma warning restore IDISP004 | |||
| /// <summary> Creates a new REST/WebSocket Discord client. </summary> | |||
| public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } | |||
| /// <summary> Creates a new REST/WebSocket Discord client. </summary> | |||
| #pragma warning disable IDISP004 | |||
| public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } | |||
| #pragma warning restore IDISP004 | |||
| private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) | |||
| : base(config, client) | |||
| { | |||
| @@ -119,10 +125,10 @@ namespace Discord.WebSocket | |||
| } | |||
| /// <inheritdoc /> | |||
| public override async Task StartAsync() | |||
| public override async Task StartAsync() | |||
| => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| public override async Task StopAsync() | |||
| public override async Task StopAsync() | |||
| => await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); | |||
| public DiscordSocketClient GetShard(int id) | |||
| @@ -145,7 +151,7 @@ namespace Discord.WebSocket | |||
| => await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| public override SocketGuild GetGuild(ulong id) | |||
| public override SocketGuild GetGuild(ulong id) | |||
| => GetShardFor(id).GetGuild(id); | |||
| /// <inheritdoc /> | |||
| @@ -173,7 +179,7 @@ namespace Discord.WebSocket | |||
| for (int i = 0; i < _shards.Length; i++) | |||
| result += _shards[i].PrivateChannels.Count; | |||
| return result; | |||
| } | |||
| } | |||
| private IEnumerable<SocketGuild> GetGuilds() | |||
| { | |||
| @@ -189,7 +195,7 @@ namespace Discord.WebSocket | |||
| for (int i = 0; i < _shards.Length; i++) | |||
| result += _shards[i].Guilds.Count; | |||
| return result; | |||
| } | |||
| } | |||
| /// <inheritdoc /> | |||
| public override SocketUser GetUser(ulong id) | |||
| @@ -369,5 +375,22 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) | |||
| => Task.FromResult<IVoiceRegion>(GetVoiceRegion(id)); | |||
| internal override void Dispose(bool disposing) | |||
| { | |||
| if (!_isDisposed) | |||
| { | |||
| if (disposing) | |||
| { | |||
| foreach (var client in _shards) | |||
| client?.Dispose(); | |||
| _connectionGroupLock?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| base.Dispose(disposing); | |||
| } | |||
| } | |||
| } | |||
| @@ -108,6 +108,8 @@ namespace Discord.API | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| base.Dispose(disposing); | |||
| } | |||
| public async Task ConnectAsync() | |||
| @@ -137,6 +139,7 @@ namespace Discord.API | |||
| ConnectionState = ConnectionState.Connecting; | |||
| try | |||
| { | |||
| _connectCancelToken?.Dispose(); | |||
| _connectCancelToken = new CancellationTokenSource(); | |||
| if (WebSocketClient != null) | |||
| WebSocketClient.SetCancelToken(_connectCancelToken.Token); | |||
| @@ -209,7 +212,7 @@ namespace Discord.API | |||
| await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); | |||
| await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | |||
| } | |||
| public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) | |||
| { | |||
| options = RequestOptions.CreateOrClone(options); | |||
| @@ -43,6 +43,8 @@ namespace Discord.WebSocket | |||
| private DateTimeOffset? _statusSince; | |||
| private RestApplication _applicationInfo; | |||
| private bool _isDisposed; | |||
| /// <summary> Gets the shard of of this client. </summary> | |||
| public int ShardId { get; } | |||
| /// <summary> Gets the current connection state of this client. </summary> | |||
| @@ -63,7 +65,7 @@ namespace Discord.WebSocket | |||
| internal WebSocketProvider WebSocketProvider { get; private set; } | |||
| internal bool AlwaysDownloadUsers { get; private set; } | |||
| internal int? HandlerTimeout { get; private set; } | |||
| internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; | |||
| /// <inheritdoc /> | |||
| public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; | |||
| @@ -110,8 +112,10 @@ namespace Discord.WebSocket | |||
| /// Initializes a new REST/WebSocket-based Discord client with the provided configuration. | |||
| /// </summary> | |||
| /// <param name="config">The configuration to be used with the client.</param> | |||
| #pragma warning disable IDISP004 | |||
| public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { } | |||
| internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } | |||
| #pragma warning restore IDISP004 | |||
| private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient) | |||
| : base(config, client) | |||
| { | |||
| @@ -170,11 +174,18 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| internal override void Dispose(bool disposing) | |||
| { | |||
| if (disposing) | |||
| if (!_isDisposed) | |||
| { | |||
| StopAsync().GetAwaiter().GetResult(); | |||
| ApiClient.Dispose(); | |||
| if (disposing) | |||
| { | |||
| StopAsync().GetAwaiter().GetResult(); | |||
| ApiClient?.Dispose(); | |||
| _stateLock?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| base.Dispose(disposing); | |||
| } | |||
| /// <inheritdoc /> | |||
| @@ -197,10 +208,10 @@ namespace Discord.WebSocket | |||
| } | |||
| /// <inheritdoc /> | |||
| public override async Task StartAsync() | |||
| public override async Task StartAsync() | |||
| => await _connection.StartAsync().ConfigureAwait(false); | |||
| /// <inheritdoc /> | |||
| public override async Task StopAsync() | |||
| public override async Task StopAsync() | |||
| => await _connection.StopAsync().ConfigureAwait(false); | |||
| private async Task OnConnectingAsync() | |||
| @@ -704,6 +715,7 @@ namespace Discord.WebSocket | |||
| { | |||
| await GuildUnavailableAsync(guild).ConfigureAwait(false); | |||
| await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); | |||
| (guild as IDisposable).Dispose(); | |||
| } | |||
| else | |||
| { | |||
| @@ -16,7 +16,7 @@ using System.Threading.Tasks; | |||
| namespace Discord.Audio | |||
| { | |||
| internal class DiscordVoiceAPIClient | |||
| internal class DiscordVoiceAPIClient : IDisposable | |||
| { | |||
| public const int MaxBitrate = 128 * 1024; | |||
| public const string Mode = "xsalsa20_poly1305"; | |||
| @@ -36,7 +36,7 @@ namespace Discord.Audio | |||
| private readonly AsyncEvent<Func<byte[], Task>> _receivedPacketEvent = new AsyncEvent<Func<byte[], Task>>(); | |||
| public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | |||
| private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | |||
| private readonly JsonSerializer _serializer; | |||
| private readonly SemaphoreSlim _connectionLock; | |||
| private readonly IUdpSocket _udp; | |||
| @@ -103,8 +103,9 @@ namespace Discord.Audio | |||
| if (disposing) | |||
| { | |||
| _connectCancelToken?.Dispose(); | |||
| (_udp as IDisposable)?.Dispose(); | |||
| (WebSocketClient as IDisposable)?.Dispose(); | |||
| _udp?.Dispose(); | |||
| WebSocketClient?.Dispose(); | |||
| _connectionLock?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| @@ -122,7 +123,7 @@ namespace Discord.Audio | |||
| } | |||
| public async Task SendAsync(byte[] data, int offset, int bytes) | |||
| { | |||
| await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false); | |||
| await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false); | |||
| await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false); | |||
| } | |||
| @@ -177,6 +178,7 @@ namespace Discord.Audio | |||
| ConnectionState = ConnectionState.Connecting; | |||
| try | |||
| { | |||
| _connectCancelToken?.Dispose(); | |||
| _connectCancelToken = new CancellationTokenSource(); | |||
| var cancelToken = _connectCancelToken.Token; | |||
| @@ -208,7 +210,7 @@ namespace Discord.Audio | |||
| { | |||
| if (ConnectionState == ConnectionState.Disconnected) return; | |||
| ConnectionState = ConnectionState.Disconnecting; | |||
| try { _connectCancelToken?.Cancel(false); } | |||
| catch { } | |||
| @@ -25,8 +25,9 @@ namespace Discord.WebSocket | |||
| /// Represents a WebSocket-based guild object. | |||
| /// </summary> | |||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
| public class SocketGuild : SocketEntity<ulong>, IGuild | |||
| public class SocketGuild : SocketEntity<ulong>, IGuild, IDisposable | |||
| { | |||
| #pragma warning disable IDISP002, IDISP006 | |||
| private readonly SemaphoreSlim _audioLock; | |||
| private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | |||
| private TaskCompletionSource<AudioClient> _audioConnectPromise; | |||
| @@ -37,6 +38,7 @@ namespace Discord.WebSocket | |||
| private ImmutableArray<GuildEmote> _emotes; | |||
| private ImmutableArray<string> _features; | |||
| private AudioClient _audioClient; | |||
| #pragma warning restore IDISP002, IDISP006 | |||
| /// <inheritdoc /> | |||
| public string Name { get; private set; } | |||
| @@ -63,7 +65,7 @@ namespace Discord.WebSocket | |||
| /// number here is the most accurate in terms of counting the number of users within this guild. | |||
| /// </para> | |||
| /// <para> | |||
| /// Use this instead of enumerating the count of the | |||
| /// Use this instead of enumerating the count of the | |||
| /// <see cref="Discord.WebSocket.SocketGuild.Users" /> collection, as you may see discrepancy | |||
| /// between that and this property. | |||
| /// </para> | |||
| @@ -872,9 +874,11 @@ namespace Discord.WebSocket | |||
| if (external) | |||
| { | |||
| #pragma warning disable IDISP001 | |||
| var _ = promise.TrySetResultAsync(null); | |||
| await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); | |||
| return null; | |||
| #pragma warning restore IDISP001 | |||
| } | |||
| if (_audioClient == null) | |||
| @@ -897,10 +901,14 @@ namespace Discord.WebSocket | |||
| }; | |||
| audioClient.Connected += () => | |||
| { | |||
| #pragma warning disable IDISP001 | |||
| var _ = promise.TrySetResultAsync(_audioClient); | |||
| #pragma warning restore IDISP001 | |||
| return Task.Delay(0); | |||
| }; | |||
| #pragma warning disable IDISP003 | |||
| _audioClient = audioClient; | |||
| #pragma warning restore IDISP003 | |||
| } | |||
| await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); | |||
| @@ -948,6 +956,7 @@ namespace Discord.WebSocket | |||
| if (_audioClient != null) | |||
| await _audioClient.StopAsync().ConfigureAwait(false); | |||
| await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); | |||
| _audioClient?.Dispose(); | |||
| _audioClient = null; | |||
| } | |||
| internal async Task FinishConnectAudio(string url, string token) | |||
| @@ -1130,5 +1139,12 @@ namespace Discord.WebSocket | |||
| /// <inheritdoc /> | |||
| async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | |||
| => await GetWebhooksAsync(options).ConfigureAwait(false); | |||
| void IDisposable.Dispose() | |||
| { | |||
| DisconnectAudioAsync().GetAwaiter().GetResult(); | |||
| _audioLock?.Dispose(); | |||
| _audioClient?.Dispose(); | |||
| } | |||
| } | |||
| } | |||
| @@ -13,24 +13,29 @@ namespace Discord.Net.Udp | |||
| private readonly SemaphoreSlim _lock; | |||
| private UdpClient _udp; | |||
| private IPEndPoint _destination; | |||
| private CancellationTokenSource _cancelTokenSource; | |||
| private CancellationTokenSource _stopCancelTokenSource, _cancelTokenSource; | |||
| private CancellationToken _cancelToken, _parentToken; | |||
| private Task _task; | |||
| private bool _isDisposed; | |||
| public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); | |||
| public DefaultUdpSocket() | |||
| { | |||
| _lock = new SemaphoreSlim(1, 1); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _stopCancelTokenSource = new CancellationTokenSource(); | |||
| } | |||
| private void Dispose(bool disposing) | |||
| { | |||
| if (!_isDisposed) | |||
| { | |||
| if (disposing) | |||
| { | |||
| StopInternalAsync(true).GetAwaiter().GetResult(); | |||
| _stopCancelTokenSource?.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| _lock?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| @@ -56,9 +61,14 @@ namespace Discord.Net.Udp | |||
| { | |||
| await StopInternalAsync().ConfigureAwait(false); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | |||
| _stopCancelTokenSource?.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| _stopCancelTokenSource = new CancellationTokenSource(); | |||
| _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _stopCancelTokenSource.Token); | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| _udp?.Dispose(); | |||
| _udp = new UdpClient(0); | |||
| _task = RunAsync(_cancelToken); | |||
| @@ -77,7 +87,7 @@ namespace Discord.Net.Udp | |||
| } | |||
| public async Task StopInternalAsync(bool isDisposing = false) | |||
| { | |||
| try { _cancelTokenSource.Cancel(false); } catch { } | |||
| try { _stopCancelTokenSource.Cancel(false); } catch { } | |||
| if (!isDisposing) | |||
| await (_task ?? Task.Delay(0)).ConfigureAwait(false); | |||
| @@ -96,8 +106,11 @@ namespace Discord.Net.Udp | |||
| } | |||
| public void SetCancelToken(CancellationToken cancelToken) | |||
| { | |||
| _cancelTokenSource?.Dispose(); | |||
| _parentToken = cancelToken; | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | |||
| _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _stopCancelTokenSource.Token); | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| } | |||
| public async Task SendAsync(byte[] data, int index, int count) | |||
| @@ -25,14 +25,14 @@ namespace Discord.Net.WebSockets | |||
| private readonly IWebProxy _proxy; | |||
| private ClientWebSocket _client; | |||
| private Task _task; | |||
| private CancellationTokenSource _cancelTokenSource; | |||
| private CancellationTokenSource _disconnectTokenSource, _cancelTokenSource; | |||
| private CancellationToken _cancelToken, _parentToken; | |||
| private bool _isDisposed, _isDisconnecting; | |||
| public DefaultWebSocketClient(IWebProxy proxy = null) | |||
| { | |||
| _lock = new SemaphoreSlim(1, 1); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _disconnectTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = CancellationToken.None; | |||
| _parentToken = CancellationToken.None; | |||
| _headers = new Dictionary<string, string>(); | |||
| @@ -43,7 +43,12 @@ namespace Discord.Net.WebSockets | |||
| if (!_isDisposed) | |||
| { | |||
| if (disposing) | |||
| { | |||
| DisconnectInternalAsync(true).GetAwaiter().GetResult(); | |||
| _disconnectTokenSource?.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| _lock?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| @@ -68,9 +73,14 @@ namespace Discord.Net.WebSockets | |||
| { | |||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||
| _cancelTokenSource = new CancellationTokenSource(); | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | |||
| _disconnectTokenSource?.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| _disconnectTokenSource = new CancellationTokenSource(); | |||
| _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectTokenSource.Token); | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| _client?.Dispose(); | |||
| _client = new ClientWebSocket(); | |||
| _client.Options.Proxy = _proxy; | |||
| _client.Options.KeepAliveInterval = TimeSpan.Zero; | |||
| @@ -98,7 +108,7 @@ namespace Discord.Net.WebSockets | |||
| } | |||
| private async Task DisconnectInternalAsync(bool isDisposing = false) | |||
| { | |||
| try { _cancelTokenSource.Cancel(false); } catch { } | |||
| try { _disconnectTokenSource.Cancel(false); } catch { } | |||
| _isDisconnecting = true; | |||
| try | |||
| @@ -117,7 +127,7 @@ namespace Discord.Net.WebSockets | |||
| } | |||
| try { _client.Dispose(); } | |||
| catch { } | |||
| _client = null; | |||
| } | |||
| } | |||
| @@ -144,8 +154,11 @@ namespace Discord.Net.WebSockets | |||
| } | |||
| public void SetCancelToken(CancellationToken cancelToken) | |||
| { | |||
| _cancelTokenSource?.Dispose(); | |||
| _parentToken = cancelToken; | |||
| _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; | |||
| _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectTokenSource.Token); | |||
| _cancelToken = _cancelTokenSource.Token; | |||
| } | |||
| public async Task SendAsync(byte[] data, int index, int count, bool isText) | |||
| @@ -166,7 +179,7 @@ namespace Discord.Net.WebSockets | |||
| frameSize = count - (i * SendChunkSize); | |||
| else | |||
| frameSize = SendChunkSize; | |||
| var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; | |||
| await _client.SendAsync(new ArraySegment<byte>(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); | |||
| } | |||
| @@ -176,7 +189,7 @@ namespace Discord.Net.WebSockets | |||
| _lock.Release(); | |||
| } | |||
| } | |||
| private async Task RunAsync(CancellationToken cancelToken) | |||
| { | |||
| var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]); | |||
| @@ -188,7 +201,7 @@ namespace Discord.Net.WebSockets | |||
| WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); | |||
| byte[] result; | |||
| int resultCount; | |||
| if (socketResult.MessageType == WebSocketMessageType.Close) | |||
| throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); | |||
| @@ -219,7 +232,7 @@ namespace Discord.Net.WebSockets | |||
| resultCount = socketResult.Count; | |||
| result = buffer.Array; | |||
| } | |||
| if (socketResult.MessageType == WebSocketMessageType.Text) | |||
| { | |||
| string text = Encoding.UTF8.GetString(result, 0, resultCount); | |||
| @@ -10,4 +10,4 @@ | |||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
| <ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| </Project> | |||
| @@ -5,6 +5,7 @@ | |||
| <TargetFramework>netcoreapp1.1</TargetFramework> | |||
| <DebugType>portable</DebugType> | |||
| <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> | |||
| <NoWarn>IDISP001,IDISP002,IDISP004,IDISP005</NoWarn> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <Content Include="xunit.runner.json"> | |||
| @@ -43,7 +43,10 @@ namespace Discord.Net | |||
| if (!_isDisposed) | |||
| { | |||
| if (disposing) | |||
| { | |||
| _blobCache.Dispose(); | |||
| _cancelTokenSource?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| } | |||
| } | |||
| @@ -70,7 +73,7 @@ namespace Discord.Net | |||
| { | |||
| if (method != "GET") | |||
| throw new InvalidOperationException("This RestClient only supports GET requests."); | |||
| string uri = Path.Combine(_baseUrl, endpoint); | |||
| var bytes = await _blobCache.DownloadUrl(uri, _headers); | |||
| return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); | |||
| @@ -84,7 +87,7 @@ namespace Discord.Net | |||
| throw new InvalidOperationException("This RestClient does not support multipart requests."); | |||
| } | |||
| public async Task ClearAsync() | |||
| public async Task ClearAsync() | |||
| { | |||
| await _blobCache.InvalidateAll(); | |||
| } | |||
| @@ -93,7 +96,7 @@ namespace Discord.Net | |||
| { | |||
| if (Info != null) | |||
| return; | |||
| bool needsReset = false; | |||
| try | |||
| { | |||
| @@ -117,4 +120,4 @@ namespace Discord.Net | |||
| await _blobCache.InsertObject<CacheInfo>("info", Info); | |||
| } | |||
| } | |||
| } | |||
| } | |||