diff --git a/samples/01_basic_ping_bot/Program.cs b/samples/01_basic_ping_bot/Program.cs index 973a6ce95..4d6674e97 100644 --- a/samples/01_basic_ping_bot/Program.cs +++ b/samples/01_basic_ping_bot/Program.cs @@ -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(); diff --git a/samples/02_commands_framework/Program.cs b/samples/02_commands_framework/Program.cs index 136494bdf..76c11f9f0 100644 --- a/samples/02_commands_framework/Program.cs +++ b/samples/02_commands_framework/Program.cs @@ -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(); - var client = services.GetRequiredService(); + client.Log += LogAsync; + services.GetRequiredService().Log += LogAsync; - client.Log += LogAsync; - services.GetRequiredService().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().InitializeAsync(); - await services.GetRequiredService().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() diff --git a/samples/03_sharded_client/Program.cs b/samples/03_sharded_client/Program.cs index 048145f9f..7a2f99168 100644 --- a/samples/03_sharded_client/Program.cs +++ b/samples/03_sharded_client/Program.cs @@ -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(); - // 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().InitializeAsync(); + await services.GetRequiredService().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() .AddSingleton() .BuildServiceProvider(); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index d5b3f9ff4..c36aec4a5 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -27,7 +27,7 @@ namespace Discord.Commands /// been successfully executed. /// /// - public class CommandService + public class CommandService : IDisposable { /// /// Occurs when a command-related information is received. @@ -67,6 +67,8 @@ namespace Discord.Commands internal readonly LogManager _logManager; internal readonly IReadOnlyDictionary _quotationMarkAliasMap; + internal bool _isDisposed; + /// /// Represents all modules loaded within . /// @@ -330,9 +332,9 @@ namespace Discord.Commands //Type Readers /// - /// Adds a custom to this for the supplied object + /// Adds a custom to this for the supplied object /// type. - /// If is a , a nullable will + /// If is a , a nullable will /// also be added. /// If a default exists for , a warning will be logged /// and the default 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); + } } } diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index a754486dd..1bef1bfea 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index d4d450e1c..ff9b3c5e0 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -12,4 +12,7 @@ + + + diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index d5a9e26a3..3f5a01f6a 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -1,15 +1,21 @@ +using System; using System.IO; + namespace Discord { /// /// An image that will be uploaded to Discord. /// - public struct Image + public struct Image : IDisposable { + private bool _isDisposed; + /// /// Gets the stream to be uploaded to Discord. /// +#pragma warning disable IDISP008 public Stream Stream { get; } +#pragma warning restore IDISP008 /// /// Create the image with a . /// @@ -19,6 +25,7 @@ namespace Discord /// public Image(Stream stream) { + _isDisposed = false; Stream = stream; } @@ -45,15 +52,28 @@ namespace Discord /// The specified is invalid, (for example, it is on an unmapped drive). /// /// - /// specified a directory.-or- The caller does not have the required permission. + /// specified a directory.-or- The caller does not have the required permission. /// - /// The file specified in was not found. + /// The file specified in was not found. /// /// An I/O error occurred while opening the file. public Image(string path) { + _isDisposed = false; Stream = File.OpenRead(path); } + /// + public void Dispose() + { + if (!_isDisposed) + { +#pragma warning disable IDISP007 + Stream?.Dispose(); +#pragma warning restore IDISP007 + + _isDisposed = true; + } + } } } diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index 2e30d2ef4..71010f70d 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -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 /// /// Represents a generic REST-based client. /// - public interface IRestClient + public interface IRestClient : IDisposable { /// /// Sets the HTTP header of this client for all requests. diff --git a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs index 10ac652b3..ed2881d1f 100644 --- a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs +++ b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Net.Udp { - public interface IUdpSocket + public interface IUdpSocket : IDisposable { event Func ReceivedDatagram; diff --git a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs index 7eccaabf2..14b41cce1 100644 --- a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Net.WebSockets { - public interface IWebSocketClient + public interface IWebSocketClient : IDisposable { event Func BinaryMessage; event Func TextMessage; diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs index afc01f87a..ef99c8045 100644 --- a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs +++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs @@ -19,6 +19,7 @@ namespace Discord.Net.Providers.WS4Net private readonly SemaphoreSlim _lock; private readonly Dictionary _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(); _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) diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index b584f5764..fc938d04d 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -34,7 +34,7 @@ namespace Discord.Rest public ISelfUser CurrentUser { get; protected set; } /// public TokenType TokenType => ApiClient.AuthTokenType; - + /// Creates a new REST-only Discord client. 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; /// - Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => throw new NotSupportedException(); /// diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 57d7c718a..eeeea4139 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -66,6 +66,7 @@ namespace Discord.API /// Unknown OAuth token type. 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 SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); - public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, + public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index e36353855..f96d5dd0b 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -31,6 +31,8 @@ namespace Discord.Rest { if (disposing) ApiClient.Dispose(); + + base.Dispose(disposing); } /// @@ -46,12 +48,12 @@ namespace Discord.Rest _applicationInfo = null; return Task.Delay(0); } - + public async Task GetApplicationInfoAsync(RequestOptions options = null) { return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); } - + public Task GetChannelAsync(ulong id, RequestOptions options = null) => ClientHelper.GetChannelAsync(this, id, options); public Task> GetPrivateChannelsAsync(RequestOptions options = null) @@ -60,7 +62,7 @@ namespace Discord.Rest => ClientHelper.GetDMChannelsAsync(this, options); public Task> GetGroupChannelsAsync(RequestOptions options = null) => ClientHelper.GetGroupChannelsAsync(this, options); - + public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options); @@ -79,12 +81,12 @@ namespace Discord.Rest => ClientHelper.GetGuildsAsync(this, options); public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); - + public Task GetUserAsync(ulong id, RequestOptions options = null) => ClientHelper.GetUserAsync(this, id, options); public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) => ClientHelper.GetGuildUserAsync(this, guildId, id, options); - + public Task> GetVoiceRegionsAsync(RequestOptions options = null) => ClientHelper.GetVoiceRegionsAsync(this, options); public Task GetVoiceRegionAsync(string id, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs index 9bbcfb8a3..941a35bf1 100644 --- a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs @@ -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); diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 05a568198..b5036d94e 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -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 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"); diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 1b4830da2..4baf76433 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -16,23 +16,24 @@ namespace Discord.Net.Queue private readonly ConcurrentDictionary _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(); _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 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(); } } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 800e04933..2210e019f 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -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>(); _ssrcMap = new ConcurrentDictionary(); _streams = new ConcurrentDictionary(); - + _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(); } } /// diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 47a7e2809..16ad0ae89 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -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 _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(); _bufferPool = new ConcurrentQueue(); 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) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index b9d6157ea..6233c47b5 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -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); } } } diff --git a/src/Discord.Net.WebSocket/ConnectionManager.cs b/src/Discord.Net.WebSocket/ConnectionManager.cs index decae4163..66f847c50 100644 --- a/src/Discord.Net.WebSocket/ConnectionManager.cs +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -6,7 +6,7 @@ using Discord.Net; namespace Discord { - internal class ConnectionManager + internal class ConnectionManager : IDisposable { public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); @@ -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 onConnecting, Func onDisconnecting, Action> 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(); State = ConnectionState.Connecting; await _logger.InfoAsync("Connecting").ConfigureAwait(false); - + try { var readyPromise = new TaskCompletionSource(); @@ -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); + } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 4e497346f..03969f535 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -18,7 +18,9 @@ namespace Discord.WebSocket private int[] _shardIds; private DiscordSocketClient[] _shards; private int _totalShards; - + + private bool _isDisposed; + /// public override int Latency { get => GetLatency(); protected set { } } /// @@ -38,11 +40,15 @@ namespace Discord.WebSocket /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket Discord client. +#pragma warning disable IDISP004 public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } +#pragma warning restore IDISP004 /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket Discord client. +#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 } /// - public override async Task StartAsync() + public override async Task StartAsync() => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); /// - 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); /// - public override SocketGuild GetGuild(ulong id) + public override SocketGuild GetGuild(ulong id) => GetShardFor(id).GetGuild(id); /// @@ -173,7 +179,7 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) result += _shards[i].PrivateChannels.Count; return result; - } + } private IEnumerable GetGuilds() { @@ -189,7 +195,7 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) result += _shards[i].Guilds.Count; return result; - } + } /// public override SocketUser GetUser(ulong id) @@ -369,5 +375,22 @@ namespace Discord.WebSocket /// Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(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); + } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index bc579793d..894ae66a8 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -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); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 3d260d1a6..6b720645e 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -43,6 +43,8 @@ namespace Discord.WebSocket private DateTimeOffset? _statusSince; private RestApplication _applicationInfo; + private bool _isDisposed; + /// Gets the shard of of this client. public int ShardId { get; } /// Gets the current connection state of this client. @@ -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; /// public override IReadOnlyCollection Guilds => State.Guilds; @@ -110,8 +112,10 @@ namespace Discord.WebSocket /// Initializes a new REST/WebSocket-based Discord client with the provided configuration. /// /// The configuration to be used with the client. +#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 /// 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); } /// @@ -197,10 +208,10 @@ namespace Discord.WebSocket } /// - public override async Task StartAsync() + public override async Task StartAsync() => await _connection.StartAsync().ConfigureAwait(false); /// - 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 { diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index 80dec0fd4..f78145dbe 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -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> _receivedPacketEvent = new AsyncEvent>(); public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - + 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 { } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 4e4124679..ca2db1a77 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -25,8 +25,9 @@ namespace Discord.WebSocket /// Represents a WebSocket-based guild object. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketGuild : SocketEntity, IGuild + public class SocketGuild : SocketEntity, IGuild, IDisposable { +#pragma warning disable IDISP002, IDISP006 private readonly SemaphoreSlim _audioLock; private TaskCompletionSource _syncPromise, _downloaderPromise; private TaskCompletionSource _audioConnectPromise; @@ -37,6 +38,7 @@ namespace Discord.WebSocket private ImmutableArray _emotes; private ImmutableArray _features; private AudioClient _audioClient; +#pragma warning restore IDISP002, IDISP006 /// 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. /// /// - /// Use this instead of enumerating the count of the + /// Use this instead of enumerating the count of the /// collection, as you may see discrepancy /// between that and this property. /// @@ -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 /// async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); + + void IDisposable.Dispose() + { + DisconnectAudioAsync().GetAwaiter().GetResult(); + _audioLock?.Dispose(); + _audioClient?.Dispose(); + } } } diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs index 251a761d4..4b37de28f 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -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) diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index dc5201ac1..df2da5813 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -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(); @@ -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(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(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); diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index ba7bbcff8..58282d85b 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 0ee6f7e59..aa6f86a34 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -5,6 +5,7 @@ netcoreapp1.1 portable $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 + IDISP001,IDISP002,IDISP004,IDISP005 diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs index 4bc8a386a..c465eaa01 100644 --- a/test/Discord.Net.Tests/Net/CachedRestClient.cs +++ b/test/Discord.Net.Tests/Net/CachedRestClient.cs @@ -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("info", Info); } } -} \ No newline at end of file +}