Browse Source

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
tags/2.0
Monica S Christopher F 6 years ago
parent
commit
7366cd4361
31 changed files with 405 additions and 153 deletions
  1. +10
    -3
      samples/01_basic_ping_bot/Program.cs
  2. +18
    -10
      samples/02_commands_framework/Program.cs
  3. +21
    -16
      samples/03_sharded_client/Program.cs
  4. +23
    -3
      src/Discord.Net.Commands/CommandService.cs
  5. +1
    -1
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  6. +3
    -0
      src/Discord.Net.Core/Discord.Net.Core.csproj
  7. +23
    -3
      src/Discord.Net.Core/Entities/Image.cs
  8. +2
    -1
      src/Discord.Net.Core/Net/Rest/IRestClient.cs
  9. +1
    -1
      src/Discord.Net.Core/Net/Udp/IUdpSocket.cs
  10. +1
    -1
      src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs
  11. +17
    -5
      src/Discord.Net.Providers.WS4Net/WS4NetClient.cs
  12. +8
    -5
      src/Discord.Net.Rest/BaseDiscordClient.cs
  13. +6
    -2
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  14. +7
    -5
      src/Discord.Net.Rest/DiscordRestClient.cs
  15. +8
    -6
      src/Discord.Net.Rest/Net/Converters/ImageConverter.cs
  16. +20
    -8
      src/Discord.Net.Rest/Net/DefaultRestClient.cs
  17. +28
    -11
      src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
  18. +13
    -12
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  19. +20
    -6
      src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
  20. +11
    -1
      src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs
  21. +34
    -9
      src/Discord.Net.WebSocket/ConnectionManager.cs
  22. +29
    -6
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  23. +4
    -1
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  24. +18
    -6
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  25. +8
    -6
      src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs
  26. +18
    -2
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  27. +20
    -7
      src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs
  28. +24
    -11
      src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs
  29. +1
    -1
      src/Discord.Net.Webhook/Discord.Net.Webhook.csproj
  30. +1
    -0
      test/Discord.Net.Tests/Discord.Net.Tests.csproj
  31. +7
    -4
      test/Discord.Net.Tests/Net/CachedRestClient.cs

+ 10
- 3
samples/01_basic_ping_bot/Program.cs 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 // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program class Program
{ {
private DiscordSocketClient _client;
private readonly DiscordSocketClient _client;


// Discord.Net heavily utilizes TAP for async, so we create // Discord.Net heavily utilizes TAP for async, so we create
// an asynchronous context from the beginning. // an asynchronous context from the beginning.
static void Main(string[] args) 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 = new DiscordSocketClient();


_client.Log += LogAsync; _client.Log += LogAsync;
_client.Ready += ReadyAsync; _client.Ready += ReadyAsync;
_client.MessageReceived += MessageReceivedAsync; _client.MessageReceived += MessageReceivedAsync;
}


public async Task MainAsync()
{
// Tokens should be considered secret data, and never hard-coded. // Tokens should be considered secret data, and never hard-coded.
await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
await _client.StartAsync(); await _client.StartAsync();


+ 18
- 10
samples/02_commands_framework/Program.cs View File

@@ -19,24 +19,32 @@ namespace _02_commands_framework
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program 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) static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult(); => new Program().MainAsync().GetAwaiter().GetResult();


public async Task MainAsync() 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) private Task LogAsync(LogMessage log)
@@ -46,7 +54,7 @@ namespace _02_commands_framework
return Task.CompletedTask; return Task.CompletedTask;
} }


private IServiceProvider ConfigureServices()
private ServiceProvider ConfigureServices()
{ {
return new ServiceCollection() return new ServiceCollection()
.AddSingleton<DiscordSocketClient>() .AddSingleton<DiscordSocketClient>()


+ 21
- 16
samples/03_sharded_client/Program.cs View File

@@ -13,41 +13,46 @@ namespace _03_sharded_client
// DiscordSocketClient instances (or shards) to serve a large number of guilds. // DiscordSocketClient instances (or shards) to serve a large number of guilds.
class Program class Program
{ {
private DiscordShardedClient _client;

static void Main(string[] args) static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult(); => new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync() public async Task MainAsync()
{ {
// You specify the amount of shards you'd like to have with the // 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. // have 1 shard per 1500-2000 guilds your bot is in.
var config = new DiscordSocketConfig var config = new DiscordSocketConfig
{ {
TotalShards = 2 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() return new ServiceCollection()
.AddSingleton(_client)
.AddSingleton(new DiscordShardedClient(config))
.AddSingleton<CommandService>() .AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>() .AddSingleton<CommandHandlingService>()
.BuildServiceProvider(); .BuildServiceProvider();


+ 23
- 3
src/Discord.Net.Commands/CommandService.cs View File

@@ -27,7 +27,7 @@ namespace Discord.Commands
/// been successfully executed. /// been successfully executed.
/// </para> /// </para>
/// </remarks> /// </remarks>
public class CommandService
public class CommandService : IDisposable
{ {
/// <summary> /// <summary>
/// Occurs when a command-related information is received. /// Occurs when a command-related information is received.
@@ -67,6 +67,8 @@ namespace Discord.Commands
internal readonly LogManager _logManager; internal readonly LogManager _logManager;
internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap; internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap;


internal bool _isDisposed;

/// <summary> /// <summary>
/// Represents all modules loaded within <see cref="CommandService"/>. /// Represents all modules loaded within <see cref="CommandService"/>.
/// </summary> /// </summary>
@@ -330,9 +332,9 @@ namespace Discord.Commands


//Type Readers //Type Readers
/// <summary> /// <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. /// 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. /// also be added.
/// If a default <see cref="TypeReader" /> exists for <typeparamref name="T" />, a warning will be logged /// 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. /// and the default <see cref="TypeReader" /> will be replaced.
@@ -607,5 +609,23 @@ namespace Discord.Commands
await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result);
return result; return result;
} }

protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_moduleLock?.Dispose();
}

_isDisposed = true;
}
}

void IDisposable.Dispose()
{
Dispose(true);
}
} }
} }

+ 1
- 1
src/Discord.Net.Commands/Discord.Net.Commands.csproj View File

@@ -16,4 +16,4 @@
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' "> <ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' ">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" />
</ItemGroup> </ItemGroup>
</Project>
</Project>

+ 3
- 0
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -12,4 +12,7 @@
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> <PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
<PackageReference Include="System.Interactive.Async" Version="3.1.1" /> <PackageReference Include="System.Interactive.Async" Version="3.1.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Release' ">
<PackageReference Include="IDisposableAnalyzers" Version="2.0.3.3" />
</ItemGroup>
</Project> </Project>

+ 23
- 3
src/Discord.Net.Core/Entities/Image.cs View File

@@ -1,15 +1,21 @@
using System;
using System.IO; using System.IO;

namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// An image that will be uploaded to Discord. /// An image that will be uploaded to Discord.
/// </summary> /// </summary>
public struct Image
public struct Image : IDisposable
{ {
private bool _isDisposed;

/// <summary> /// <summary>
/// Gets the stream to be uploaded to Discord. /// Gets the stream to be uploaded to Discord.
/// </summary> /// </summary>
#pragma warning disable IDISP008
public Stream Stream { get; } public Stream Stream { get; }
#pragma warning restore IDISP008
/// <summary> /// <summary>
/// Create the image with a <see cref="System.IO.Stream"/>. /// Create the image with a <see cref="System.IO.Stream"/>.
/// </summary> /// </summary>
@@ -19,6 +25,7 @@ namespace Discord
/// </param> /// </param>
public Image(Stream stream) public Image(Stream stream)
{ {
_isDisposed = false;
Stream = stream; Stream = stream;
} }


@@ -45,15 +52,28 @@ namespace Discord
/// The specified <paramref name="path"/> is invalid, (for example, it is on an unmapped drive). /// The specified <paramref name="path"/> is invalid, (for example, it is on an unmapped drive).
/// </exception> /// </exception>
/// <exception cref="System.UnauthorizedAccessException"> /// <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>
/// <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>
/// <exception cref="IOException">An I/O error occurred while opening the file. </exception> /// <exception cref="IOException">An I/O error occurred while opening the file. </exception>
public Image(string path) public Image(string path)
{ {
_isDisposed = false;
Stream = File.OpenRead(path); Stream = File.OpenRead(path);
} }


/// <inheritdoc/>
public void Dispose()
{
if (!_isDisposed)
{
#pragma warning disable IDISP007
Stream?.Dispose();
#pragma warning restore IDISP007

_isDisposed = true;
}
}
} }
} }

+ 2
- 1
src/Discord.Net.Core/Net/Rest/IRestClient.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -7,7 +8,7 @@ namespace Discord.Net.Rest
/// <summary> /// <summary>
/// Represents a generic REST-based client. /// Represents a generic REST-based client.
/// </summary> /// </summary>
public interface IRestClient
public interface IRestClient : IDisposable
{ {
/// <summary> /// <summary>
/// Sets the HTTP header of this client for all requests. /// Sets the HTTP header of this client for all requests.


+ 1
- 1
src/Discord.Net.Core/Net/Udp/IUdpSocket.cs View File

@@ -4,7 +4,7 @@ using System.Threading.Tasks;


namespace Discord.Net.Udp namespace Discord.Net.Udp
{ {
public interface IUdpSocket
public interface IUdpSocket : IDisposable
{ {
event Func<byte[], int, int, Task> ReceivedDatagram; event Func<byte[], int, int, Task> ReceivedDatagram;




+ 1
- 1
src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs View File

@@ -4,7 +4,7 @@ using System.Threading.Tasks;


namespace Discord.Net.WebSockets namespace Discord.Net.WebSockets
{ {
public interface IWebSocketClient
public interface IWebSocketClient : IDisposable
{ {
event Func<byte[], int, int, Task> BinaryMessage; event Func<byte[], int, int, Task> BinaryMessage;
event Func<string, Task> TextMessage; event Func<string, Task> TextMessage;


+ 17
- 5
src/Discord.Net.Providers.WS4Net/WS4NetClient.cs View File

@@ -19,6 +19,7 @@ namespace Discord.Net.Providers.WS4Net
private readonly SemaphoreSlim _lock; private readonly SemaphoreSlim _lock;
private readonly Dictionary<string, string> _headers; private readonly Dictionary<string, string> _headers;
private WS4NetSocket _client; private WS4NetSocket _client;
private CancellationTokenSource _disconnectCancelTokenSource;
private CancellationTokenSource _cancelTokenSource; private CancellationTokenSource _cancelTokenSource;
private CancellationToken _cancelToken, _parentToken; private CancellationToken _cancelToken, _parentToken;
private ManualResetEventSlim _waitUntilConnect; private ManualResetEventSlim _waitUntilConnect;
@@ -28,7 +29,7 @@ namespace Discord.Net.Providers.WS4Net
{ {
_headers = new Dictionary<string, string>(); _headers = new Dictionary<string, string>();
_lock = new SemaphoreSlim(1, 1); _lock = new SemaphoreSlim(1, 1);
_cancelTokenSource = new CancellationTokenSource();
_disconnectCancelTokenSource = new CancellationTokenSource();
_cancelToken = CancellationToken.None; _cancelToken = CancellationToken.None;
_parentToken = CancellationToken.None; _parentToken = CancellationToken.None;
_waitUntilConnect = new ManualResetEventSlim(); _waitUntilConnect = new ManualResetEventSlim();
@@ -38,7 +39,11 @@ namespace Discord.Net.Providers.WS4Net
if (!_isDisposed) if (!_isDisposed)
{ {
if (disposing) if (disposing)
{
DisconnectInternalAsync(true).GetAwaiter().GetResult(); DisconnectInternalAsync(true).GetAwaiter().GetResult();
_lock?.Dispose();
_cancelTokenSource?.Dispose();
}
_isDisposed = true; _isDisposed = true;
} }
} }
@@ -63,8 +68,13 @@ namespace Discord.Net.Providers.WS4Net
{ {
await DisconnectInternalAsync().ConfigureAwait(false); 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()) _client = new WS4NetSocket(host, "", customHeaderItems: _headers.ToList())
{ {
@@ -96,7 +106,7 @@ namespace Discord.Net.Providers.WS4Net
} }
private Task DisconnectInternalAsync(bool isDisposing = false) private Task DisconnectInternalAsync(bool isDisposing = false)
{ {
_cancelTokenSource.Cancel();
_disconnectCancelTokenSource.Cancel();
if (_client == null) if (_client == null)
return Task.Delay(0); return Task.Delay(0);


@@ -125,8 +135,10 @@ namespace Discord.Net.Providers.WS4Net
} }
public void SetCancelToken(CancellationToken cancelToken) public void SetCancelToken(CancellationToken cancelToken)
{ {
_cancelTokenSource?.Dispose();
_parentToken = cancelToken; _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) public async Task SendAsync(byte[] data, int index, int count, bool isText)


+ 8
- 5
src/Discord.Net.Rest/BaseDiscordClient.cs View File

@@ -34,7 +34,7 @@ namespace Discord.Rest
public ISelfUser CurrentUser { get; protected set; } public ISelfUser CurrentUser { get; protected set; }
/// <inheritdoc /> /// <inheritdoc />
public TokenType TokenType => ApiClient.AuthTokenType; public TokenType TokenType => ApiClient.AuthTokenType;
/// <summary> Creates a new REST-only Discord client. </summary> /// <summary> Creates a new REST-only Discord client. </summary>
internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client)
{ {
@@ -106,9 +106,9 @@ namespace Discord.Rest


await _loggedInEvent.InvokeAsync().ConfigureAwait(false); await _loggedInEvent.InvokeAsync().ConfigureAwait(false);
} }
internal virtual Task OnLoginAsync(TokenType tokenType, string token)
internal virtual Task OnLoginAsync(TokenType tokenType, string token)
=> Task.Delay(0); => Task.Delay(0);
public async Task LogoutAsync() public async Task LogoutAsync()
{ {
await _stateLock.WaitAsync().ConfigureAwait(false); await _stateLock.WaitAsync().ConfigureAwait(false);
@@ -131,14 +131,17 @@ namespace Discord.Rest


await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); await _loggedOutEvent.InvokeAsync().ConfigureAwait(false);
} }
internal virtual Task OnLogoutAsync()
internal virtual Task OnLogoutAsync()
=> Task.Delay(0); => Task.Delay(0);


internal virtual void Dispose(bool disposing) internal virtual void Dispose(bool disposing)
{ {
if (!_isDisposed) if (!_isDisposed)
{ {
#pragma warning disable IDISP007
ApiClient.Dispose(); ApiClient.Dispose();
#pragma warning restore IDISP007
_stateLock?.Dispose();
_isDisposed = true; _isDisposed = true;
} }
} }
@@ -156,7 +159,7 @@ namespace Discord.Rest
ISelfUser IDiscordClient.CurrentUser => CurrentUser; ISelfUser IDiscordClient.CurrentUser => CurrentUser;


/// <inheritdoc /> /// <inheritdoc />
Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options)
=> throw new NotSupportedException(); => throw new NotSupportedException();


/// <inheritdoc /> /// <inheritdoc />


+ 6
- 2
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -66,6 +66,7 @@ namespace Discord.API
/// <exception cref="ArgumentException">Unknown OAuth token type.</exception> /// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
internal void SetBaseUrl(string baseUrl) internal void SetBaseUrl(string baseUrl)
{ {
RestClient?.Dispose();
RestClient = _restClientProvider(baseUrl); RestClient = _restClientProvider(baseUrl);
RestClient.SetHeader("accept", "*/*"); RestClient.SetHeader("accept", "*/*");
RestClient.SetHeader("user-agent", UserAgent); RestClient.SetHeader("user-agent", UserAgent);
@@ -93,7 +94,9 @@ namespace Discord.API
if (disposing) if (disposing)
{ {
_loginCancelToken?.Dispose(); _loginCancelToken?.Dispose();
(RestClient as IDisposable)?.Dispose();
RestClient?.Dispose();
RequestQueue?.Dispose();
_stateLock?.Dispose();
} }
_isDisposed = true; _isDisposed = true;
} }
@@ -117,6 +120,7 @@ namespace Discord.API


try try
{ {
_loginCancelToken?.Dispose();
_loginCancelToken = new CancellationTokenSource(); _loginCancelToken = new CancellationTokenSource();


AuthToken = null; 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, 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) ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
=> SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); => 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) string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
{ {
options = options ?? new RequestOptions(); options = options ?? new RequestOptions();


+ 7
- 5
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -31,6 +31,8 @@ namespace Discord.Rest
{ {
if (disposing) if (disposing)
ApiClient.Dispose(); ApiClient.Dispose();

base.Dispose(disposing);
} }


/// <inheritdoc /> /// <inheritdoc />
@@ -46,12 +48,12 @@ namespace Discord.Rest
_applicationInfo = null; _applicationInfo = null;
return Task.Delay(0); return Task.Delay(0);
} }
public async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null) public async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null)
{ {
return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false));
} }
public Task<RestChannel> GetChannelAsync(ulong id, RequestOptions options = null) public Task<RestChannel> GetChannelAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetChannelAsync(this, id, options); => ClientHelper.GetChannelAsync(this, id, options);
public Task<IReadOnlyCollection<IRestPrivateChannel>> GetPrivateChannelsAsync(RequestOptions options = null) public Task<IReadOnlyCollection<IRestPrivateChannel>> GetPrivateChannelsAsync(RequestOptions options = null)
@@ -60,7 +62,7 @@ namespace Discord.Rest
=> ClientHelper.GetDMChannelsAsync(this, options); => ClientHelper.GetDMChannelsAsync(this, options);
public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null) public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null)
=> ClientHelper.GetGroupChannelsAsync(this, options); => ClientHelper.GetGroupChannelsAsync(this, options);
public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null) public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null)
=> ClientHelper.GetConnectionsAsync(this, options); => ClientHelper.GetConnectionsAsync(this, options);


@@ -79,12 +81,12 @@ namespace Discord.Rest
=> ClientHelper.GetGuildsAsync(this, options); => ClientHelper.GetGuildsAsync(this, options);
public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null)
=> ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options);
public Task<RestUser> GetUserAsync(ulong id, RequestOptions options = null) public Task<RestUser> GetUserAsync(ulong id, RequestOptions options = null)
=> ClientHelper.GetUserAsync(this, id, options); => ClientHelper.GetUserAsync(this, id, options);
public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null)
=> ClientHelper.GetGuildUserAsync(this, guildId, id, options); => ClientHelper.GetGuildUserAsync(this, guildId, id, options);
public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null) public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null)
=> ClientHelper.GetVoiceRegionsAsync(this, options); => ClientHelper.GetVoiceRegionsAsync(this, options);
public Task<RestVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null) public Task<RestVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null)


+ 8
- 6
src/Discord.Net.Rest/Net/Converters/ImageConverter.cs View File

@@ -34,12 +34,14 @@ namespace Discord.Net.Converters
} }
else 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); string base64 = Convert.ToBase64String(bytes, 0, length);


+ 20
- 8
src/Discord.Net.Rest/Net/DefaultRestClient.cs View File

@@ -27,12 +27,14 @@ namespace Discord.Net.Rest
{ {
_baseUrl = baseUrl; _baseUrl = baseUrl;


#pragma warning disable IDISP014
_client = new HttpClient(new HttpClientHandler _client = new HttpClient(new HttpClientHandler
{ {
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false, UseCookies = false,
UseProxy = useProxy, UseProxy = useProxy,
}); });
#pragma warning restore IDISP014
SetHeader("accept-encoding", "gzip, deflate"); SetHeader("accept-encoding", "gzip, deflate");


_cancelToken = CancellationToken.None; _cancelToken = CancellationToken.None;
@@ -91,12 +93,14 @@ namespace Discord.Net.Rest
{ {
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
MemoryStream memoryStream = null;
if (multipartParams != null) if (multipartParams != null)
{ {
foreach (var p in multipartParams) foreach (var p in multipartParams)
{ {
switch (p.Value) switch (p.Value)
{ {
#pragma warning disable IDISP004
case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; }
case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), 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; } case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; }
@@ -105,12 +109,15 @@ namespace Discord.Net.Rest
var stream = fileValue.Stream; var stream = fileValue.Stream;
if (!stream.CanSeek) if (!stream.CanSeek)
{ {
var memoryStream = new MemoryStream();
memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false); await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0; memoryStream.Position = 0;
#pragma warning disable IDISP001
stream = memoryStream; stream = memoryStream;
#pragma warning restore IDISP001
} }
content.Add(new StreamContent(stream), p.Key, fileValue.Filename); content.Add(new StreamContent(stream), p.Key, fileValue.Filename);
#pragma warning restore IDISP004
continue; continue;
} }
default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\".");
@@ -118,19 +125,24 @@ namespace Discord.Net.Rest
} }
} }
restRequest.Content = content; 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) 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"); private static readonly HttpMethod Patch = new HttpMethod("PATCH");


+ 28
- 11
src/Discord.Net.Rest/Net/Queue/RequestQueue.cs View File

@@ -16,23 +16,24 @@ namespace Discord.Net.Queue


private readonly ConcurrentDictionary<string, RequestBucket> _buckets; private readonly ConcurrentDictionary<string, RequestBucket> _buckets;
private readonly SemaphoreSlim _tokenLock; private readonly SemaphoreSlim _tokenLock;
private readonly CancellationTokenSource _cancelToken; //Dispose token
private readonly CancellationTokenSource _cancelTokenSource; //Dispose token
private CancellationTokenSource _clearToken; private CancellationTokenSource _clearToken;
private CancellationToken _parentToken; private CancellationToken _parentToken;
private CancellationTokenSource _requestCancelTokenSource;
private CancellationToken _requestCancelToken; //Parent token + Clear token private CancellationToken _requestCancelToken; //Parent token + Clear token
private DateTimeOffset _waitUntil; private DateTimeOffset _waitUntil;


private Task _cleanupTask; private Task _cleanupTask;
public RequestQueue() public RequestQueue()
{ {
_tokenLock = new SemaphoreSlim(1, 1); _tokenLock = new SemaphoreSlim(1, 1);


_clearToken = new CancellationTokenSource(); _clearToken = new CancellationTokenSource();
_cancelToken = new CancellationTokenSource();
_cancelTokenSource = new CancellationTokenSource();
_requestCancelToken = CancellationToken.None; _requestCancelToken = CancellationToken.None;
_parentToken = CancellationToken.None; _parentToken = CancellationToken.None;
_buckets = new ConcurrentDictionary<string, RequestBucket>(); _buckets = new ConcurrentDictionary<string, RequestBucket>();


_cleanupTask = RunCleanup(); _cleanupTask = RunCleanup();
@@ -44,7 +45,9 @@ namespace Discord.Net.Queue
try try
{ {
_parentToken = cancelToken; _parentToken = cancelToken;
_requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token;
_requestCancelTokenSource?.Dispose();
_requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token);
_requestCancelToken = _requestCancelTokenSource.Token;
} }
finally { _tokenLock.Release(); } finally { _tokenLock.Release(); }
} }
@@ -54,9 +57,14 @@ namespace Discord.Net.Queue
try try
{ {
_clearToken?.Cancel(); _clearToken?.Cancel();
_clearToken?.Dispose();
_clearToken = new CancellationTokenSource(); _clearToken = new CancellationTokenSource();
if (_parentToken != null) if (_parentToken != null)
_requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token;
{
_requestCancelTokenSource?.Dispose();
_requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken);
_requestCancelToken = _requestCancelTokenSource.Token;
}
else else
_requestCancelToken = _clearToken.Token; _requestCancelToken = _clearToken.Token;
} }
@@ -65,13 +73,19 @@ namespace Discord.Net.Queue


public async Task<Stream> SendAsync(RestRequest request) public async Task<Stream> SendAsync(RestRequest request)
{ {
CancellationTokenSource createdTokenSource = null;
if (request.Options.CancelToken.CanBeCanceled) 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 else
request.Options.CancelToken = _requestCancelToken; request.Options.CancelToken = _requestCancelToken;


var bucket = GetOrCreateBucket(request.Options.BucketId, request); 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) public async Task SendAsync(WebSocketRequest request)
{ {
@@ -109,7 +123,7 @@ namespace Discord.Net.Queue
{ {
try try
{ {
while (!_cancelToken.IsCancellationRequested)
while (!_cancelTokenSource.IsCancellationRequested)
{ {
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
foreach (var bucket in _buckets.Select(x => x.Value)) foreach (var bucket in _buckets.Select(x => x.Value))
@@ -117,7 +131,7 @@ namespace Discord.Net.Queue
if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0)
_buckets.TryRemove(bucket.Id, out _); _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) { } catch (OperationCanceledException) { }
@@ -126,7 +140,10 @@ namespace Discord.Net.Queue


public void Dispose() public void Dispose()
{ {
_cancelToken.Dispose();
_cancelTokenSource?.Dispose();
_tokenLock?.Dispose();
_clearToken?.Dispose();
_requestCancelTokenSource?.Dispose();
} }
} }
} }

+ 13
- 12
src/Discord.Net.WebSocket/Audio/AudioClient.cs View File

@@ -71,7 +71,7 @@ namespace Discord.Audio
ApiClient.ReceivedPacket += ProcessPacketAsync; ApiClient.ReceivedPacket += ProcessPacketAsync;


_stateLock = new SemaphoreSlim(1, 1); _stateLock = new SemaphoreSlim(1, 1);
_connection = new ConnectionManager(_stateLock, _audioLogger, 30000,
_connection = new ConnectionManager(_stateLock, _audioLogger, 30000,
OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x);
_connection.Connected += () => _connectedEvent.InvokeAsync(); _connection.Connected += () => _connectedEvent.InvokeAsync();
_connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex);
@@ -79,7 +79,7 @@ namespace Discord.Audio
_keepaliveTimes = new ConcurrentQueue<KeyValuePair<ulong, int>>(); _keepaliveTimes = new ConcurrentQueue<KeyValuePair<ulong, int>>();
_ssrcMap = new ConcurrentDictionary<uint, ulong>(); _ssrcMap = new ConcurrentDictionary<uint, ulong>();
_streams = new ConcurrentDictionary<ulong, StreamPair>(); _streams = new ConcurrentDictionary<ulong, StreamPair>();
_serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() };
_serializer.Error += (s, e) => _serializer.Error += (s, e) =>
{ {
@@ -91,7 +91,7 @@ namespace Discord.Audio
UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); 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; _url = url;
_userId = userId; _userId = userId;
@@ -100,7 +100,7 @@ namespace Discord.Audio
await _connection.StartAsync().ConfigureAwait(false); await _connection.StartAsync().ConfigureAwait(false);
} }
public async Task StopAsync() public async Task StopAsync()
{
{
await _connection.StopAsync().ConfigureAwait(false); await _connection.StopAsync().ConfigureAwait(false);
} }


@@ -225,11 +225,11 @@ namespace Discord.Audio


if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode))
throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}");
ApiClient.SetUdpEndpoint(data.Ip, data.Port); ApiClient.SetUdpEndpoint(data.Ip, data.Port);
await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false);


_heartbeatTask = RunHeartbeatAsync(41250, _connection.CancelToken); _heartbeatTask = RunHeartbeatAsync(41250, _connection.CancelToken);
} }
break; break;
@@ -305,9 +305,9 @@ namespace Discord.Audio
catch (Exception ex) catch (Exception ex)
{ {
await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false); await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false);
return;
return;
} }
await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false);
await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false);
} }
@@ -317,7 +317,7 @@ namespace Discord.Audio
{ {
await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false);


ulong value =
ulong value =
((ulong)packet[0] >> 0) | ((ulong)packet[0] >> 0) |
((ulong)packet[1] >> 8) | ((ulong)packet[1] >> 8) |
((ulong)packet[2] >> 16) | ((ulong)packet[2] >> 16) |
@@ -341,7 +341,7 @@ namespace Discord.Audio
} }
} }
else else
{
{
if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc))
{ {
await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false); await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false);
@@ -388,7 +388,7 @@ namespace Discord.Audio
var now = Environment.TickCount; var now = Environment.TickCount;


//Did server respond to our last heartbeat? //Did server respond to our last heartbeat?
if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis &&
if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis &&
ConnectionState == ConnectionState.Connected) ConnectionState == ConnectionState.Connected)
{ {
_connection.Error(new Exception("Server missed last heartbeat")); _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 _audioLogger.WarningAsync("Failed to send keepalive", ex).ConfigureAwait(false);
} }
await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false);
} }
await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false);
@@ -467,6 +467,7 @@ namespace Discord.Audio
{ {
StopAsync().GetAwaiter().GetResult(); StopAsync().GetAwaiter().GetResult();
ApiClient.Dispose(); ApiClient.Dispose();
_stateLock?.Dispose();
} }
} }
/// <inheritdoc /> /// <inheritdoc />


+ 20
- 6
src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs View File

@@ -27,7 +27,7 @@ namespace Discord.Audio.Streams


private readonly AudioClient _client; private readonly AudioClient _client;
private readonly AudioStream _next; private readonly AudioStream _next;
private readonly CancellationTokenSource _cancelTokenSource;
private readonly CancellationTokenSource _disposeTokenSource, _cancelTokenSource;
private readonly CancellationToken _cancelToken; private readonly CancellationToken _cancelToken;
private readonly Task _task; private readonly Task _task;
private readonly ConcurrentQueue<Frame> _queuedFrames; private readonly ConcurrentQueue<Frame> _queuedFrames;
@@ -49,12 +49,13 @@ namespace Discord.Audio.Streams
_logger = logger; _logger = logger;
_queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up _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>(); _queuedFrames = new ConcurrentQueue<Frame>();
_bufferPool = new ConcurrentQueue<byte[]>(); _bufferPool = new ConcurrentQueue<byte[]>();
for (int i = 0; i < _queueLength; i++) for (int i = 0; i < _queueLength; i++)
_bufferPool.Enqueue(new byte[maxFrameSize]);
_bufferPool.Enqueue(new byte[maxFrameSize]);
_queueLock = new SemaphoreSlim(_queueLength, _queueLength); _queueLock = new SemaphoreSlim(_queueLength, _queueLength);
_silenceFrames = MaxSilenceFrames; _silenceFrames = MaxSilenceFrames;


@@ -63,7 +64,12 @@ namespace Discord.Audio.Streams
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
_cancelTokenSource.Cancel();
{
_disposeTokenSource?.Cancel();
_disposeTokenSource?.Dispose();
_cancelTokenSource?.Dispose();
_queueLock?.Dispose();
}
base.Dispose(disposing); 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 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) public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken)
{ {
CancellationTokenSource writeCancelToken = null;
if (cancelToken.CanBeCanceled) if (cancelToken.CanBeCanceled)
cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token;
{
writeCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken);
cancelToken = writeCancelToken.Token;
}
else else
cancelToken = _cancelToken; cancelToken = _cancelToken;


@@ -142,6 +152,9 @@ namespace Discord.Audio.Streams
#if DEBUG #if DEBUG
var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock
#endif #endif
#pragma warning disable IDISP016
writeCancelToken?.Dispose();
#pragma warning restore IDISP016
return; return;
} }
Buffer.BlockCopy(data, offset, buffer, 0, count); Buffer.BlockCopy(data, offset, buffer, 0, count);
@@ -153,6 +166,7 @@ namespace Discord.Audio.Streams
#endif #endif
_isPreloaded = true; _isPreloaded = true;
} }
writeCancelToken?.Dispose();
} }


public override async Task FlushAsync(CancellationToken cancelToken) public override async Task FlushAsync(CancellationToken cancelToken)


+ 11
- 1
src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs View File

@@ -96,7 +96,17 @@ namespace Discord.Audio.Streams


protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
_isDisposed = true;
if (!_isDisposed)
{
if (isDisposing)
{
_signal?.Dispose();
}

_isDisposed = true;
}

base.Dispose(isDisposing);
} }
} }
} }

+ 34
- 9
src/Discord.Net.WebSocket/ConnectionManager.cs View File

@@ -6,7 +6,7 @@ using Discord.Net;


namespace Discord namespace Discord
{ {
internal class ConnectionManager
internal class ConnectionManager : IDisposable
{ {
public event Func<Task> Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } public event Func<Task> Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } }
private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>();
@@ -23,10 +23,12 @@ namespace Discord
private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken; private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken;
private Task _task; private Task _task;


private bool _isDisposed;

public ConnectionState State { get; private set; } public ConnectionState State { get; private set; }
public CancellationToken CancelToken { 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) Func<Task> onConnecting, Func<Exception, Task> onDisconnecting, Action<Func<Exception, Task>> clientDisconnectHandler)
{ {
_stateLock = stateLock; _stateLock = stateLock;
@@ -55,6 +57,7 @@ namespace Discord
{ {
await AcquireConnectionLock().ConfigureAwait(false); await AcquireConnectionLock().ConfigureAwait(false);
var reconnectCancelToken = new CancellationTokenSource(); var reconnectCancelToken = new CancellationTokenSource();
_reconnectCancelToken?.Dispose();
_reconnectCancelToken = reconnectCancelToken; _reconnectCancelToken = reconnectCancelToken;
_task = Task.Run(async () => _task = Task.Run(async () =>
{ {
@@ -67,16 +70,16 @@ namespace Discord
try try
{ {
await ConnectAsync(reconnectCancelToken).ConfigureAwait(false); await ConnectAsync(reconnectCancelToken).ConfigureAwait(false);
nextReconnectDelay = 1000; //Reset delay
nextReconnectDelay = 1000; //Reset delay
await _connectionPromise.Task.ConfigureAwait(false); await _connectionPromise.Task.ConfigureAwait(false);
} }
catch (OperationCanceledException ex)
{
catch (OperationCanceledException ex)
{
Cancel(); //In case this exception didn't come from another Error call Cancel(); //In case this exception didn't come from another Error call
await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false); 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 Error(ex); //In case this exception didn't come from another Error call
if (!reconnectCancelToken.IsCancellationRequested) if (!reconnectCancelToken.IsCancellationRequested)
{ {
@@ -113,6 +116,8 @@ namespace Discord


private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken)
{ {
_connectionCancelToken?.Dispose();
_combinedCancelToken?.Dispose();
_connectionCancelToken = new CancellationTokenSource(); _connectionCancelToken = new CancellationTokenSource();
_combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token); _combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token);
CancelToken = _combinedCancelToken.Token; CancelToken = _combinedCancelToken.Token;
@@ -120,7 +125,7 @@ namespace Discord
_connectionPromise = new TaskCompletionSource<bool>(); _connectionPromise = new TaskCompletionSource<bool>();
State = ConnectionState.Connecting; State = ConnectionState.Connecting;
await _logger.InfoAsync("Connecting").ConfigureAwait(false); await _logger.InfoAsync("Connecting").ConfigureAwait(false);
try try
{ {
var readyPromise = new TaskCompletionSource<bool>(); var readyPromise = new TaskCompletionSource<bool>();
@@ -206,5 +211,25 @@ namespace Discord
break; break;
} }
} }

protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_combinedCancelToken?.Dispose();
_reconnectCancelToken?.Dispose();
_connectionCancelToken?.Dispose();
}

_isDisposed = true;
}
}

public void Dispose()
{
Dispose(true);
}
} }
}
}

+ 29
- 6
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -18,7 +18,9 @@ namespace Discord.WebSocket
private int[] _shardIds; private int[] _shardIds;
private DiscordSocketClient[] _shards; private DiscordSocketClient[] _shards;
private int _totalShards; private int _totalShards;

private bool _isDisposed;

/// <inheritdoc /> /// <inheritdoc />
public override int Latency { get => GetLatency(); protected set { } } public override int Latency { get => GetLatency(); protected set { } }
/// <inheritdoc /> /// <inheritdoc />
@@ -38,11 +40,15 @@ namespace Discord.WebSocket
/// <summary> Creates a new REST/WebSocket Discord client. </summary> /// <summary> Creates a new REST/WebSocket Discord client. </summary>
public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { }
/// <summary> Creates a new REST/WebSocket Discord client. </summary> /// <summary> Creates a new REST/WebSocket Discord client. </summary>
#pragma warning disable IDISP004
public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { }
#pragma warning restore IDISP004
/// <summary> Creates a new REST/WebSocket Discord client. </summary> /// <summary> Creates a new REST/WebSocket Discord client. </summary>
public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { }
/// <summary> Creates a new REST/WebSocket Discord client. </summary> /// <summary> Creates a new REST/WebSocket Discord client. </summary>
#pragma warning disable IDISP004
public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { }
#pragma warning restore IDISP004
private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client)
: base(config, client) : base(config, client)
{ {
@@ -119,10 +125,10 @@ namespace Discord.WebSocket
} }


/// <inheritdoc /> /// <inheritdoc />
public override async Task StartAsync()
public override async Task StartAsync()
=> await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
public override async Task StopAsync()
public override async Task StopAsync()
=> await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); => await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false);


public DiscordSocketClient GetShard(int id) public DiscordSocketClient GetShard(int id)
@@ -145,7 +151,7 @@ namespace Discord.WebSocket
=> await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false); => await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false);


/// <inheritdoc /> /// <inheritdoc />
public override SocketGuild GetGuild(ulong id)
public override SocketGuild GetGuild(ulong id)
=> GetShardFor(id).GetGuild(id); => GetShardFor(id).GetGuild(id);


/// <inheritdoc /> /// <inheritdoc />
@@ -173,7 +179,7 @@ namespace Discord.WebSocket
for (int i = 0; i < _shards.Length; i++) for (int i = 0; i < _shards.Length; i++)
result += _shards[i].PrivateChannels.Count; result += _shards[i].PrivateChannels.Count;
return result; return result;
}
}


private IEnumerable<SocketGuild> GetGuilds() private IEnumerable<SocketGuild> GetGuilds()
{ {
@@ -189,7 +195,7 @@ namespace Discord.WebSocket
for (int i = 0; i < _shards.Length; i++) for (int i = 0; i < _shards.Length; i++)
result += _shards[i].Guilds.Count; result += _shards[i].Guilds.Count;
return result; return result;
}
}


/// <inheritdoc /> /// <inheritdoc />
public override SocketUser GetUser(ulong id) public override SocketUser GetUser(ulong id)
@@ -369,5 +375,22 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options)
=> Task.FromResult<IVoiceRegion>(GetVoiceRegion(id)); => 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);
}
} }
} }

+ 4
- 1
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -108,6 +108,8 @@ namespace Discord.API
} }
_isDisposed = true; _isDisposed = true;
} }

base.Dispose(disposing);
} }


public async Task ConnectAsync() public async Task ConnectAsync()
@@ -137,6 +139,7 @@ namespace Discord.API
ConnectionState = ConnectionState.Connecting; ConnectionState = ConnectionState.Connecting;
try try
{ {
_connectCancelToken?.Dispose();
_connectCancelToken = new CancellationTokenSource(); _connectCancelToken = new CancellationTokenSource();
if (WebSocketClient != null) if (WebSocketClient != null)
WebSocketClient.SetCancelToken(_connectCancelToken.Token); WebSocketClient.SetCancelToken(_connectCancelToken.Token);
@@ -209,7 +212,7 @@ namespace Discord.API
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
} }
public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null)
{ {
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);


+ 18
- 6
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -43,6 +43,8 @@ namespace Discord.WebSocket
private DateTimeOffset? _statusSince; private DateTimeOffset? _statusSince;
private RestApplication _applicationInfo; private RestApplication _applicationInfo;


private bool _isDisposed;

/// <summary> Gets the shard of of this client. </summary> /// <summary> Gets the shard of of this client. </summary>
public int ShardId { get; } public int ShardId { get; }
/// <summary> Gets the current connection state of this client. </summary> /// <summary> Gets the current connection state of this client. </summary>
@@ -63,7 +65,7 @@ namespace Discord.WebSocket
internal WebSocketProvider WebSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; }
internal bool AlwaysDownloadUsers { get; private set; } internal bool AlwaysDownloadUsers { get; private set; }
internal int? HandlerTimeout { get; private set; } internal int? HandlerTimeout { get; private set; }
internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient;
/// <inheritdoc /> /// <inheritdoc />
public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; 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. /// Initializes a new REST/WebSocket-based Discord client with the provided configuration.
/// </summary> /// </summary>
/// <param name="config">The configuration to be used with the client.</param> /// <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) { } public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { }
internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } 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) private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient)
: base(config, client) : base(config, client)
{ {
@@ -170,11 +174,18 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
internal override void Dispose(bool disposing) 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 /> /// <inheritdoc />
@@ -197,10 +208,10 @@ namespace Discord.WebSocket
} }


/// <inheritdoc /> /// <inheritdoc />
public override async Task StartAsync()
public override async Task StartAsync()
=> await _connection.StartAsync().ConfigureAwait(false); => await _connection.StartAsync().ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
public override async Task StopAsync()
public override async Task StopAsync()
=> await _connection.StopAsync().ConfigureAwait(false); => await _connection.StopAsync().ConfigureAwait(false);


private async Task OnConnectingAsync() private async Task OnConnectingAsync()
@@ -704,6 +715,7 @@ namespace Discord.WebSocket
{ {
await GuildUnavailableAsync(guild).ConfigureAwait(false); await GuildUnavailableAsync(guild).ConfigureAwait(false);
await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false);
(guild as IDisposable).Dispose();
} }
else else
{ {


+ 8
- 6
src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs View File

@@ -16,7 +16,7 @@ using System.Threading.Tasks;


namespace Discord.Audio namespace Discord.Audio
{ {
internal class DiscordVoiceAPIClient
internal class DiscordVoiceAPIClient : IDisposable
{ {
public const int MaxBitrate = 128 * 1024; public const int MaxBitrate = 128 * 1024;
public const string Mode = "xsalsa20_poly1305"; 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>>(); 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); } } 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 AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>();
private readonly JsonSerializer _serializer; private readonly JsonSerializer _serializer;
private readonly SemaphoreSlim _connectionLock; private readonly SemaphoreSlim _connectionLock;
private readonly IUdpSocket _udp; private readonly IUdpSocket _udp;
@@ -103,8 +103,9 @@ namespace Discord.Audio
if (disposing) if (disposing)
{ {
_connectCancelToken?.Dispose(); _connectCancelToken?.Dispose();
(_udp as IDisposable)?.Dispose();
(WebSocketClient as IDisposable)?.Dispose();
_udp?.Dispose();
WebSocketClient?.Dispose();
_connectionLock?.Dispose();
} }
_isDisposed = true; _isDisposed = true;
} }
@@ -122,7 +123,7 @@ namespace Discord.Audio
} }
public async Task SendAsync(byte[] data, int offset, int bytes) 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); await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false);
} }


@@ -177,6 +178,7 @@ namespace Discord.Audio
ConnectionState = ConnectionState.Connecting; ConnectionState = ConnectionState.Connecting;
try try
{ {
_connectCancelToken?.Dispose();
_connectCancelToken = new CancellationTokenSource(); _connectCancelToken = new CancellationTokenSource();
var cancelToken = _connectCancelToken.Token; var cancelToken = _connectCancelToken.Token;


@@ -208,7 +210,7 @@ namespace Discord.Audio
{ {
if (ConnectionState == ConnectionState.Disconnected) return; if (ConnectionState == ConnectionState.Disconnected) return;
ConnectionState = ConnectionState.Disconnecting; ConnectionState = ConnectionState.Disconnecting;
try { _connectCancelToken?.Cancel(false); } try { _connectCancelToken?.Cancel(false); }
catch { } catch { }




+ 18
- 2
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -25,8 +25,9 @@ namespace Discord.WebSocket
/// Represents a WebSocket-based guild object. /// Represents a WebSocket-based guild object.
/// </summary> /// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [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 readonly SemaphoreSlim _audioLock;
private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; private TaskCompletionSource<bool> _syncPromise, _downloaderPromise;
private TaskCompletionSource<AudioClient> _audioConnectPromise; private TaskCompletionSource<AudioClient> _audioConnectPromise;
@@ -37,6 +38,7 @@ namespace Discord.WebSocket
private ImmutableArray<GuildEmote> _emotes; private ImmutableArray<GuildEmote> _emotes;
private ImmutableArray<string> _features; private ImmutableArray<string> _features;
private AudioClient _audioClient; private AudioClient _audioClient;
#pragma warning restore IDISP002, IDISP006


/// <inheritdoc /> /// <inheritdoc />
public string Name { get; private set; } 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. /// number here is the most accurate in terms of counting the number of users within this guild.
/// </para> /// </para>
/// <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 /// <see cref="Discord.WebSocket.SocketGuild.Users" /> collection, as you may see discrepancy
/// between that and this property. /// between that and this property.
/// </para> /// </para>
@@ -872,9 +874,11 @@ namespace Discord.WebSocket


if (external) if (external)
{ {
#pragma warning disable IDISP001
var _ = promise.TrySetResultAsync(null); var _ = promise.TrySetResultAsync(null);
await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false);
return null; return null;
#pragma warning restore IDISP001
} }


if (_audioClient == null) if (_audioClient == null)
@@ -897,10 +901,14 @@ namespace Discord.WebSocket
}; };
audioClient.Connected += () => audioClient.Connected += () =>
{ {
#pragma warning disable IDISP001
var _ = promise.TrySetResultAsync(_audioClient); var _ = promise.TrySetResultAsync(_audioClient);
#pragma warning restore IDISP001
return Task.Delay(0); return Task.Delay(0);
}; };
#pragma warning disable IDISP003
_audioClient = audioClient; _audioClient = audioClient;
#pragma warning restore IDISP003
} }


await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false);
@@ -948,6 +956,7 @@ namespace Discord.WebSocket
if (_audioClient != null) if (_audioClient != null)
await _audioClient.StopAsync().ConfigureAwait(false); await _audioClient.StopAsync().ConfigureAwait(false);
await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false);
_audioClient?.Dispose();
_audioClient = null; _audioClient = null;
} }
internal async Task FinishConnectAudio(string url, string token) internal async Task FinishConnectAudio(string url, string token)
@@ -1130,5 +1139,12 @@ namespace Discord.WebSocket
/// <inheritdoc /> /// <inheritdoc />
async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options)
=> await GetWebhooksAsync(options).ConfigureAwait(false); => await GetWebhooksAsync(options).ConfigureAwait(false);

void IDisposable.Dispose()
{
DisconnectAudioAsync().GetAwaiter().GetResult();
_audioLock?.Dispose();
_audioClient?.Dispose();
}
} }
} }

+ 20
- 7
src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs View File

@@ -13,24 +13,29 @@ namespace Discord.Net.Udp
private readonly SemaphoreSlim _lock; private readonly SemaphoreSlim _lock;
private UdpClient _udp; private UdpClient _udp;
private IPEndPoint _destination; private IPEndPoint _destination;
private CancellationTokenSource _cancelTokenSource;
private CancellationTokenSource _stopCancelTokenSource, _cancelTokenSource;
private CancellationToken _cancelToken, _parentToken; private CancellationToken _cancelToken, _parentToken;
private Task _task; private Task _task;
private bool _isDisposed; private bool _isDisposed;
public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0);


public DefaultUdpSocket() public DefaultUdpSocket()
{ {
_lock = new SemaphoreSlim(1, 1); _lock = new SemaphoreSlim(1, 1);
_cancelTokenSource = new CancellationTokenSource();
_stopCancelTokenSource = new CancellationTokenSource();
} }
private void Dispose(bool disposing) private void Dispose(bool disposing)
{ {
if (!_isDisposed) if (!_isDisposed)
{ {
if (disposing) if (disposing)
{
StopInternalAsync(true).GetAwaiter().GetResult(); StopInternalAsync(true).GetAwaiter().GetResult();
_stopCancelTokenSource?.Dispose();
_cancelTokenSource?.Dispose();
_lock?.Dispose();
}
_isDisposed = true; _isDisposed = true;
} }
} }
@@ -56,9 +61,14 @@ namespace Discord.Net.Udp
{ {
await StopInternalAsync().ConfigureAwait(false); 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); _udp = new UdpClient(0);


_task = RunAsync(_cancelToken); _task = RunAsync(_cancelToken);
@@ -77,7 +87,7 @@ namespace Discord.Net.Udp
} }
public async Task StopInternalAsync(bool isDisposing = false) public async Task StopInternalAsync(bool isDisposing = false)
{ {
try { _cancelTokenSource.Cancel(false); } catch { }
try { _stopCancelTokenSource.Cancel(false); } catch { }


if (!isDisposing) if (!isDisposing)
await (_task ?? Task.Delay(0)).ConfigureAwait(false); await (_task ?? Task.Delay(0)).ConfigureAwait(false);
@@ -96,8 +106,11 @@ namespace Discord.Net.Udp
} }
public void SetCancelToken(CancellationToken cancelToken) public void SetCancelToken(CancellationToken cancelToken)
{ {
_cancelTokenSource?.Dispose();

_parentToken = cancelToken; _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) public async Task SendAsync(byte[] data, int index, int count)


+ 24
- 11
src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs View File

@@ -25,14 +25,14 @@ namespace Discord.Net.WebSockets
private readonly IWebProxy _proxy; private readonly IWebProxy _proxy;
private ClientWebSocket _client; private ClientWebSocket _client;
private Task _task; private Task _task;
private CancellationTokenSource _cancelTokenSource;
private CancellationTokenSource _disconnectTokenSource, _cancelTokenSource;
private CancellationToken _cancelToken, _parentToken; private CancellationToken _cancelToken, _parentToken;
private bool _isDisposed, _isDisconnecting; private bool _isDisposed, _isDisconnecting;


public DefaultWebSocketClient(IWebProxy proxy = null) public DefaultWebSocketClient(IWebProxy proxy = null)
{ {
_lock = new SemaphoreSlim(1, 1); _lock = new SemaphoreSlim(1, 1);
_cancelTokenSource = new CancellationTokenSource();
_disconnectTokenSource = new CancellationTokenSource();
_cancelToken = CancellationToken.None; _cancelToken = CancellationToken.None;
_parentToken = CancellationToken.None; _parentToken = CancellationToken.None;
_headers = new Dictionary<string, string>(); _headers = new Dictionary<string, string>();
@@ -43,7 +43,12 @@ namespace Discord.Net.WebSockets
if (!_isDisposed) if (!_isDisposed)
{ {
if (disposing) if (disposing)
{
DisconnectInternalAsync(true).GetAwaiter().GetResult(); DisconnectInternalAsync(true).GetAwaiter().GetResult();
_disconnectTokenSource?.Dispose();
_cancelTokenSource?.Dispose();
_lock?.Dispose();
}
_isDisposed = true; _isDisposed = true;
} }
} }
@@ -68,9 +73,14 @@ namespace Discord.Net.WebSockets
{ {
await DisconnectInternalAsync().ConfigureAwait(false); 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 = new ClientWebSocket();
_client.Options.Proxy = _proxy; _client.Options.Proxy = _proxy;
_client.Options.KeepAliveInterval = TimeSpan.Zero; _client.Options.KeepAliveInterval = TimeSpan.Zero;
@@ -98,7 +108,7 @@ namespace Discord.Net.WebSockets
} }
private async Task DisconnectInternalAsync(bool isDisposing = false) private async Task DisconnectInternalAsync(bool isDisposing = false)
{ {
try { _cancelTokenSource.Cancel(false); } catch { }
try { _disconnectTokenSource.Cancel(false); } catch { }


_isDisconnecting = true; _isDisconnecting = true;
try try
@@ -117,7 +127,7 @@ namespace Discord.Net.WebSockets
} }
try { _client.Dispose(); } try { _client.Dispose(); }
catch { } catch { }
_client = null; _client = null;
} }
} }
@@ -144,8 +154,11 @@ namespace Discord.Net.WebSockets
} }
public void SetCancelToken(CancellationToken cancelToken) public void SetCancelToken(CancellationToken cancelToken)
{ {
_cancelTokenSource?.Dispose();

_parentToken = cancelToken; _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) public async Task SendAsync(byte[] data, int index, int count, bool isText)
@@ -166,7 +179,7 @@ namespace Discord.Net.WebSockets
frameSize = count - (i * SendChunkSize); frameSize = count - (i * SendChunkSize);
else else
frameSize = SendChunkSize; frameSize = SendChunkSize;
var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary;
await _client.SendAsync(new ArraySegment<byte>(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); await _client.SendAsync(new ArraySegment<byte>(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false);
} }
@@ -176,7 +189,7 @@ namespace Discord.Net.WebSockets
_lock.Release(); _lock.Release();
} }
} }
private async Task RunAsync(CancellationToken cancelToken) private async Task RunAsync(CancellationToken cancelToken)
{ {
var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]); 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); WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false);
byte[] result; byte[] result;
int resultCount; int resultCount;
if (socketResult.MessageType == WebSocketMessageType.Close) if (socketResult.MessageType == WebSocketMessageType.Close)
throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription);


@@ -219,7 +232,7 @@ namespace Discord.Net.WebSockets
resultCount = socketResult.Count; resultCount = socketResult.Count;
result = buffer.Array; result = buffer.Array;
} }
if (socketResult.MessageType == WebSocketMessageType.Text) if (socketResult.MessageType == WebSocketMessageType.Text)
{ {
string text = Encoding.UTF8.GetString(result, 0, resultCount); string text = Encoding.UTF8.GetString(result, 0, resultCount);


+ 1
- 1
src/Discord.Net.Webhook/Discord.Net.Webhook.csproj View File

@@ -10,4 +10,4 @@
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" /> <ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" />
</ItemGroup> </ItemGroup>
</Project>
</Project>

+ 1
- 0
test/Discord.Net.Tests/Discord.Net.Tests.csproj View File

@@ -5,6 +5,7 @@
<TargetFramework>netcoreapp1.1</TargetFramework> <TargetFramework>netcoreapp1.1</TargetFramework>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback>
<NoWarn>IDISP001,IDISP002,IDISP004,IDISP005</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Content Include="xunit.runner.json"> <Content Include="xunit.runner.json">


+ 7
- 4
test/Discord.Net.Tests/Net/CachedRestClient.cs View File

@@ -43,7 +43,10 @@ namespace Discord.Net
if (!_isDisposed) if (!_isDisposed)
{ {
if (disposing) if (disposing)
{
_blobCache.Dispose(); _blobCache.Dispose();
_cancelTokenSource?.Dispose();
}
_isDisposed = true; _isDisposed = true;
} }
} }
@@ -70,7 +73,7 @@ namespace Discord.Net
{ {
if (method != "GET") if (method != "GET")
throw new InvalidOperationException("This RestClient only supports GET requests."); throw new InvalidOperationException("This RestClient only supports GET requests.");
string uri = Path.Combine(_baseUrl, endpoint); string uri = Path.Combine(_baseUrl, endpoint);
var bytes = await _blobCache.DownloadUrl(uri, _headers); var bytes = await _blobCache.DownloadUrl(uri, _headers);
return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); 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."); throw new InvalidOperationException("This RestClient does not support multipart requests.");
} }


public async Task ClearAsync()
public async Task ClearAsync()
{ {
await _blobCache.InvalidateAll(); await _blobCache.InvalidateAll();
} }
@@ -93,7 +96,7 @@ namespace Discord.Net
{ {
if (Info != null) if (Info != null)
return; return;
bool needsReset = false; bool needsReset = false;
try try
{ {
@@ -117,4 +120,4 @@ namespace Discord.Net
await _blobCache.InsertObject<CacheInfo>("info", Info); await _blobCache.InsertObject<CacheInfo>("info", Info);
} }
} }
}
}

Loading…
Cancel
Save