feature: Implement Dispose for types which have disposable data (#1171)

* 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 feedback
This commit is contained in:
Monica S
2018-11-29 01:18:16 +00:00
committed by Christopher F
parent dca6c33da3
commit 7366cd4361
31 changed files with 406 additions and 154 deletions

View File

@@ -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();
public async Task MainAsync()
{
new Program().MainAsync().GetAwaiter().GetResult();
}
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();

View File

@@ -19,18 +19,25 @@ 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>();
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();
@@ -38,6 +45,7 @@ namespace _02_commands_framework
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>()

View File

@@ -13,8 +13,6 @@ 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()
@@ -27,27 +25,34 @@ namespace _03_sharded_client
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;
client.ShardReady += ReadyAsync;
client.Log += LogAsync;
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);
}
}
private IServiceProvider ConfigureServices()
private ServiceProvider ConfigureServices(DiscordSocketConfig config)
{
return new ServiceCollection()
.AddSingleton(_client)
.AddSingleton(new DiscordShardedClient(config))
.AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>()
.BuildServiceProvider();

View File

@@ -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>
@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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;
}
@@ -52,8 +59,21 @@ namespace Discord
/// <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;
}
}
}
}

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)

View File

@@ -138,7 +138,10 @@ namespace Discord.Rest
{
if (!_isDisposed)
{
#pragma warning disable IDISP007
ApiClient.Dispose();
#pragma warning restore IDISP007
_stateLock?.Dispose();
_isDisposed = true;
}
}

View File

@@ -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;

View File

@@ -31,6 +31,8 @@ namespace Discord.Rest
{
if (disposing)
ApiClient.Dispose();
base.Dispose(disposing);
}
/// <inheritdoc />

View File

@@ -34,13 +34,15 @@ namespace Discord.Net.Converters
}
else
{
var cloneStream = new MemoryStream();
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);
writer.WriteValue($"data:image/jpeg;base64,{base64}");

View File

@@ -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,13 +125,17 @@ 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;
using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken))
{
cancelToken = cancelTokenSource.Token;
HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false);
var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);
@@ -132,6 +143,7 @@ namespace Discord.Net.Rest
return new RestResponse(response.StatusCode, headers, stream);
}
}
private static readonly HttpMethod Patch = new HttpMethod("PATCH");
private HttpMethod GetMethod(string method)

View File

@@ -16,9 +16,10 @@ 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;
@@ -29,7 +30,7 @@ namespace Discord.Net.Queue
_tokenLock = new SemaphoreSlim(1, 1);
_clearToken = new CancellationTokenSource();
_cancelToken = new CancellationTokenSource();
_cancelTokenSource = new CancellationTokenSource();
_requestCancelToken = CancellationToken.None;
_parentToken = CancellationToken.None;
@@ -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();
}
}
}

View File

@@ -467,6 +467,7 @@ namespace Discord.Audio
{
StopAsync().GetAwaiter().GetResult();
ApiClient.Dispose();
_stateLock?.Dispose();
}
}
/// <inheritdoc />

View File

@@ -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,8 +49,9 @@ 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++)
@@ -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)

View File

@@ -96,7 +96,17 @@ namespace Discord.Audio.Streams
protected override void Dispose(bool isDisposing)
{
if (!_isDisposed)
{
if (isDisposing)
{
_signal?.Dispose();
}
_isDisposed = true;
}
base.Dispose(isDisposing);
}
}
}

View File

@@ -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,6 +23,8 @@ 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; }
@@ -55,6 +57,7 @@ namespace Discord
{
await AcquireConnectionLock().ConfigureAwait(false);
var reconnectCancelToken = new CancellationTokenSource();
_reconnectCancelToken?.Dispose();
_reconnectCancelToken = reconnectCancelToken;
_task = Task.Run(async () =>
{
@@ -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;
@@ -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);
}
}
}

View File

@@ -19,6 +19,8 @@ namespace Discord.WebSocket
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)
{
@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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>
@@ -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)
{
@@ -169,12 +173,19 @@ namespace Discord.WebSocket
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost);
/// <inheritdoc />
internal override void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
StopAsync().GetAwaiter().GetResult();
ApiClient.Dispose();
ApiClient?.Dispose();
_stateLock?.Dispose();
}
_isDisposed = true;
}
base.Dispose(disposing);
}
/// <inheritdoc />
@@ -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
{

View File

@@ -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";
@@ -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;
}
@@ -177,6 +178,7 @@ namespace Discord.Audio
ConnectionState = ConnectionState.Connecting;
try
{
_connectCancelToken?.Dispose();
_connectCancelToken = new CancellationTokenSource();
var cancelToken = _connectCancelToken.Token;

View File

@@ -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; }
@@ -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();
}
}
}

View File

@@ -13,7 +13,7 @@ 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;
@@ -23,14 +23,19 @@ namespace Discord.Net.Udp
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)

View File

@@ -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
@@ -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)

View File

@@ -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">

View File

@@ -43,7 +43,10 @@ namespace Discord.Net
if (!_isDisposed)
{
if (disposing)
{
_blobCache.Dispose();
_cancelTokenSource?.Dispose();
}
_isDisposed = true;
}
}