Add support for server-client mode in dsrouter. (#2251)
authorJohan Lorensson <lateralusx.github@gmail.com>
Wed, 12 May 2021 09:41:55 +0000 (11:41 +0200)
committerGitHub <noreply@github.com>
Wed, 12 May 2021 09:41:55 +0000 (02:41 -0700)
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs
src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs
src/Tools/dotnet-dsrouter/Program.cs

index f0e3d9f2645a910cff0661fc9453aef85435ed14..967d5470cb0e8fc91ab171aafb5b778cfbadfa31 100644 (file)
@@ -13,6 +13,7 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using Microsoft.Extensions.Logging;
 using System.Net;
+using System.Net.Sockets;
 
 namespace Microsoft.Diagnostics.NETCore.Client
 {
@@ -35,17 +36,13 @@ namespace Microsoft.Diagnostics.NETCore.Client
     /// </summary>
     internal class DiagnosticsServerRouterFactory
     {
-        protected readonly ILogger _logger;
+        int IsStreamConnectedTimeoutMs { get; set; } = 500;
 
-        public DiagnosticsServerRouterFactory(ILogger logger)
-        {
-            _logger = logger;
-        }
+        public virtual string IpcAddress { get; }
 
-        public ILogger Logger
-        {
-            get { return _logger; }
-        }
+        public virtual string TcpAddress { get; }
+
+        public virtual ILogger Logger { get; }
 
         public virtual void Start()
         {
@@ -66,23 +63,104 @@ namespace Microsoft.Diagnostics.NETCore.Client
         {
             throw new NotImplementedException();
         }
+
+        protected bool IsStreamConnected(Stream stream, CancellationToken token)
+        {
+            bool connected = true;
+
+            if (stream is NamedPipeServerStream || stream is NamedPipeClientStream)
+            {
+                PipeStream pipeStream = stream as PipeStream;
+
+                // PeekNamedPipe will return false if the pipe is disconnected/broken.
+                connected = NativeMethods.PeekNamedPipe(
+                    pipeStream.SafePipeHandle,
+                    null,
+                    0,
+                    IntPtr.Zero,
+                    IntPtr.Zero,
+                    IntPtr.Zero);
+            }
+            else if (stream is ExposedSocketNetworkStream networkStream)
+            {
+                bool blockingState = networkStream.Socket.Blocking;
+                try
+                {
+                    // Check connection read state by peek one byte. Will return 0 in case connection is closed.
+                    // A closed connection could also raise exception, but then socket connected state should
+                    // be set to false.
+                    networkStream.Socket.Blocking = false;
+                    if (networkStream.Socket.Receive(new byte[1], 0, 1, System.Net.Sockets.SocketFlags.Peek) == 0)
+                        connected = false;
+
+                    // Check connection write state by sending non-blocking zero-byte data.
+                    // A closed connection should raise exception, but then socket connected state should
+                    // be set to false.
+                    if (connected)
+                        networkStream.Socket.Send(Array.Empty<byte>(), 0, System.Net.Sockets.SocketFlags.None);
+                }
+                catch (Exception)
+                {
+                    connected = networkStream.Socket.Connected;
+                }
+                finally
+                {
+                    networkStream.Socket.Blocking = blockingState;
+                }
+            }
+            else
+            {
+                connected = false;
+            }
+
+            return connected;
+        }
+
+        protected async Task IsStreamConnectedAsync(Stream stream, CancellationToken token)
+        {
+            while (!token.IsCancellationRequested)
+            {
+                // Check if tcp stream connection is still available.
+                if (!IsStreamConnected(stream, token))
+                {
+                    throw new EndOfStreamException();
+                }
+
+                try
+                {
+                    // Wait before rechecking connection.
+                    await Task.Delay(IsStreamConnectedTimeoutMs, token).ConfigureAwait(false);
+                }
+                catch { }
+            }
+        }
+
+        protected bool IsCompletedSuccessfully(Task t)
+        {
+#if NETCOREAPP2_0_OR_GREATER
+            return t.IsCompletedSuccessfully;
+#else
+            return t.IsCompleted && !t.IsCanceled && !t.IsFaulted;
+#endif
+        }
     }
 
     /// <summary>
     /// This class represent a TCP/IP server endpoint used when building up router instances.
     /// </summary>
-    internal class TcpServerRouterFactory : DiagnosticsServerRouterFactory
+    internal class TcpServerRouterFactory : IIpcServerTransportCallbackInternal
     {
-        protected string _tcpServerAddress;
+        readonly ILogger _logger;
+
+        string _tcpServerAddress;
 
-        protected ReversedDiagnosticsServer _tcpServer;
-        protected IpcEndpointInfo _tcpServerEndpointInfo;
+        ReversedDiagnosticsServer _tcpServer;
+        IpcEndpointInfo _tcpServerEndpointInfo;
 
-        protected bool _auto_shutdown;
+        bool _auto_shutdown;
 
-        protected int RuntimeTimeoutMs { get; set; } = 60000;
-        protected int TcpServerTimeoutMs { get; set; } = 5000;
-        protected int IsStreamConnectedTimeoutMs { get; set; } = 500;
+        int RuntimeTimeoutMs { get; set; } = 60000;
+        int TcpServerTimeoutMs { get; set; } = 5000;
 
         public Guid RuntimeInstanceId
         {
@@ -99,9 +177,10 @@ namespace Microsoft.Diagnostics.NETCore.Client
             get { return _tcpServerAddress; }
         }
 
-        protected TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger logger)
-            : base(logger)
+        public TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger logger)
         {
+            _logger = logger;
+
             _tcpServerAddress = IpcTcpSocketEndPoint.NormalizeTcpIpEndPoint(string.IsNullOrEmpty(tcpServer) ? "127.0.0.1:0" : tcpServer);
 
             _auto_shutdown = runtimeTimeoutMs != Timeout.Infinite;
@@ -110,19 +189,20 @@ namespace Microsoft.Diagnostics.NETCore.Client
 
             _tcpServer = new ReversedDiagnosticsServer(_tcpServerAddress, enableTcpIpProtocol : true);
             _tcpServerEndpointInfo = new IpcEndpointInfo();
+            _tcpServer.TransportCallback = this;
         }
 
-        public override void Start()
+        public void Start()
         {
             _tcpServer.Start();
         }
 
-        public override async Task Stop()
+        public async Task Stop()
         {
             await _tcpServer.DisposeAsync().ConfigureAwait(false);
         }
 
-        public override void Reset()
+        public void Reset()
         {
             if (_tcpServerEndpointInfo.Endpoint != null)
             {
@@ -131,11 +211,11 @@ namespace Microsoft.Diagnostics.NETCore.Client
             }
         }
 
-        protected async Task<Stream> AcceptTcpStreamAsync(CancellationToken token)
+        public async Task<Stream> AcceptTcpStreamAsync(CancellationToken token)
         {
             Stream tcpServerStream;
 
-            Logger.LogDebug($"Waiting for a new tcp connection at endpoint \"{_tcpServerAddress}\".");
+            _logger?.LogDebug($"Waiting for a new tcp connection at endpoint \"{_tcpServerAddress}\".");
 
             if (_tcpServerEndpointInfo.Endpoint == null)
             {
@@ -152,7 +232,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
                 {
                     if (acceptTimeoutTokenSource.IsCancellationRequested)
                     {
-                        Logger.LogDebug("No runtime instance connected before timeout.");
+                        _logger?.LogDebug("No runtime instance connected before timeout.");
 
                         if (_auto_shutdown)
                             throw new RuntimeTimeoutException(RuntimeTimeoutMs);
@@ -176,7 +256,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
             {
                 if (connectTimeoutTokenSource.IsCancellationRequested)
                 {
-                    Logger.LogDebug("No tcp stream connected before timeout.");
+                    _logger?.LogDebug("No tcp stream connected before timeout.");
                     throw new BackendStreamTimeoutException(TcpServerTimeoutMs);
                 }
 
@@ -184,89 +264,326 @@ namespace Microsoft.Diagnostics.NETCore.Client
             }
 
             if (tcpServerStream != null)
-                Logger.LogDebug($"Successfully connected tcp stream, runtime id={RuntimeInstanceId}, runtime pid={RuntimeProcessId}.");
+                _logger?.LogDebug($"Successfully connected tcp stream, runtime id={RuntimeInstanceId}, runtime pid={RuntimeProcessId}.");
 
             return tcpServerStream;
         }
 
-        protected bool IsStreamConnected(Stream stream, CancellationToken token)
+        public void CreatedNewServer(EndPoint localEP)
         {
-            bool connected = true;
+            if (localEP is IPEndPoint ipEP)
+                _tcpServerAddress = _tcpServerAddress.Replace(":0", string.Format(":{0}", ipEP.Port));
+        }
+    }
 
-            if (stream is NamedPipeServerStream || stream is NamedPipeClientStream)
+    /// <summary>
+    /// This class represent a TCP/IP client endpoint used when building up router instances.
+    /// </summary>
+    internal class TcpClientRouterFactory
+    {
+        readonly ILogger _logger;
+
+        readonly string _tcpClientAddress;
+
+        bool _auto_shutdown;
+
+        int TcpClientTimeoutMs { get; set; } = Timeout.Infinite;
+
+        int TcpClientRetryTimeoutMs { get; set; } = 500;
+
+        public string TcpClientAddress {
+            get { return _tcpClientAddress; }
+        }
+
+        public TcpClientRouterFactory(string tcpClient, int runtimeTimeoutMs, ILogger logger)
+        {
+            _logger = logger;
+            _tcpClientAddress = IpcTcpSocketEndPoint.NormalizeTcpIpEndPoint(string.IsNullOrEmpty(tcpClient) ? "127.0.0.1:" + string.Format("{0}", 56000 + (Process.GetCurrentProcess().Id % 1000)) : tcpClient);
+            _auto_shutdown = runtimeTimeoutMs != Timeout.Infinite;
+            if (runtimeTimeoutMs != Timeout.Infinite)
+                TcpClientTimeoutMs = runtimeTimeoutMs;
+        }
+
+        public async Task<Stream> ConnectTcpStreamAsync(CancellationToken token)
+        {
+            Stream tcpClientStream = null;
+
+            _logger?.LogDebug($"Connecting new tcp endpoint \"{_tcpClientAddress}\".");
+
+            bool retry = false;
+            IpcTcpSocketEndPoint clientTcpEndPoint = new IpcTcpSocketEndPoint(_tcpClientAddress);
+            Socket clientSocket = null;
+
+            using var connectTimeoutTokenSource = new CancellationTokenSource();
+            using var connectTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, connectTimeoutTokenSource.Token);
+
+            connectTimeoutTokenSource.CancelAfter(TcpClientTimeoutMs);
+
+            do
             {
-                PipeStream pipeStream = stream as PipeStream;
+                clientSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
 
-                // PeekNamedPipe will return false if the pipe is disconnected/broken.
-                connected = NativeMethods.PeekNamedPipe(
-                    pipeStream.SafePipeHandle,
-                    null,
-                    0,
-                    IntPtr.Zero,
-                    IntPtr.Zero,
-                    IntPtr.Zero);
+                try
+                {
+                    await ConnectAsyncInternal(clientSocket, clientTcpEndPoint, token).ConfigureAwait(false);
+                    retry = false;
+                }
+                catch (Exception)
+                {
+                    clientSocket?.Dispose();
+
+                    if (connectTimeoutTokenSource.IsCancellationRequested)
+                    {
+                        _logger?.LogDebug("No tcp stream connected, timing out.");
+
+                        if (_auto_shutdown)
+                            throw new RuntimeTimeoutException(TcpClientTimeoutMs);
+
+                        throw new TimeoutException();
+                    }
+
+                    // If we are not doing auto shutdown when runtime is unavailable, fail right away, this will
+                    // break any accepted IPC connections, making sure client is notified and could reconnect.
+                    // If we do have auto shutdown enabled, retry until succeed or time out.
+                    if (!_auto_shutdown)
+                    {
+                        _logger?.LogTrace($"Failed connecting {_tcpClientAddress}.");
+                        throw;
+                    }
+
+                    _logger?.LogTrace($"Failed connecting {_tcpClientAddress}, wait {TcpClientRetryTimeoutMs} ms before retrying.");
+
+                    // If we get an error (without hitting timeout above), most likely due to unavailable listener.
+                    // Delay execution to prevent to rapid retry attempts.
+                    await Task.Delay(TcpClientRetryTimeoutMs, token).ConfigureAwait(false);
+
+                    retry = true;
+                }
             }
-            else if (stream is ExposedSocketNetworkStream networkStream)
+            while (retry);
+
+            tcpClientStream = new ExposedSocketNetworkStream(clientSocket, ownsSocket: true);
+            _logger?.LogDebug("Successfully connected tcp stream.");
+
+            return tcpClientStream;
+        }
+
+        async Task ConnectAsyncInternal(Socket clientSocket, EndPoint remoteEP, CancellationToken token)
+        {
+            using (token.Register(() => clientSocket.Close(0)))
             {
-                bool blockingState = networkStream.Socket.Blocking;
                 try
                 {
-                    // Check connection read state by peek one byte. Will return 0 in case connection is closed.
-                    // A closed connection could also raise exception, but then socket connected state should
-                    // be set to false.
-                    networkStream.Socket.Blocking = false;
-                    if (networkStream.Socket.Receive(new byte[1], 0, 1, System.Net.Sockets.SocketFlags.Peek) == 0)
-                        connected = false;
-
-                    // Check connection write state by sending non-blocking zero-byte data.
-                    // A closed connection should raise exception, but then socket connected state should
-                    // be set to false.
-                    if (connected)
-                        networkStream.Socket.Send(Array.Empty<byte>(), 0, System.Net.Sockets.SocketFlags.None);
+                    Func<AsyncCallback, object, IAsyncResult> beginConnect = (callback, state) =>
+                    {
+                        return clientSocket.BeginConnect(remoteEP, callback, state);
+                    };
+                    await Task.Factory.FromAsync(beginConnect, clientSocket.EndConnect, this).ConfigureAwait(false);
                 }
-                catch (Exception)
+                // When the socket is closed, the FromAsync logic will try to call EndAccept on the socket,
+                // but that will throw an ObjectDisposedException. Only catch the exception if due to cancellation.
+                catch (ObjectDisposedException) when (token.IsCancellationRequested)
                 {
-                    connected = networkStream.Socket.Connected;
+                    // First check if the cancellation token caused the closing of the socket,
+                    // then rethrow the exception if it did not.
+                    token.ThrowIfCancellationRequested();
                 }
-                finally
+            }
+        }
+    }
+
+    /// <summary>
+    /// This class represent a IPC server endpoint used when building up router instances.
+    /// </summary>
+    internal class IpcServerRouterFactory
+    {
+        readonly ILogger _logger;
+
+        readonly string _ipcServerPath;
+
+        IpcServerTransport _ipcServer;
+
+        int IpcServerTimeoutMs { get; set; } = Timeout.Infinite;
+
+        public string IpcServerPath {
+            get { return _ipcServerPath; }
+        }
+
+        public IpcServerRouterFactory(string ipcServer, ILogger logger)
+        {
+            _logger = logger;
+            _ipcServerPath = ipcServer;
+            if (string.IsNullOrEmpty(_ipcServerPath))
+                _ipcServerPath = GetDefaultIpcServerPath();
+
+            _ipcServer = IpcServerTransport.Create(_ipcServerPath, IpcServerTransport.MaxAllowedConnections, false);
+        }
+
+        public void Start()
+        {
+        }
+
+        public void Stop()
+        {
+            _ipcServer?.Dispose();
+        }
+
+        public async Task<Stream> AcceptIpcStreamAsync(CancellationToken token)
+        {
+            Stream ipcServerStream = null;
+
+            _logger?.LogDebug($"Waiting for new ipc connection at endpoint \"{_ipcServerPath}\".");
+
+
+            using var connectTimeoutTokenSource = new CancellationTokenSource();
+            using var connectTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, connectTimeoutTokenSource.Token);
+
+            try
+            {
+                connectTimeoutTokenSource.CancelAfter(IpcServerTimeoutMs);
+                ipcServerStream = await _ipcServer.AcceptAsync(connectTokenSource.Token).ConfigureAwait(false);
+            }
+            catch (Exception)
+            {
+                ipcServerStream?.Dispose();
+
+                if (connectTimeoutTokenSource.IsCancellationRequested)
                 {
-                    networkStream.Socket.Blocking = blockingState;
+                    _logger?.LogDebug("No ipc stream connected, timing out.");
+                    throw new TimeoutException();
                 }
+
+                throw;
+            }
+
+            if (ipcServerStream != null)
+                _logger?.LogDebug("Successfully connected ipc stream.");
+
+            return ipcServerStream;
+        }
+
+        static string GetDefaultIpcServerPath()
+        {
+            int processId = Process.GetCurrentProcess().Id;
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}");
             }
             else
             {
-                connected = false;
+                DateTime unixEpoch;
+#if NETCOREAPP2_1_OR_GREATER
+                unixEpoch = DateTime.UnixEpoch;
+#else
+                unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
+#endif
+                TimeSpan diff = Process.GetCurrentProcess().StartTime.ToUniversalTime() - unixEpoch;
+                return Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-{(long)diff.TotalSeconds}-socket");
             }
+        }
+    }
 
-            return connected;
+    /// <summary>
+    /// This class represent a IPC client endpoint used when building up router instances.
+    /// </summary>
+    internal class IpcClientRouterFactory
+    {
+        readonly ILogger _logger;
+
+        readonly string _ipcClientPath;
+
+        int IpcClientTimeoutMs { get; set; } = Timeout.Infinite;
+
+        int IpcClientRetryTimeoutMs { get; set; } = 500;
+
+        public string IpcClientPath {
+            get { return _ipcClientPath; }
         }
 
-        protected async Task IsStreamConnectedAsync(Stream stream, CancellationToken token)
+        public IpcClientRouterFactory(string ipcClient, ILogger logger)
         {
-            while (!token.IsCancellationRequested)
+            _logger = logger;
+            _ipcClientPath = ipcClient;
+        }
+
+        public async Task<Stream> ConnectIpcStreamAsync(CancellationToken token)
+        {
+            Stream ipcClientStream = null;
+
+            _logger?.LogDebug($"Connecting new ipc endpoint \"{_ipcClientPath}\".");
+
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
             {
-                // Check if tcp stream connection is still available.
-                if (!IsStreamConnected(stream, token))
+                var namedPipe = new NamedPipeClientStream(
+                    ".",
+                    _ipcClientPath,
+                    PipeDirection.InOut,
+                    PipeOptions.Asynchronous,
+                    TokenImpersonationLevel.Impersonation);
+
+                try
                 {
-                    throw new EndOfStreamException();
+                    await namedPipe.ConnectAsync(IpcClientTimeoutMs, token).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    namedPipe?.Dispose();
+
+                    if (ex is TimeoutException)
+                        _logger?.LogDebug("No ipc stream connected, timing out.");
+
+                    throw;
+                }
+
+                ipcClientStream = namedPipe;
+            }
+            else
+            {
+                bool retry = false;
+                IpcUnixDomainSocket unixDomainSocket;
+
+                using var connectTimeoutTokenSource = new CancellationTokenSource();
+                using var connectTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, connectTimeoutTokenSource.Token);
+
+                connectTimeoutTokenSource.CancelAfter(IpcClientTimeoutMs);
+
+                do
+                {
+                    unixDomainSocket = new IpcUnixDomainSocket();
+
+                    try
+                    {
+                        await unixDomainSocket.ConnectAsync(new IpcUnixDomainSocketEndPoint(_ipcClientPath), token).ConfigureAwait(false);
+                        retry = false;
+                    }
+                    catch (Exception)
+                    {
+                        unixDomainSocket?.Dispose();
+
+                        if (connectTimeoutTokenSource.IsCancellationRequested)
+                        {
+                            _logger?.LogDebug("No ipc stream connected, timing out.");
+                            throw new TimeoutException();
+                        }
+
+                        _logger?.LogTrace($"Failed connecting {_ipcClientPath}, wait {IpcClientRetryTimeoutMs} ms before retrying.");
+
+                        // If we get an error (without hitting timeout above), most likely due to unavailable listener.
+                        // Delay execution to prevent to rapid retry attempts.
+                        await Task.Delay(IpcClientRetryTimeoutMs, token).ConfigureAwait(false);
+
+                        retry = true;
+                    }
                 }
+                while (retry);
 
-                try
-                {
-                    // Wait before rechecking connection.
-                    await Task.Delay(IsStreamConnectedTimeoutMs, token).ConfigureAwait(false);
-                }
-                catch { }
+                ipcClientStream = new ExposedSocketNetworkStream(unixDomainSocket, ownsSocket: true);
             }
-        }
 
-        protected bool IsCompletedSuccessfully(Task t)
-        {
-#if NETCOREAPP2_0_OR_GREATER
-            return t.IsCompletedSuccessfully;
-#else
-            return t.IsCompleted && !t.IsCanceled && !t.IsFaulted;
-#endif
+            if (ipcClientStream != null)
+                _logger?.LogDebug("Successfully connected ipc stream.");
+
+            return ipcClientStream;
         }
     }
 
@@ -274,55 +591,60 @@ namespace Microsoft.Diagnostics.NETCore.Client
     /// This class creates IPC Server - TCP Server router instances.
     /// Supports NamedPipes/UnixDomainSocket server and TCP/IP server.
     /// </summary>
-    internal class IpcServerTcpServerRouterFactory : TcpServerRouterFactory, IIpcServerTransportCallbackInternal
+    internal class IpcServerTcpServerRouterFactory : DiagnosticsServerRouterFactory
     {
-        readonly string _ipcServerPath;
-
-        IpcServerTransport _ipcServer;
-
-        protected int IpcServerTimeoutMs { get; set; } = Timeout.Infinite;
+        ILogger _logger;
+        TcpServerRouterFactory _tcpServerRouterFactory;
+        IpcServerRouterFactory _ipcServerRouterFactory;
 
         public IpcServerTcpServerRouterFactory(string ipcServer, string tcpServer, int runtimeTimeoutMs, ILogger logger)
-            : base(tcpServer, runtimeTimeoutMs, logger)
         {
-            _ipcServerPath = ipcServer;
-            if (string.IsNullOrEmpty(_ipcServerPath))
-                _ipcServerPath = GetDefaultIpcServerPath();
+            _logger = logger;
+            _tcpServerRouterFactory = new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger);
+            _ipcServerRouterFactory = new IpcServerRouterFactory(ipcServer, logger);
+        }
 
-            _ipcServer = IpcServerTransport.Create(_ipcServerPath, IpcServerTransport.MaxAllowedConnections, false);
-            _tcpServer.TransportCallback = this;
+        public override string IpcAddress
+        {
+            get
+            {
+                return _ipcServerRouterFactory.IpcServerPath;
+            }
         }
 
-        public static string GetDefaultIpcServerPath()
+        public override string TcpAddress
         {
-            int processId = Process.GetCurrentProcess().Id;
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            get
             {
-                return Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}");
+                return _tcpServerRouterFactory.TcpServerAddress;
             }
-            else
+        }
+
+        public override ILogger Logger
+        {
+            get
             {
-                DateTime unixEpoch;
-#if NETCOREAPP2_1_OR_GREATER
-                unixEpoch = DateTime.UnixEpoch;
-#else
-                unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
-#endif
-                TimeSpan diff = Process.GetCurrentProcess().StartTime.ToUniversalTime() - unixEpoch;
-                return Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-{(long)diff.TotalSeconds}-socket");
+                return _logger;
             }
         }
 
         public override void Start()
         {
-            base.Start();
-            _logger.LogInformation($"Starting IPC server ({_ipcServerPath}) <--> TCP server ({_tcpServerAddress}) router.");
+            _tcpServerRouterFactory.Start();
+            _ipcServerRouterFactory.Start();
+
+            _logger?.LogInformation($"Starting IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router.");
         }
 
         public override Task Stop()
         {
-            _ipcServer?.Dispose();
-            return base.Stop();
+            _ipcServerRouterFactory.Stop();
+            return _tcpServerRouterFactory.Stop();
+        }
+
+        public override void Reset()
+        {
+            _tcpServerRouterFactory.Reset();
         }
 
         public override async Task<Router> CreateRouterAsync(CancellationToken token)
@@ -330,17 +652,17 @@ namespace Microsoft.Diagnostics.NETCore.Client
             Stream tcpServerStream = null;
             Stream ipcServerStream = null;
 
-            Logger.LogDebug($"Trying to create new router instance.");
+            _logger?.LogDebug($"Trying to create new router instance.");
 
             try
             {
                 using CancellationTokenSource cancelRouter = CancellationTokenSource.CreateLinkedTokenSource(token);
 
                 // Get new tcp server endpoint.
-                using var tcpServerStreamTask = AcceptTcpStreamAsync(cancelRouter.Token);
+                using var tcpServerStreamTask = _tcpServerRouterFactory.AcceptTcpStreamAsync(cancelRouter.Token);
 
                 // Get new ipc server endpoint.
-                using var ipcServerStreamTask = AcceptIpcStreamAsync(cancelRouter.Token);
+                using var ipcServerStreamTask = _ipcServerRouterFactory.AcceptIpcStreamAsync(cancelRouter.Token);
 
                 await Task.WhenAny(ipcServerStreamTask, tcpServerStreamTask).ConfigureAwait(false);
 
@@ -375,7 +697,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
 
                         if (checkIpcStreamTask.IsFaulted)
                         {
-                            Logger.LogInformation("Broken ipc connection detected, aborting tcp connection.");
+                            _logger?.LogInformation("Broken ipc connection detected, aborting tcp connection.");
                             checkIpcStreamTask.GetAwaiter().GetResult();
                         }
 
@@ -410,7 +732,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
 
                         if (checkTcpStreamTask.IsFaulted)
                         {
-                            Logger.LogInformation("Broken tcp connection detected, aborting ipc connection.");
+                            _logger?.LogInformation("Broken tcp connection detected, aborting ipc connection.");
                             checkTcpStreamTask.GetAwaiter().GetResult();
                         }
 
@@ -438,7 +760,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
             }
             catch (Exception)
             {
-                Logger.LogDebug("Failed creating new router instance.");
+                _logger?.LogDebug("Failed creating new router instance.");
 
                 // Cleanup and rethrow.
                 ipcServerStream?.Dispose();
@@ -448,49 +770,128 @@ namespace Microsoft.Diagnostics.NETCore.Client
             }
 
             // Create new router.
-            Logger.LogDebug("New router instance successfully created.");
+            _logger?.LogDebug("New router instance successfully created.");
 
-            return new Router(ipcServerStream, tcpServerStream, Logger);
+            return new Router(ipcServerStream, tcpServerStream, _logger);
         }
+    }
+
+    /// <summary>
+    /// This class creates IPC Server - TCP Client router instances.
+    /// Supports NamedPipes/UnixDomainSocket server and TCP/IP client.
+    /// </summary>
+    internal class IpcServerTcpClientRouterFactory : DiagnosticsServerRouterFactory
+    {
+        ILogger _logger;
+        IpcServerRouterFactory _ipcServerRouterFactory;
+        TcpClientRouterFactory _tcpClientRouterFactory;
 
-        protected async Task<Stream> AcceptIpcStreamAsync(CancellationToken token)
+        public IpcServerTcpClientRouterFactory(string ipcServer, string tcpClient, int runtimeTimeoutMs, ILogger logger)
         {
-            Stream ipcServerStream = null;
+            _logger = logger;
+            _ipcServerRouterFactory = new IpcServerRouterFactory(ipcServer, logger);
+            _tcpClientRouterFactory = new TcpClientRouterFactory(tcpClient, runtimeTimeoutMs, logger);
+        }
 
-            Logger.LogDebug($"Waiting for new ipc connection at endpoint \"{_ipcServerPath}\".");
+        public override string IpcAddress
+        {
+            get
+            {
+                return _ipcServerRouterFactory.IpcServerPath;
+            }
+        }
 
+        public override string TcpAddress
+        {
+            get
+            {
+                return _tcpClientRouterFactory.TcpClientAddress;
+            }
+        }
 
-            using var connectTimeoutTokenSource = new CancellationTokenSource();
-            using var connectTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, connectTimeoutTokenSource.Token);
+        public override ILogger Logger
+        {
+            get
+            {
+                return _logger;
+            }
+        }
+
+        public override void Start()
+        {
+            _ipcServerRouterFactory.Start();
+            _logger?.LogInformation($"Starting IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> TCP client ({_tcpClientRouterFactory.TcpClientAddress}) router.");
+        }
+
+        public override Task Stop()
+        {
+            _ipcServerRouterFactory.Stop();
+            return Task.CompletedTask;
+        }
+
+        public override async Task<Router> CreateRouterAsync(CancellationToken token)
+        {
+            Stream tcpClientStream = null;
+            Stream ipcServerStream = null;
+
+            _logger?.LogDebug("Trying to create a new router instance.");
 
             try
             {
-                connectTimeoutTokenSource.CancelAfter(IpcServerTimeoutMs);
-                ipcServerStream = await _ipcServer.AcceptAsync(connectTokenSource.Token).ConfigureAwait(false);
+                using CancellationTokenSource cancelRouter = CancellationTokenSource.CreateLinkedTokenSource(token);
+
+                // Get new server endpoint.
+                ipcServerStream = await _ipcServerRouterFactory.AcceptIpcStreamAsync(cancelRouter.Token).ConfigureAwait(false);
+
+                // Get new client endpoint.
+                using var tcpClientStreamTask = _tcpClientRouterFactory.ConnectTcpStreamAsync(cancelRouter.Token);
+
+                // We have a valid ipc stream and a pending tcp stream. Wait for completion
+                // or disconnect of ipc stream.
+                using var checkIpcStreamTask = IsStreamConnectedAsync(ipcServerStream, cancelRouter.Token);
+
+                // Wait for at least completion of one task.
+                await Task.WhenAny(tcpClientStreamTask, checkIpcStreamTask).ConfigureAwait(false);
+
+                // Cancel out any pending tasks not yet completed.
+                cancelRouter.Cancel();
+
+                try
+                {
+                    await Task.WhenAll(tcpClientStreamTask, checkIpcStreamTask).ConfigureAwait(false);
+                }
+                catch (Exception)
+                {
+                    // Check if we have an accepted tcp stream.
+                    if (IsCompletedSuccessfully(tcpClientStreamTask))
+                        tcpClientStreamTask.Result?.Dispose();
+
+                    if (checkIpcStreamTask.IsFaulted)
+                    {
+                        _logger?.LogInformation("Broken ipc connection detected, aborting tcp connection.");
+                        checkIpcStreamTask.GetAwaiter().GetResult();
+                    }
+
+                    throw;
+                }
+
+                tcpClientStream = tcpClientStreamTask.Result;
             }
             catch (Exception)
             {
-                ipcServerStream?.Dispose();
+                _logger?.LogDebug("Failed creating new router instance.");
 
-                if (connectTimeoutTokenSource.IsCancellationRequested)
-                {
-                    Logger.LogDebug("No ipc stream connected, timing out.");
-                    throw new TimeoutException();
-                }
+                // Cleanup and rethrow.
+                ipcServerStream?.Dispose();
+                tcpClientStream?.Dispose();
 
                 throw;
             }
 
-            if (ipcServerStream != null)
-                Logger.LogDebug("Successfully connected ipc stream.");
-
-            return ipcServerStream;
-        }
+            // Create new router.
+            _logger?.LogDebug("New router instance successfully created.");
 
-        public void CreatedNewServer(EndPoint localEP)
-        {
-            if (localEP is IPEndPoint ipEP)
-                _tcpServerAddress = _tcpServerAddress.Replace(":0", string.Format(":{0}", ipEP.Port));
+            return new Router(ipcServerStream, tcpClientStream, _logger);
         }
     }
 
@@ -498,25 +899,57 @@ namespace Microsoft.Diagnostics.NETCore.Client
     /// This class creates IPC Client - TCP Server router instances.
     /// Supports NamedPipes/UnixDomainSocket client and TCP/IP server.
     /// </summary>
-    internal class IpcClientTcpServerRouterFactory : TcpServerRouterFactory, IIpcServerTransportCallbackInternal
+    internal class IpcClientTcpServerRouterFactory : DiagnosticsServerRouterFactory
     {
-        readonly string _ipcClientPath;
+        ILogger _logger;
+        IpcClientRouterFactory _ipcClientRouterFactory;
+        TcpServerRouterFactory _tcpServerRouterFactory;
+
+        public IpcClientTcpServerRouterFactory(string ipcClient, string tcpServer, int runtimeTimeoutMs, ILogger logger)
+        {
+            _logger = logger;
+            _ipcClientRouterFactory = new IpcClientRouterFactory(ipcClient, logger);
+            _tcpServerRouterFactory = new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger);
+        }
 
-        protected int IpcClientTimeoutMs { get; set; } = Timeout.Infinite;
+        public override string IpcAddress
+        {
+            get
+            {
+                return _ipcClientRouterFactory.IpcClientPath;
+            }
+        }
 
-        protected int IpcClientRetryTimeoutMs { get; set; } = 500;
+        public override string TcpAddress
+        {
+            get
+            {
+                return _tcpServerRouterFactory.TcpServerAddress;
+            }
+        }
 
-        public IpcClientTcpServerRouterFactory(string ipcClient, string tcpServer, int runtimeTimeoutMs, ILogger logger)
-            : base(tcpServer, runtimeTimeoutMs, logger)
+        public override ILogger Logger
         {
-            _ipcClientPath = ipcClient;
-            _tcpServer.TransportCallback = this;
+            get
+            {
+                return _logger;
+            }
         }
 
         public override void Start()
         {
-            base.Start();
-            _logger.LogInformation($"Starting IPC client ({_ipcClientPath}) <--> TCP server ({_tcpServerAddress}) router.");
+            _tcpServerRouterFactory.Start();
+            _logger?.LogInformation($"Starting IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router.");
+        }
+
+        public override Task Stop()
+        {
+            return _tcpServerRouterFactory.Stop();
+        }
+
+        public override void Reset()
+        {
+            _tcpServerRouterFactory.Reset();
         }
 
         public override async Task<Router> CreateRouterAsync(CancellationToken token)
@@ -524,17 +957,17 @@ namespace Microsoft.Diagnostics.NETCore.Client
             Stream tcpServerStream = null;
             Stream ipcClientStream = null;
 
-            Logger.LogDebug("Trying to create a new router instance.");
+            _logger?.LogDebug("Trying to create a new router instance.");
 
             try
             {
                 using CancellationTokenSource cancelRouter = CancellationTokenSource.CreateLinkedTokenSource(token);
 
                 // Get new server endpoint.
-                tcpServerStream = await AcceptTcpStreamAsync(cancelRouter.Token).ConfigureAwait(false);
+                tcpServerStream = await _tcpServerRouterFactory.AcceptTcpStreamAsync(cancelRouter.Token).ConfigureAwait(false);
 
                 // Get new client endpoint.
-                using var ipcClientStreamTask = ConnectIpcStreamAsync(cancelRouter.Token);
+                using var ipcClientStreamTask = _ipcClientRouterFactory.ConnectIpcStreamAsync(cancelRouter.Token);
 
                 // We have a valid tcp stream and a pending ipc stream. Wait for completion
                 // or disconnect of tcp stream.
@@ -558,7 +991,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
 
                     if (checkTcpStreamTask.IsFaulted)
                     {
-                        Logger.LogInformation("Broken tcp connection detected, aborting ipc connection.");
+                        _logger?.LogInformation("Broken tcp connection detected, aborting ipc connection.");
                         checkTcpStreamTask.GetAwaiter().GetResult();
                     }
 
@@ -566,122 +999,33 @@ namespace Microsoft.Diagnostics.NETCore.Client
                 }
 
                 ipcClientStream = ipcClientStreamTask.Result;
-            }
-            catch (Exception)
-            {
-                Logger.LogDebug("Failed creating new router instance.");
-
-                // Cleanup and rethrow.
-                tcpServerStream?.Dispose();
-                ipcClientStream?.Dispose();
-
-                throw;
-            }
-
-            // Create new router.
-            Logger.LogDebug("New router instance successfully created.");
-
-            return new Router(ipcClientStream, tcpServerStream, Logger, (ulong)IpcAdvertise.V1SizeInBytes);
-        }
-
-        protected async Task<Stream> ConnectIpcStreamAsync(CancellationToken token)
-        {
-            Stream ipcClientStream = null;
-
-            Logger.LogDebug($"Connecting new ipc endpoint \"{_ipcClientPath}\".");
-
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            {
-                var namedPipe = new NamedPipeClientStream(
-                    ".",
-                    _ipcClientPath,
-                    PipeDirection.InOut,
-                    PipeOptions.Asynchronous,
-                    TokenImpersonationLevel.Impersonation);
 
                 try
                 {
-                    await namedPipe.ConnectAsync(IpcClientTimeoutMs, token).ConfigureAwait(false);
+                    // TcpServer consumes advertise message, needs to be replayed back to ipc client stream. Use router process ID as representation.
+                    await IpcAdvertise.SerializeAsync(ipcClientStream, _tcpServerRouterFactory.RuntimeInstanceId, (ulong)Process.GetCurrentProcess().Id, token).ConfigureAwait(false);
                 }
-                catch (Exception ex)
+                catch (Exception)
                 {
-                    namedPipe?.Dispose();
-
-                    if (ex is TimeoutException)
-                        Logger.LogDebug("No ipc stream connected, timing out.");
-
+                    _logger?.LogDebug("Failed sending advertise message.");
                     throw;
                 }
-
-                ipcClientStream = namedPipe;
-            }
-            else
-            {
-                bool retry = false;
-                IpcUnixDomainSocket unixDomainSocket;
-                do
-                {
-                    unixDomainSocket = new IpcUnixDomainSocket();
-
-                    using var connectTimeoutTokenSource = new CancellationTokenSource();
-                    using var connectTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, connectTimeoutTokenSource.Token);
-
-                    try
-                    {
-                        connectTimeoutTokenSource.CancelAfter(IpcClientTimeoutMs);
-                        await unixDomainSocket.ConnectAsync(new IpcUnixDomainSocketEndPoint(_ipcClientPath), token).ConfigureAwait(false);
-                        retry = false;
-                    }
-                    catch (Exception)
-                    {
-                        unixDomainSocket?.Dispose();
-
-                        if (connectTimeoutTokenSource.IsCancellationRequested)
-                        {
-                            Logger.LogDebug("No ipc stream connected, timing out.");
-                            throw new TimeoutException();
-                        }
-
-                        Logger.LogTrace($"Failed connecting {_ipcClientPath}, wait {IpcClientRetryTimeoutMs} ms before retrying.");
-
-                        // If we get an error (without hitting timeout above), most likely due to unavailable listener.
-                        // Delay execution to prevent to rapid retry attempts.
-                        await Task.Delay(IpcClientRetryTimeoutMs, token).ConfigureAwait(false);
-
-                        if (IpcClientTimeoutMs != Timeout.Infinite)
-                            throw;
-
-                        retry = true;
-                    }
-                }
-                while (retry);
-
-                ipcClientStream = new ExposedSocketNetworkStream(unixDomainSocket, ownsSocket: true);
-            }
-
-            try
-            {
-                // ReversedDiagnosticsServer consumes advertise message, needs to be replayed back to ipc client stream. Use router process ID as representation.
-                await IpcAdvertise.SerializeAsync(ipcClientStream, RuntimeInstanceId, (ulong)Process.GetCurrentProcess().Id, token).ConfigureAwait(false);
             }
             catch (Exception)
             {
-                Logger.LogDebug("Failed sending advertise message.");
+                _logger?.LogDebug("Failed creating new router instance.");
 
+                // Cleanup and rethrow.
+                tcpServerStream?.Dispose();
                 ipcClientStream?.Dispose();
+
                 throw;
             }
 
-            if (ipcClientStream != null)
-                Logger.LogDebug("Successfully connected ipc stream.");
-
-            return ipcClientStream;
-        }
+            // Create new router.
+            _logger?.LogDebug("New router instance successfully created.");
 
-        public void CreatedNewServer(EndPoint localEP)
-        {
-            if (localEP is IPEndPoint ipEP)
-                _tcpServerAddress = _tcpServerAddress.Replace(":0", string.Format(":{0}", ipEP.Port));
+            return new Router(ipcClientStream, tcpServerStream, _logger, (ulong)IpcAdvertise.V1SizeInBytes);
         }
     }
 
@@ -782,8 +1126,8 @@ namespace Microsoft.Diagnostics.NETCore.Client
 
                 Interlocked.Decrement(ref s_routerInstanceCount);
 
-                _logger.LogTrace($"Diposed stats: Backend->Frontend {_backendToFrontendByteTransfer} bytes, Frontend->Backend {_frontendToBackendByteTransfer} bytes.");
-                _logger.LogTrace($"Active instances: {s_routerInstanceCount}");
+                _logger?.LogTrace($"Diposed stats: Backend->Frontend {_backendToFrontendByteTransfer} bytes, Frontend->Backend {_frontendToBackendByteTransfer} bytes.");
+                _logger?.LogTrace($"Active instances: {s_routerInstanceCount}");
             }
         }
 
@@ -794,27 +1138,27 @@ namespace Microsoft.Diagnostics.NETCore.Client
                 byte[] buffer = new byte[1024];
                 while (!token.IsCancellationRequested)
                 {
-                    _logger.LogTrace("Start reading bytes from backend.");
+                    _logger?.LogTrace("Start reading bytes from backend.");
 
                     int bytesRead = await _backendStream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
 
-                    _logger.LogTrace($"Read {bytesRead} bytes from backend.");
+                    _logger?.LogTrace($"Read {bytesRead} bytes from backend.");
 
                     // Check for end of stream indicating that remote end hung-up.
                     if (bytesRead == 0)
                     {
-                        _logger.LogTrace("Backend hung up.");
+                        _logger?.LogTrace("Backend hung up.");
                         break;
                     }
 
                     _backendToFrontendByteTransfer += (ulong)bytesRead;
 
-                    _logger.LogTrace($"Start writing {bytesRead} bytes to frontend.");
+                    _logger?.LogTrace($"Start writing {bytesRead} bytes to frontend.");
 
                     await _frontendStream.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false);
                     await _frontendStream.FlushAsync().ConfigureAwait(false);
 
-                    _logger.LogTrace($"Wrote {bytesRead} bytes to frontend.");
+                    _logger?.LogTrace($"Wrote {bytesRead} bytes to frontend.");
                 }
             }
             catch (Exception)
@@ -822,7 +1166,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
                 // Completing task will trigger dispose of instance and cleanup.
                 // Faliure mainly consists of closed/disposed streams and cancelation requests.
                 // Just make sure task gets complete, nothing more needs to be in response to these exceptions.
-                _logger.LogTrace("Failed stream operation. Completing task.");
+                _logger?.LogTrace("Failed stream operation. Completing task.");
             }
 
             RouterTaskCompleted?.TrySetResult(true);
@@ -835,27 +1179,27 @@ namespace Microsoft.Diagnostics.NETCore.Client
                 byte[] buffer = new byte[1024];
                 while (!token.IsCancellationRequested)
                 {
-                    _logger.LogTrace("Start reading bytes from frotend.");
+                    _logger?.LogTrace("Start reading bytes from frotend.");
 
                     int bytesRead = await _frontendStream.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
 
-                    _logger.LogTrace($"Read {bytesRead} bytes from frontend.");
+                    _logger?.LogTrace($"Read {bytesRead} bytes from frontend.");
 
                     // Check for end of stream indicating that remote end hung-up.
                     if (bytesRead == 0)
                     {
-                        _logger.LogTrace("Frontend hung up.");
+                        _logger?.LogTrace("Frontend hung up.");
                         break;
                     }
 
                     _frontendToBackendByteTransfer += (ulong)bytesRead;
 
-                    _logger.LogTrace($"Start writing {bytesRead} bytes to backend.");
+                    _logger?.LogTrace($"Start writing {bytesRead} bytes to backend.");
 
                     await _backendStream.WriteAsync(buffer, 0, bytesRead, token).ConfigureAwait(false);
                     await _backendStream.FlushAsync().ConfigureAwait(false);
 
-                    _logger.LogTrace($"Wrote {bytesRead} bytes to backend.");
+                    _logger?.LogTrace($"Wrote {bytesRead} bytes to backend.");
                 }
             }
             catch (Exception)
@@ -863,7 +1207,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
                 // Completing task will trigger dispose of instance and cleanup.
                 // Faliure mainly consists of closed/disposed streams and cancelation requests.
                 // Just make sure task gets complete, nothing more needs to be in response to these exceptions.
-                _logger.LogTrace("Failed stream operation. Completing task.");
+                _logger?.LogTrace("Failed stream operation. Completing task.");
             }
 
             RouterTaskCompleted?.TrySetResult(true);
index 1625c18db5629d15c0320101729c38b05b546c85..c4fdd81b2c90d8f86753320e5d420383736f10a2 100644 (file)
@@ -18,7 +18,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
     {
         internal interface Callbacks
         {
-            void OnRouterStarted(string boundTcpServerAddress);
+            void OnRouterStarted(string tcpAddress);
             void OnRouterStopped();
         }
 
@@ -32,6 +32,11 @@ namespace Microsoft.Diagnostics.NETCore.Client
             return await runRouter(token, new IpcServerTcpServerRouterFactory(ipcServer, tcpServer, runtimeTimeoutMs, logger), callbacks).ConfigureAwait(false);
         }
 
+        public static async Task<int> runIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeoutMs, ILogger logger, Callbacks callbacks)
+        {
+            return await runRouter(token, new IpcServerTcpClientRouterFactory(ipcServer, tcpClient, runtimeTimeoutMs, logger), callbacks).ConfigureAwait(false);
+        }
+
         public static bool isLoopbackOnly(string address)
         {
             bool isLooback = false;
@@ -46,7 +51,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
             return isLooback;
         }
 
-        async static Task<int> runRouter(CancellationToken token, TcpServerRouterFactory routerFactory, Callbacks callbacks)
+        async static Task<int> runRouter(CancellationToken token, DiagnosticsServerRouterFactory routerFactory, Callbacks callbacks)
         {
             List<Task> runningTasks = new List<Task>();
             List<Router> runningRouters = new List<Router>();
@@ -54,7 +59,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
             try
             {
                 routerFactory.Start();
-                callbacks?.OnRouterStarted(routerFactory.TcpServerAddress);
+                callbacks?.OnRouterStarted(routerFactory.TcpAddress);
 
                 while (!token.IsCancellationRequested)
                 {
@@ -110,7 +115,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
                         // reconnect using same or different runtime instance.
                         if (ex is BackendStreamTimeoutException && runningRouters.Count == 0)
                         {
-                            routerFactory.Logger.LogDebug("No backend stream available before timeout.");
+                            routerFactory.Logger?.LogDebug("No backend stream available before timeout.");
                             routerFactory.Reset();
                         }
 
@@ -118,8 +123,8 @@ namespace Microsoft.Diagnostics.NETCore.Client
                         // Shutdown router to prevent instances to outlive runtime process (if auto shutdown is enabled).
                         if (ex is RuntimeTimeoutException)
                         {
-                            routerFactory.Logger.LogInformation("No runtime connected before timeout.");
-                            routerFactory.Logger.LogInformation("Starting automatic shutdown.");
+                            routerFactory.Logger?.LogInformation("No runtime connected before timeout.");
+                            routerFactory.Logger?.LogInformation("Starting automatic shutdown.");
                             throw;
                         }
                     }
@@ -127,12 +132,12 @@ namespace Microsoft.Diagnostics.NETCore.Client
             }
             catch (Exception ex)
             {
-                routerFactory.Logger.LogInformation($"Shutting down due to error: {ex.Message}");
+                routerFactory.Logger?.LogInformation($"Shutting down due to error: {ex.Message}");
             }
             finally
             {
                 if (token.IsCancellationRequested)
-                    routerFactory.Logger.LogInformation("Shutting down due to cancelation request.");
+                    routerFactory.Logger?.LogInformation("Shutting down due to cancelation request.");
 
                 runningRouters.RemoveAll(IsRouterDead);
                 runningRouters.Clear();
@@ -140,7 +145,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
                 await routerFactory?.Stop();
                 callbacks?.OnRouterStopped();
 
-                routerFactory.Logger.LogInformation("Router stopped.");
+                routerFactory.Logger?.LogInformation("Router stopped.");
             }
             return 0;
         }
index 47520d44f43bfdc3ca3634a208e59b8bf889084e..c51639f3d85cc2d3077e833ea22e1443fafdcdd4 100644 (file)
@@ -16,13 +16,14 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
     {
         public CancellationToken CommandToken { get; set; }
         public bool SuspendProcess { get; set; }
+        public bool ConnectMode { get; set; }
         public bool Verbose { get; set; }
 
-        public void OnRouterStarted(string boundTcpServerAddress)
+        public void OnRouterStarted(string tcpAddress)
         {
             if (ProcessLauncher.Launcher.HasChildProc)
             {
-                string diagnosticPorts = boundTcpServerAddress + (SuspendProcess ? ",suspend" : ",nosuspend");
+                string diagnosticPorts = tcpAddress + (SuspendProcess ? ",suspend" : ",nosuspend") + (ConnectMode ? ",connect" : ",listen");
                 if (ProcessLauncher.Launcher.ChildProc.StartInfo.Arguments.Contains("${DOTNET_DiagnosticPorts}", StringComparison.OrdinalIgnoreCase))
                 {
                     ProcessLauncher.Launcher.ChildProc.StartInfo.Arguments = ProcessLauncher.Launcher.ChildProc.StartInfo.Arguments.Replace("${DOTNET_DiagnosticPorts}", diagnosticPorts);
@@ -64,6 +65,7 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             factory.AddConsole(logLevel, false);
 
             Launcher.SuspendProcess = true;
+            Launcher.ConnectMode = true;
             Launcher.Verbose = logLevel != LogLevel.Information;
             Launcher.CommandToken = token;
 
@@ -105,7 +107,8 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             using var factory = new LoggerFactory();
             factory.AddConsole(logLevel, false);
 
-            Launcher.SuspendProcess = true;
+            Launcher.SuspendProcess = false;
+            Launcher.ConnectMode = true;
             Launcher.Verbose = logLevel != LogLevel.Information;
             Launcher.CommandToken = token;
 
@@ -131,6 +134,47 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             return routerTask.Result;
         }
 
+        public async Task<int> RunIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeout, string verbose)
+        {
+            using CancellationTokenSource cancelRouterTask = new CancellationTokenSource();
+            using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token);
+
+            LogLevel logLevel = LogLevel.Information;
+            if (string.Compare(verbose, "debug", StringComparison.OrdinalIgnoreCase) == 0)
+                logLevel = LogLevel.Debug;
+            else if (string.Compare(verbose, "trace", StringComparison.OrdinalIgnoreCase) == 0)
+                logLevel = LogLevel.Trace;
+
+            using var factory = new LoggerFactory();
+            factory.AddConsole(logLevel, false);
+
+            Launcher.SuspendProcess = false;
+            Launcher.ConnectMode = false;
+            Launcher.Verbose = logLevel != LogLevel.Information;
+            Launcher.CommandToken = token;
+
+            var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpClientRouter(linkedCancelToken.Token, ipcServer, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, factory.CreateLogger("dotnet-dsrounter"), Launcher);
+
+            while (!linkedCancelToken.IsCancellationRequested)
+            {
+                await Task.WhenAny(routerTask, Task.Delay(250)).ConfigureAwait(false);
+                if (routerTask.IsCompleted)
+                    break;
+
+                if (!Console.IsInputRedirected && Console.KeyAvailable)
+                {
+                    ConsoleKey cmd = Console.ReadKey(true).Key;
+                    if (cmd == ConsoleKey.Q)
+                    {
+                        cancelRouterTask.Cancel();
+                        break;
+                    }
+                }
+            }
+
+            return routerTask.Result;
+        }
+
         static void checkLoopbackOnly(string tcpServer)
         {
             if (!string.IsNullOrEmpty(tcpServer) && !DiagnosticsServerRouterRunner.isLoopbackOnly(tcpServer))
index a186bec91beeb861fc3134befa823beb8dcb5324..a56da4fbb8da4ad735b42eaf3c2d475cbeb3da3b 100644 (file)
@@ -19,6 +19,7 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
     {
         delegate Task<int> DiagnosticsServerIpcClientTcpServerRouterDelegate(CancellationToken ct, string ipcClient, string tcpServer, int runtimeTimeoutS, string verbose);
         delegate Task<int> DiagnosticsServerIpcServerTcpServerRouterDelegate(CancellationToken ct, string ipcServer, string tcpServer, int runtimeTimeoutS, string verbose);
+        delegate Task<int> DiagnosticsServerIpcServerTcpClientRouterDelegate(CancellationToken ct, string ipcServer, string tcpClient, int runtimeTimeoutS, string verbose);
 
         private static Command IpcClientTcpServerRouterCommand() =>
             new Command(
@@ -41,11 +42,24 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
                                 "and a TCP/IP server (accepting runtime TCP client).")
             {
                 // Handler
-                HandlerDescriptor.FromDelegate((DiagnosticsServerIpcClientTcpServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerTcpServerRouter).GetCommandHandler(),
+                HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerTcpServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerTcpServerRouter).GetCommandHandler(),
                 // Options
                 IpcServerAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption()
             };
 
+        private static Command IpcServerTcpClientRouterCommand() =>
+            new Command(
+                name: "server-client",
+                description: "Start a .NET application Diagnostics Server routing local IPC client <--> remote TCP server. " +
+                                "Router is configured using an IPC server (connecting to by diagnostic tools) " +
+                                "and a TCP/IP client (connecting runtime TCP server).")
+            {
+                // Handler
+                HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerTcpClientRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerTcpClientRouter).GetCommandHandler(),
+                // Options
+                IpcServerAddressOption(), TcpClientAddressOption(), RuntimeTimeoutOption(), VerboseOption()
+            };
+
         private static Option IpcClientAddressOption() =>
             new Option(
                 aliases: new[] { "--ipc-client", "-ipcc" },
@@ -66,6 +80,16 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
                 Argument = new Argument<string>(name: "ipcServer", getDefaultValue: () => "")
             };
 
+        private static Option TcpClientAddressOption() =>
+            new Option(
+                aliases: new[] { "--tcp-client", "-tcpc" },
+                description: "The runtime TCP/IP address using format [host]:[port]. " +
+                                "Router can can connect 127.0.0.1, [::1], ipv4 address, ipv6 address, hostname addresses." +
+                                "Launch runtime using DOTNET_DiagnosticPorts environment variable to setup listener")
+            {
+                Argument = new Argument<string>(name: "tcpClient", getDefaultValue: () => "")
+            };
+
         private static Option TcpServerAddressOption() =>
             new Option(
                 aliases: new[] { "--tcp-server", "-tcps" },
@@ -106,6 +130,7 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             var parser = new CommandLineBuilder()
                 .AddCommand(IpcClientTcpServerRouterCommand())
                 .AddCommand(IpcServerTcpServerRouterCommand())
+                .AddCommand(IpcServerTcpClientRouterCommand())
                 .UseDefaults()
                 .Build();