// We use a separate bool field to store whether the value has been set.
// We don't use nullables, due to one of the properties being a reference type.
+
+ private static readonly CancellationTokenSource s_canceledSource = CreateCanceledSource();
+ private CancellationTokenSource _disposing;
private ShadowOptions _shadowOptions; // shadow state used in public properties before the socket is created
private int _connectRunning; // tracks whether a connect operation that could set _clientSocket is currently running
// Nop. We want to lazily-allocate the socket.
}
+ private void DisposeCore()
+ {
+ // In case there's a concurrent ConnectAsync operation, we need to signal to that
+ // operation that we're being disposed of, so that it can dispose of the current
+ // temporary socket that hasn't yet been published as the official one. If there's
+ // already a cancellation source, just cancel it. If there isn't, try to swap in
+ // an already-canceled source so that we don't have to artificially create a new one
+ // (since not all async connect operations require temporary sockets), but we may
+ // lose that race condition, in which case we still need to dispose of whatever is
+ // published. It's fine to Cancel an already canceled cancellation source.
+ if (Volatile.Read(ref _disposing) == null)
+ {
+ Interlocked.CompareExchange(ref _disposing, s_canceledSource, null);
+ }
+ _disposing.Cancel();
+ }
+
private Socket ClientCore
{
get
try
{
// The Client socket is being explicitly accessed, so we're forced
- // to create it if it doesn't exist.
- if (_clientSocket == null)
+ // to create it if it doesn't exist. Only do so if we haven't been disposed of,
+ // which nulls out the field.
+ if (_clientSocket == null && (_disposing == null || !_disposing.IsCancellationRequested))
{
// Create the socket, and transfer to it any of our shadow properties.
_clientSocket = CreateSocket();
{
try
{
+ // Make sure we've created a disposing cancellation source so that we get alerted
+ // to a potentially concurrent disposal happening.
+ if (Volatile.Read(ref _disposing) != null && _disposing.IsCancellationRequested)
+ {
+ throw new ObjectDisposedException(GetType().Name);
+ }
+ Interlocked.CompareExchange(ref _disposing, new CancellationTokenSource(), null);
+
// For each address, create a new socket (configured appropriately) and try to connect
// to the endpoint. If we're successful, set the newly connected socket as the client
// socket, and we're done. If we're unsuccessful, try the next address.
Socket s = CreateSocket();
try
{
+ // Configure the socket
ApplyInitializedOptionsToSocket(s);
- await s.ConnectAsync(address, port).ConfigureAwait(false);
+ // Register to dispose of the socket when the TcpClient is Dispose'd of.
+ // Some consumers use Dispose as a way to cancel a connect operation, as
+ // TcpClient.Dispose calls Socket.Dispose on the stored socket... but we've
+ // not stored the socket into the field yet, as doing so will publish it
+ // to be seen via the Client property. Instead, we register to be notified
+ // when Dispose is called or has happened, and Dispose of the socket
+ using (_disposing.Token.Register(o => ((Socket)o).Dispose(), s))
+ {
+ await s.ConnectAsync(address, port).ConfigureAwait(false);
+ }
_clientSocket = s;
_active = true;
+ if (_disposing.IsCancellationRequested)
+ {
+ s.Dispose();
+ _clientSocket = null;
+ }
return;
}
- catch (Exception exc)
+ catch (Exception exc) when (!(exc is ObjectDisposedException))
{
s.Dispose();
lastException = ExceptionDispatchInfo.Capture(exc);
Volatile.Write(ref _connectRunning, 0);
}
+ private static CancellationTokenSource CreateCanceledSource()
+ {
+ var cts = new CancellationTokenSource();
+ cts.Cancel();
+ return cts;
+ }
+
private sealed class ShadowOptions
{
internal int _exclusiveAddressUse;
[DebuggerBrowsable(DebuggerBrowsableState.Never)] // TODO: Remove once https://github.com/dotnet/corefx/issues/5868 is addressed.
public Socket Client
{
- get
- {
- Socket s = ClientCore;
- Debug.Assert(s != null);
- return s;
- }
- set
- {
- ClientCore = value;
- }
+ get { return ClientCore; }
+ set { ClientCore = value; }
}
public bool Connected { get { return ConnectedCore; } }
NetEventSource.Enter(NetEventSource.ComponentType.Socket, this, "EndConnect", asyncResult);
}
- Client.EndConnect(asyncResult);
+ Socket s = Client;
+ if (s == null)
+ {
+ // Dispose nulls out the client socket field.
+ throw new ObjectDisposedException(GetType().Name);
+ }
+ s.EndConnect(asyncResult);
+
_active = true;
if (NetEventSource.Log.IsEnabled())
{
}
}
+ DisposeCore(); // platform-specific disposal work
+
GC.SuppressFinalize(this);
}
using System.Threading.Tasks;
using System.Net.Test.Common;
using System.Text;
+using System.Collections.Generic;
+using System.Diagnostics;
namespace System.Net.Sockets.Tests
{
// minimums and maximums, silently capping to those amounts.
}
}
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task Dispose_CancelsConnectAsync(bool connectByName)
+ {
+ using (var server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
+ {
+ // Set up a server socket to which to connect
+ server.Bind(new IPEndPoint(IPAddress.Loopback, 0));
+ server.Listen(1);
+ var endpoint = (IPEndPoint)server.LocalEndPoint;
+
+ // Connect asynchronously...
+ var client = new TcpClient();
+ Task connectTask = connectByName ?
+ client.ConnectAsync("localhost", endpoint.Port) :
+ client.ConnectAsync(endpoint.Address, endpoint.Port);
+
+ // ...and hopefully before it's completed connecting, dispose.
+ var sw = Stopwatch.StartNew();
+ client.Dispose();
+
+ // There is a race condition here. If the connection succeeds before the
+ // disposal, then the task will complete successfully. Otherwise, it should
+ // fail with an ObjectDisposedException.
+ try
+ {
+ await connectTask;
+ }
+ catch (ObjectDisposedException) { }
+ sw.Stop();
+
+ Assert.Null(client.Client); // should be nulled out after Dispose
+ }
+ }
}
}