Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connection cancellation #110

Merged
merged 7 commits into from
Sep 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SharpXMPP.Console/SharpXMPP.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>9.0</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
67 changes: 67 additions & 0 deletions SharpXMPP.NUnit/Compat/TcpClientExTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.dotMemoryUnit;
using NUnit.Framework;

namespace SharpXMPP.Compat
{
public class TcpClientExTests
{
/// <summary>
/// This test checks for TcpClient leaks which are guaranteed to happen if the requests aren't properly
/// cancelled.
/// </summary>
/// <remarks>
/// Run with dotMemoryUnit to check for leaks, run without it for basic sanity check. For details see
/// https://www.jetbrains.com/help/dotmemory-unit/Get_Started.html#3-run-the-test
/// </remarks>
[Test, DotMemoryUnit(FailIfRunWithoutSupport = false)]
public void TestCancellation()
{
const int iterations = 100;
int cancelled = 0;

var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
try
{
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
for (int i = 0; i < iterations; ++i)
{
Task.Run(async () =>
{
using var cts = new CancellationTokenSource();
var token = cts.Token;

using var client = new TcpClient();
var task = client.ConnectWithCancellationAsync(IPAddress.Loopback, port, token);
cts.Cancel();

try
{
await task;
}
catch (OperationCanceledException)
{
++cancelled;
}
}).Wait();
}
}
finally
{
listener.Stop();
}

if (cancelled == 0)
Assert.Inconclusive("No cancellations detected, all connections succeeded");

dotMemory.Check(memory =>
// note there's 1 task object on pre-.NET 5 runtimes
Assert.LessOrEqual(memory.GetObjects(o => o.Type.Is<Task>()).ObjectsCount, 1));
}
}
}
5 changes: 3 additions & 2 deletions SharpXMPP.NUnit/SharpXMPP.NUnit.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>net48;netcoreapp2.1;netcoreapp3.1;net5.0</TargetFrameworks>
<DebugType>portable</DebugType>
<AssemblyName>SharpXMPP.NUnit</AssemblyName>
<PackageId>SharpXMPP.NUnit</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<LangVersion>8.0</LangVersion>
<LangVersion>9.0</LangVersion>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>
Expand All @@ -15,6 +15,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="JetBrains.DotMemoryUnit" Version="3.1.20200127.214830" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
Expand Down
25 changes: 25 additions & 0 deletions SharpXMPP.Shared/Compat/SslStreamEx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Net.Security;
using System.Threading;
using System.Threading.Tasks;

namespace SharpXMPP.Compat
{
public static class SslStreamEx
{
public static Task AuthenticateAsClientWithCancellationAsync(
this SslStream sslStream,
string targetHost,
CancellationToken cancellationToken)
{
#if NET5_0_OR_GREATER
return sslStream.AuthenticateAsClientAsync(
new SslClientAuthenticationOptions { TargetHost = targetHost },
cancellationToken);
#else
// No cancellation on older runtimes :(
cancellationToken.ThrowIfCancellationRequested();
return sslStream.AuthenticateAsClientAsync(targetHost);
#endif
}
}
}
62 changes: 62 additions & 0 deletions SharpXMPP.Shared/Compat/TcpClientEx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace SharpXMPP.Compat
{
internal static class TcpClientEx
{
#if !NET5_0_OR_GREATER
private static async Task AbandonOnCancel(this Task task, CancellationToken cancellationToken)
{
// See https://devblogs.microsoft.com/pfxteam/how-do-i-cancel-non-cancelable-async-operations/ for details.
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(() => tcs.TrySetResult(true)))
{
if (task != await Task.WhenAny(task, tcs.Task))
{
throw new OperationCanceledException(cancellationToken);
}
}

await task;
}
#endif

// Unfortunately, only .NET 5+ supports TcpClient connection cancellation. We'll do the best effort here,
// though.
//
// TcpClient uses Socket::BeginConnect under the covers for all the connection methods on .NET Framework, which
// is documented to be cancelled on Close(). So, ideally, if the caller eventually disposes the client, then all
// the resources will be freed upon its destruction. Which means we are free to just abandon the task in
// question.

public static Task ConnectWithCancellationAsync(
this TcpClient tcpClient,
IPAddress address,
int port,
CancellationToken cancellationToken)
{
#if NET5_0_OR_GREATER
return tcpClient.ConnectAsync(address, port, cancellationToken).AsTask();
#else
return tcpClient.ConnectAsync(address, port).AbandonOnCancel(cancellationToken);
#endif
}

public static Task ConnectWithCancellationAsync(
this TcpClient tcpClient,
IPAddress[] addresses,
int port,
CancellationToken cancellationToken)
{
#if NET5_0_OR_GREATER
return tcpClient.ConnectAsync(addresses, port, cancellationToken).AsTask();
#else
return tcpClient.ConnectAsync(addresses, port).AbandonOnCancel(cancellationToken);
#endif
}
}
}
18 changes: 11 additions & 7 deletions SharpXMPP.Shared/Resolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpXMPP.Compat;

namespace SharpXMPP
{
Expand All @@ -18,12 +20,15 @@ public struct SRVRecord

public static class Resolver
{
public async static Task<List<SRVRecord>> ResolveXMPPClient(string domain)
[Obsolete("Call overload with CancellationToken")]
public static Task<List<SRVRecord>> ResolveXMPPClient(string domain) =>
vitalyster marked this conversation as resolved.
Show resolved Hide resolved
ResolveXMPPClient(domain, default);

public static async Task<List<SRVRecord>> ResolveXMPPClient(string domain, CancellationToken cancellationToken)
{
var result = new List<SRVRecord>();
var client = new TcpClient();
await client.ConnectAsync(IPAddress.Parse("1.1.1.1"), 53);
var stream = client.GetStream();
using var client = new TcpClient();
await client.ConnectWithCancellationAsync(IPAddress.Parse("1.1.1.1"), 53, cancellationToken);
using var stream = client.GetStream();
var message = EncodeQuery(domain);
var lengthPrefix = IPAddress.HostToNetworkOrder((short)message.Length);
var lengthPrefixBytes = BitConverter.GetBytes(lengthPrefix);
Expand All @@ -34,8 +39,7 @@ public async static Task<List<SRVRecord>> ResolveXMPPClient(string domain)
stream.Read(responseLengthBytes, 0, 2);
var responseMessage = new byte[IPAddress.NetworkToHostOrder(BitConverter.ToInt16(responseLengthBytes, 0))];
stream.Read(responseMessage, 0, responseMessage.Length);
result = Decode(responseMessage);
return result;
return Decode(responseMessage);
}

private static byte[] EncodeQuery(string domain)
Expand Down
4 changes: 2 additions & 2 deletions SharpXMPP.Shared/SharpXMPP.Shared.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard1.6;net451</TargetFrameworks>
<TargetFrameworks>netstandard1.6;net451;net5.0</TargetFrameworks>
<AssemblyName>SharpXMPP.Shared</AssemblyName>
<NetStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard1.6' ">1.6.1</NetStandardImplicitPackageVersion>
<LangVersion>7.2</LangVersion>
<LangVersion>9.0</LangVersion>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>

Expand Down
Loading