| File: SocketServer.cs | Web Access |
| Project: src\vstest\src\Microsoft.TestPlatform.CommunicationUtilities\Microsoft.TestPlatform.CommunicationUtilities.csproj (Microsoft.TestPlatform.CommunicationUtilities) |
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.Utilities; namespace Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; /// <summary> /// Communication server implementation over sockets. /// </summary> [SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "Would cause a breaking change if users are inheriting this class and implement IDisposable")] public class SocketServer : ICommunicationEndPoint { private readonly CancellationTokenSource _cancellation; private readonly Func<TcpListener, Task<TcpClient>> _acceptClientAsync; private readonly Func<Stream, ICommunicationChannel> _channelFactory; private readonly object _stateSyncObject = new(); private ICommunicationChannel? _channel; private TcpListener? _tcpListener; private TcpClient? _tcpClient; private int _stopRequested; private bool _stopped; private string? _endPoint; /// <summary> /// Initializes a new instance of the <see cref="SocketServer"/> class. /// </summary> public SocketServer() : this(stream => new LengthPrefixCommunicationChannel(stream)) { } /// <summary> /// Initializes a new instance of the <see cref="SocketServer"/> class with given channel /// factory implementation. /// </summary> /// <param name="channelFactory">Factory to create communication channel.</param> protected SocketServer(Func<Stream, ICommunicationChannel> channelFactory) : this(channelFactory, tcpListener => tcpListener.AcceptTcpClientAsync()) { } internal SocketServer( Func<Stream, ICommunicationChannel> channelFactory, Func<TcpListener, Task<TcpClient>> acceptClientAsync) { // Used to cancel the message loop _cancellation = new CancellationTokenSource(); _channelFactory = channelFactory; _acceptClientAsync = acceptClientAsync; } /// <inheritdoc /> public event EventHandler<ConnectedEventArgs>? Connected; /// <inheritdoc /> public event EventHandler<DisconnectedEventArgs>? Disconnected; public string? Start(string endPoint) { try { TcpListener tcpListener; lock (_stateSyncObject) { if (_stopRequested != 0) { throw new ObjectDisposedException(nameof(SocketServer)); } _tcpListener = new TcpListener(endPoint.GetIpEndPoint()); _tcpListener.Start(); tcpListener = _tcpListener; _endPoint = _tcpListener.LocalEndpoint.ToString(); EqtTrace.Info("SocketServer.Start: Listening on endpoint : {0}", _endPoint); } // Serves a single client at the moment. An error in connection, or message loop just // terminates the entire server. _ = AcceptClientAsync(tcpListener); return _endPoint; } catch (SocketException ex) { EqtTrace.Error("Failed for address {0}, with: {1}", endPoint, ex); throw; } } /// <inheritdoc /> public void Stop() { EqtTrace.Info("SocketServer.Stop: Stop server endPoint: {0}", _endPoint); lock (_stateSyncObject) { if (_stopRequested != 0) { return; } _stopRequested = 1; EqtTrace.Info("SocketServer.Stop: Cancellation requested. Stopping message loop."); try { _cancellation.Cancel(); } catch (ObjectDisposedException) { // StopOnError disposed the cancellation source concurrently. } _tcpListener?.Stop(); } } private async Task AcceptClientAsync(TcpListener tcpListener) { TcpClient? client = null; try { try { client = await _acceptClientAsync(tcpListener).ConfigureAwait(false); } catch (Exception ex) when (Volatile.Read(ref _stopRequested) != 0 && ex is ObjectDisposedException or SocketException or InvalidOperationException) { EqtTrace.Verbose("SocketServer.AcceptClientAsync: Listener stopped before a client connected: {0}", ex); return; } lock (_stateSyncObject) { if (_stopRequested != 0) { client.Close(); return; } _tcpClient = client; } OnClientConnected(client); } catch (Exception ex) { EqtTrace.Error("SocketServer.AcceptClientAsync: Failed to accept a client: {0}", ex); client?.Close(); Stop(); } } private void OnClientConnected(TcpClient client) { client.Client.NoDelay = true; if (Connected is null) { return; } _channel = _channelFactory(client.GetStream()); Connected.SafeInvoke(this, new ConnectedEventArgs(_channel), "SocketServer: ClientConnected"); EqtTrace.Verbose("SocketServer.OnClientConnected: Client connected for endPoint: {0}, starting MessageLoopAsync:", _endPoint); // Start the message loop _ = Task.Run(() => client.MessageLoopAsync(_channel, error => StopOnError(error), _cancellation.Token)); } /// <summary> /// Stop the connection when error was encountered. Dispose all communication, and notify subscribers of Disconnected event /// that we aborted. /// </summary> /// <param name="error"></param> private void StopOnError(Exception? error) { EqtTrace.Info("SocketServer.PrivateStop: Stopping server endPoint: {0} error: {1}", _endPoint, error); if (_stopped) { return; } TPDebug.Assert(_tcpListener is not null, $"{nameof(_tcpListener)} is null"); TPDebug.Assert(_channel is not null, $"{nameof(_channel)} is null"); // Do not allow stop to be called multiple times. _stopped = true; // Stop accepting any other connections _tcpListener.Stop(); // Close the client and dispose the underlying stream // tcpClient.Close() calls tcpClient.Dispose(). _tcpClient?.Close(); _channel.Dispose(); _cancellation.Dispose(); EqtTrace.Info("SocketServer.Stop: Raise disconnected event endPoint: {0} error: {1}", _endPoint, error); Disconnected?.SafeInvoke(this, new DisconnectedEventArgs { Error = error }, "SocketServer: ClientDisconnected"); } }