From: Johan Lorensson Date: Wed, 12 May 2021 09:41:55 +0000 (+0200) Subject: Add support for server-client mode in dsrouter. (#2251) X-Git-Tag: submit/tizen/20210909.063632~15^2~37 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=33cf8c96e565f243c17eca80bb8670d9ed8672c2;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Add support for server-client mode in dsrouter. (#2251) --- diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index f0e3d9f26..967d5470c 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -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 /// 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(), 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 + } } /// /// This class represent a TCP/IP server endpoint used when building up router instances. /// - 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 AcceptTcpStreamAsync(CancellationToken token) + public async Task 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) + /// + /// This class represent a TCP/IP client endpoint used when building up router instances. + /// + 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 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(), 0, System.Net.Sockets.SocketFlags.None); + Func 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 + } + } + } + + /// + /// This class represent a IPC server endpoint used when building up router instances. + /// + 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 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; + /// + /// This class represent a IPC client endpoint used when building up router instances. + /// + 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 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. /// - 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 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); } + } + + /// + /// This class creates IPC Server - TCP Client router instances. + /// Supports NamedPipes/UnixDomainSocket server and TCP/IP client. + /// + internal class IpcServerTcpClientRouterFactory : DiagnosticsServerRouterFactory + { + ILogger _logger; + IpcServerRouterFactory _ipcServerRouterFactory; + TcpClientRouterFactory _tcpClientRouterFactory; - protected async Task 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 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. /// - 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 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 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); diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs index 1625c18db..c4fdd81b2 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs @@ -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 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 runRouter(CancellationToken token, TcpServerRouterFactory routerFactory, Callbacks callbacks) + async static Task runRouter(CancellationToken token, DiagnosticsServerRouterFactory routerFactory, Callbacks callbacks) { List runningTasks = new List(); List runningRouters = new List(); @@ -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; } diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 47520d44f..c51639f3d 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -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 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)) diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index a186bec91..a56da4fbb 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -19,6 +19,7 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter { delegate Task DiagnosticsServerIpcClientTcpServerRouterDelegate(CancellationToken ct, string ipcClient, string tcpServer, int runtimeTimeoutS, string verbose); delegate Task DiagnosticsServerIpcServerTcpServerRouterDelegate(CancellationToken ct, string ipcServer, string tcpServer, int runtimeTimeoutS, string verbose); + delegate Task 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(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(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();