diff --git a/diagnostics.sln b/diagnostics.sln index dda545db55..203e7bc89e 100644 --- a/diagnostics.sln +++ b/diagnostics.sln @@ -200,6 +200,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Debug EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExitCodeTracee", "src\tests\ExitCodeTracee\ExitCodeTracee.csproj", "{61F73DD0-F346-4D7A-AB12-63415B4EEEE1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-dsrouter", "src\Tools\dotnet-dsrouter\dotnet-dsrouter.csproj", "{2BD55143-B102-4FEC-84AD-DC3B330FA9AC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Checked|Any CPU = Checked|Any CPU @@ -1617,6 +1619,46 @@ Global {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|x64.Build.0 = Release|Any CPU {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU {61F73DD0-F346-4D7A-AB12-63415B4EEEE1}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|Any CPU.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|ARM.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|ARM.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|ARM64.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|x64.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|x64.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|x86.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Checked|x86.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|ARM.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|ARM.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|ARM64.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|x64.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|x64.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|x86.ActiveCfg = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Debug|x86.Build.0 = Debug|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|Any CPU.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|ARM.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|ARM.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|ARM64.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|ARM64.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|x64.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|x64.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|x86.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.Release|x86.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC}.RelWithDebInfo|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1668,6 +1710,7 @@ Global {5FC66A16-41E9-4D22-A44C-FEBB7DCCAAF8} = {19FAB78C-3351-4911-8F0C-8C6056401740} {064BC7DD-D44C-400E-9215-7546E092AB98} = {03479E19-3F18-49A6-910A-F5041E27E7C0} {61F73DD0-F346-4D7A-AB12-63415B4EEEE1} = {03479E19-3F18-49A6-910A-F5041E27E7C0} + {2BD55143-B102-4FEC-84AD-DC3B330FA9AC} = {B62728C8-1267-4043-B46F-5537BBAEC692} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcAdvertise.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcAdvertise.cs index fbf11dbb40..4aa3432d0d 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcAdvertise.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcAdvertise.cs @@ -37,6 +37,8 @@ private IpcAdvertise(byte[] magic, Guid cookie, UInt64 pid, UInt16 future) RuntimeInstanceCookie = cookie; } + public static int V1SizeInBytes { get; } = IpcAdvertiseV1SizeInBytes; + public static async Task ParseAsync(Stream stream, CancellationToken token) { byte[] buffer = new byte[IpcAdvertiseV1SizeInBytes]; @@ -78,6 +80,28 @@ public static async Task ParseAsync(Stream stream, CancellationTok return new IpcAdvertise(magic, cookie, pid, future); } + public static async Task SerializeAsync(Stream stream, Guid runtimeInstanceCookie, ulong processId, CancellationToken token) + { + int index = 0; + byte[] buffer = new byte[IpcAdvertiseV1SizeInBytes]; + + Array.Copy(Magic_V1, buffer, Magic_V1.Length); + index += Magic_V1.Length; + + byte[] cookieBuffer = runtimeInstanceCookie.ToByteArray(); + Array.Copy(cookieBuffer, 0, buffer, index, cookieBuffer.Length); + index += cookieBuffer.Length; + + Array.Copy(BitConverter.GetBytes(processId), 0, buffer, index, sizeof(ulong)); + index += sizeof(ulong); + + short future = 0; + Array.Copy(BitConverter.GetBytes(future), 0, buffer, index, sizeof(short)); + index += sizeof(short); + + await stream.WriteAsync(buffer, 0, index).ConfigureAwait(false); + } + public override string ToString() { return $"{{ Magic={Magic}; ClrInstanceId={RuntimeInstanceCookie}; ProcessId={ProcessId}; Future={Future} }}"; diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcServerTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcServerTransport.cs index 9421783908..21168c7b35 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcServerTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcServerTransport.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.IO.Pipes; +using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Threading; @@ -17,18 +18,30 @@ internal abstract class IpcServerTransport : IDisposable private IIpcServerTransportCallbackInternal _callback; private bool _disposed; - public static IpcServerTransport Create(string transportPath, int maxConnections) + public static IpcServerTransport Create(string address, int maxConnections, bool enableTcpIpProtocol, IIpcServerTransportCallbackInternal transportCallback = null) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!enableTcpIpProtocol || !IpcTcpSocketEndPoint.IsTcpIpEndPoint(address)) { - return new WindowsPipeServerTransport(transportPath, maxConnections); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return new IpcWindowsNamedPipeServerTransport(address, maxConnections, transportCallback); + } + else + { + return new IpcUnixDomainSocketServerTransport(address, maxConnections, transportCallback); + } } else { - return new UnixDomainSocketServerTransport(transportPath, maxConnections); + return new IpcTcpSocketServerTransport(address, maxConnections, transportCallback); } } + protected IpcServerTransport(IIpcServerTransportCallbackInternal transportCallback = null) + { + _callback = transportCallback; + } + public void Dispose() { if (!_disposed) @@ -45,18 +58,10 @@ protected virtual void Dispose(bool disposing) public abstract Task AcceptAsync(CancellationToken token); - public static int MaxAllowedConnections - { + public static int MaxAllowedConnections { get { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return NamedPipeServerStream.MaxAllowedServerInstances; - } - else - { - return (int)SocketOptionName.MaxConnections; - } + return -1; } } @@ -73,13 +78,13 @@ internal void SetCallback(IIpcServerTransportCallbackInternal callback) _callback = callback; } - protected void OnCreateNewServer() + protected void OnCreateNewServer(EndPoint localEP) { - _callback?.CreatedNewServer(); + _callback?.CreatedNewServer(localEP); } } - internal sealed class WindowsPipeServerTransport : IpcServerTransport + internal sealed class IpcWindowsNamedPipeServerTransport : IpcServerTransport { private const string PipePrefix = @"\\.\pipe\"; @@ -89,11 +94,12 @@ internal sealed class WindowsPipeServerTransport : IpcServerTransport private readonly string _pipeName; private readonly int _maxInstances; - public WindowsPipeServerTransport(string pipeName, int maxInstances) + public IpcWindowsNamedPipeServerTransport(string pipeName, int maxInstances, IIpcServerTransportCallbackInternal transportCallback = null) + : base(transportCallback) { - _maxInstances = maxInstances; + _maxInstances = maxInstances != MaxAllowedConnections ? maxInstances : NamedPipeServerStream.MaxAllowedServerInstances; _pipeName = pipeName.StartsWith(PipePrefix) ? pipeName.Substring(PipePrefix.Length) : pipeName; - CreateNewPipeServer(); + _stream = CreateNewNamedPipeServer(_pipeName, _maxInstances); } protected override void Dispose(bool disposing) @@ -113,50 +119,50 @@ public override async Task AcceptAsync(CancellationToken token) VerifyNotDisposed(); using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancellation.Token); - - NamedPipeServerStream connectedStream; try { + // Connect client to named pipe server stream. await _stream.WaitForConnectionAsync(linkedSource.Token).ConfigureAwait(false); - connectedStream = _stream; + // Transfer ownership of connected named pipe. + var connectedStream = _stream; + + // Setup new named pipe server stream used in upcomming accept calls. + _stream = CreateNewNamedPipeServer(_pipeName, _maxInstances); + + return connectedStream; } - finally + catch (Exception) { - if (!_cancellation.IsCancellationRequested) + // Keep named pipe server stream when getting any kind of cancel request. + // Cancel happens when complete transport is about to disposed or caller + // cancels out specific accept call, no need to recycle named pipe server stream. + // In all other exception scenarios named pipe server stream will be re-created. + if (!linkedSource.IsCancellationRequested) { - CreateNewPipeServer(); + _stream.Dispose(); + _stream = CreateNewNamedPipeServer(_pipeName, _maxInstances); } + throw; } - return connectedStream; } - private void CreateNewPipeServer() + private NamedPipeServerStream CreateNewNamedPipeServer(string pipeName, int maxInstances) { - _stream = new NamedPipeServerStream( - _pipeName, - PipeDirection.InOut, - _maxInstances, - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous); - OnCreateNewServer(); + var stream = new NamedPipeServerStream(pipeName, PipeDirection.InOut, maxInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + OnCreateNewServer(null); + return stream; } } - internal sealed class UnixDomainSocketServerTransport : IpcServerTransport + internal abstract class IpcSocketServerTransport : IpcServerTransport { private readonly CancellationTokenSource _cancellation = new CancellationTokenSource(); - private readonly int _backlog; - private readonly string _path; + protected IpcSocket _socket; - private UnixDomainSocket _socket; - - public UnixDomainSocketServerTransport(string path, int backlog) + protected IpcSocketServerTransport(IIpcServerTransportCallbackInternal transportCallback = null) + : base(transportCallback) { - _backlog = backlog; - _path = path; - - CreateNewSocketServer(); } protected override void Dispose(bool disposing) @@ -187,33 +193,96 @@ public override async Task AcceptAsync(CancellationToken token) using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancellation.Token); try { - Socket socket = await _socket.AcceptAsync(linkedSource.Token).ConfigureAwait(false); + // Accept next client socket. + var socket = await _socket.AcceptAsync(linkedSource.Token).ConfigureAwait(false); + + // Configure client socket based on transport type. + OnAccept(socket); return new ExposedSocketNetworkStream(socket, ownsSocket: true); } catch (Exception) { - // Recreate socket if transport is not disposed. - if (!_cancellation.IsCancellationRequested) + // Keep server socket when getting any kind of cancel request. + // Cancel happens when complete transport is about to disposed or caller + // cancels out specific accept call, no need to recycle server socket. + // In all other exception scenarios server socket will be re-created. + if (!linkedSource.IsCancellationRequested) { - CreateNewSocketServer(); + _socket = CreateNewSocketServer(); } throw; } } - private void CreateNewSocketServer() + internal abstract bool OnAccept(Socket socket); + + internal abstract IpcSocket CreateNewSocketServer(); + } + + internal sealed class IpcTcpSocketServerTransport : IpcSocketServerTransport + { + private readonly int _backlog; + private readonly IpcTcpSocketEndPoint _endPoint; + + public IpcTcpSocketServerTransport(string address, int backlog, IIpcServerTransportCallbackInternal transportCallback = null) + : base(transportCallback) + { + _endPoint = new IpcTcpSocketEndPoint(address); + _backlog = backlog != MaxAllowedConnections ? backlog : 100; + _socket = CreateNewSocketServer(); + } + + internal override bool OnAccept(Socket socket) + { + socket.NoDelay = true; + return true; + } + + internal override IpcSocket CreateNewSocketServer() + { + var socket = new IpcSocket(SocketType.Stream, ProtocolType.Tcp); + if (_endPoint.DualMode) + socket.DualMode = _endPoint.DualMode; + socket.Bind(_endPoint); + socket.Listen(_backlog); + socket.LingerState.Enabled = false; + OnCreateNewServer(socket.LocalEndPoint); + return socket; + } + } + + internal sealed class IpcUnixDomainSocketServerTransport : IpcSocketServerTransport + { + private readonly int _backlog; + private readonly IpcUnixDomainSocketEndPoint _endPoint; + + public IpcUnixDomainSocketServerTransport(string path, int backlog, IIpcServerTransportCallbackInternal transportCallback = null) + : base(transportCallback) + { + _backlog = backlog != MaxAllowedConnections ? backlog : (int)SocketOptionName.MaxConnections; + _endPoint = new IpcUnixDomainSocketEndPoint(path); + _socket = CreateNewSocketServer(); + } + + internal override bool OnAccept(Socket socket) + { + return true; + } + + internal override IpcSocket CreateNewSocketServer() { - _socket = new UnixDomainSocket(); - _socket.Bind(_path); - _socket.Listen(_backlog); - _socket.LingerState.Enabled = false; - OnCreateNewServer(); + var socket = new IpcUnixDomainSocket(); + socket.Bind(_endPoint); + socket.Listen(_backlog); + socket.LingerState.Enabled = false; + OnCreateNewServer(null); + return socket; } } internal interface IIpcServerTransportCallbackInternal { - void CreatedNewServer(); + void CreatedNewServer(EndPoint localEP); } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/UnixDomainSocket.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcSocket.cs similarity index 58% rename from src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/UnixDomainSocket.cs rename to src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcSocket.cs index 579bf831d4..3fe202a62e 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/UnixDomainSocket.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcSocket.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; -using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; @@ -12,13 +11,15 @@ namespace Microsoft.Diagnostics.NETCore.Client { - internal sealed class UnixDomainSocket : Socket + internal class IpcSocket : Socket { - private bool _ownsSocketFile; - private string _path; + public IpcSocket(SocketType socketType, ProtocolType protocolType) + : base(socketType, protocolType) + { + } - public UnixDomainSocket() : - base(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified) + public IpcSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) + : base(addressFamily, socketType, protocolType) { } @@ -44,24 +45,13 @@ public async Task AcceptAsync(CancellationToken token) } } - public void Bind(string path) + public virtual void Connect(EndPoint remoteEP, TimeSpan timeout) { - Bind(CreateUnixDomainSocketEndPoint(path)); - - _ownsSocketFile = true; - _path = path; - } - - public void Connect(string path, TimeSpan timeout) - { - IAsyncResult result = BeginConnect(CreateUnixDomainSocketEndPoint(path), null, null); + IAsyncResult result = BeginConnect(remoteEP, null, null); if (result.AsyncWaitHandle.WaitOne(timeout)) { EndConnect(result); - - _ownsSocketFile = false; - _path = path; } else { @@ -70,7 +60,7 @@ public void Connect(string path, TimeSpan timeout) } } - public async Task ConnectAsync(string path, CancellationToken token) + public async Task ConnectAsync(EndPoint remoteEP, CancellationToken token) { using (token.Register(() => Close(0))) { @@ -78,7 +68,7 @@ public async Task ConnectAsync(string path, CancellationToken token) { Func beginConnect = (callback, state) => { - return BeginConnect(CreateUnixDomainSocketEndPoint(path), callback, state); + return BeginConnect(remoteEP, callback, state); }; await Task.Factory.FromAsync(beginConnect, EndConnect, this).ConfigureAwait(false); } @@ -93,33 +83,5 @@ public async Task ConnectAsync(string path, CancellationToken token) } } - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (_ownsSocketFile && !string.IsNullOrEmpty(_path) && File.Exists(_path)) - { - File.Delete(_path); - } - } - base.Dispose(disposing); - } - - private static EndPoint CreateUnixDomainSocketEndPoint(string path) - { -#if NETCOREAPP - return new UnixDomainSocketEndPoint(path); -#elif NETSTANDARD2_0 - // UnixDomainSocketEndPoint is not part of .NET Standard 2.0 - var type = typeof(Socket).Assembly.GetType("System.Net.Sockets.UnixDomainSocketEndPoint") - ?? Type.GetType("System.Net.Sockets.UnixDomainSocketEndPoint, System.Core"); - if (type == null) - { - throw new PlatformNotSupportedException("Current process is not running a compatible .NET runtime."); - } - var ctor = type.GetConstructor(new[] { typeof(string) }); - return (EndPoint)ctor.Invoke(new object[] { path }); -#endif - } } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTcpSocketEndPoint.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTcpSocketEndPoint.cs new file mode 100644 index 0000000000..00537bf72b --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTcpSocketEndPoint.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal sealed class IpcTcpSocketEndPoint + { + public bool DualMode { get; } + public IPEndPoint EndPoint { get; } + + public static bool IsTcpIpEndPoint(string endPoint) + { + bool result = true; + + try + { + ParseTcpIpEndPoint(endPoint, out _, out _); + } + catch(Exception) + { + result = false; + } + + return result; + } + + public static string NormalizeTcpIpEndPoint(string endPoint) + { + ParseTcpIpEndPoint(endPoint, out string host, out int port); + return string.Format("{0}:{1}", host, port); + } + + public IpcTcpSocketEndPoint(string endPoint) + { + ParseTcpIpEndPoint(endPoint, out string host, out int port); + EndPoint = CreateEndPoint(host, port); + DualMode = string.CompareOrdinal(host, "*") == 0; + } + + public static implicit operator EndPoint(IpcTcpSocketEndPoint endPoint) => endPoint.EndPoint; + + private static void ParseTcpIpEndPoint(string endPoint, out string host, out int port) + { + host = ""; + port = -1; + + bool usesWildcardHost = false; + string uriToParse= ""; + + if (endPoint.Contains("://")) + { + // Host can contain wildcard (*) that is a reserved charachter in URI's. + // Replace with dummy localhost representation just for parsing purpose. + if (endPoint.IndexOf("//*", StringComparison.Ordinal) != -1) + { + usesWildcardHost = true; + uriToParse = endPoint.Replace("//*", "//localhost"); + } + else + { + uriToParse = endPoint; + } + } + + try + { + if (!string.IsNullOrEmpty(uriToParse) && Uri.TryCreate(uriToParse, UriKind.RelativeOrAbsolute, out Uri uri)) + { + if (string.Compare(uri.Scheme, Uri.UriSchemeNetTcp, StringComparison.OrdinalIgnoreCase) != 0 && + string.Compare(uri.Scheme, "tcp", StringComparison.OrdinalIgnoreCase) != 0) + { + throw new ArgumentException(string.Format("Unsupported Uri schema, \"{0}\"", uri.Scheme)); + } + + host = usesWildcardHost ? "*" : uri.Host; + port = uri.IsDefaultPort ? 0 : uri.Port; + } + } + catch (InvalidOperationException) + { + } + + if (string.IsNullOrEmpty(host) || port == -1) + { + string[] segments = endPoint.Split(':'); + if (segments.Length > 2) + { + host = string.Join(":", segments, 0, segments.Length - 1); + port = int.Parse(segments[segments.Length - 1]); + } + else if (segments.Length == 2) + { + host = segments[0]; + port = int.Parse(segments[1]); + } + + if (string.CompareOrdinal(host, "*") != 0) + { + if (!IPAddress.TryParse(host, out _)) + { + if (!Uri.TryCreate(Uri.UriSchemeNetTcp + "://" + host + ":" + port, UriKind.RelativeOrAbsolute, out _)) + { + host = ""; + port = -1; + } + } + } + } + + if (string.IsNullOrEmpty(host) || port == -1) + { + throw new ArgumentException(string.Format("Could not parse {0} into host, port", endPoint)); + } + } + + private static IPEndPoint CreateEndPoint(string host, int port) + { + IPAddress ipAddress = null; + try + { + if (string.CompareOrdinal(host, "*") == 0) + { + if (Socket.OSSupportsIPv6) + { + ipAddress = IPAddress.IPv6Any; + } + else + { + ipAddress = IPAddress.Any; + } + } + else if (!IPAddress.TryParse(host, out ipAddress)) + { + var hostEntry = Dns.GetHostEntry(host); + if (hostEntry.AddressList.Length > 0) + ipAddress = hostEntry.AddressList[0]; + } + } + catch(Exception) + { + } + + if (ipAddress == null) + throw new ArgumentException(string.Format("Could not resolve {0} into an IP address", host)); + + return new IPEndPoint(ipAddress, port); + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs index 211763f5d8..77715c5aa6 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs @@ -134,8 +134,8 @@ public override Stream Connect(TimeSpan timeout) } else { - var socket = new UnixDomainSocket(); - socket.Connect(Path.Combine(IpcRootPath, address), timeout); + var socket = new IpcUnixDomainSocket(); + socket.Connect(new IpcUnixDomainSocketEndPoint(Path.Combine(IpcRootPath, address)), timeout); return new ExposedSocketNetworkStream(socket, ownsSocket: true); } } @@ -156,8 +156,8 @@ public override async Task ConnectAsync(CancellationToken token) } else { - var socket = new UnixDomainSocket(); - await socket.ConnectAsync(Path.Combine(IpcRootPath, address), token).ConfigureAwait(false); + var socket = new IpcUnixDomainSocket(); + await socket.ConnectAsync(new IpcUnixDomainSocketEndPoint(Path.Combine(IpcRootPath, address)), token).ConfigureAwait(false); return new ExposedSocketNetworkStream(socket, ownsSocket: true); } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcUnixDomainSocket.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcUnixDomainSocket.cs new file mode 100644 index 0000000000..cd16fed02e --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcUnixDomainSocket.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal sealed class IpcUnixDomainSocket : IpcSocket + { + private bool _ownsSocketFile; + private string _path; + + internal IpcUnixDomainSocket() + : base(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified) + { + } + + public void Bind(IpcUnixDomainSocketEndPoint localEP) + { + base.Bind(localEP); + _path = localEP.Path; + _ownsSocketFile = true; + } + + public override void Connect(EndPoint localEP, TimeSpan timeout) + { + base.Connect(localEP, timeout); + _ownsSocketFile = false; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_ownsSocketFile && !string.IsNullOrEmpty(_path) && File.Exists(_path)) + { + File.Delete(_path); + } + } + base.Dispose(disposing); + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcUnixDomainSocketEndPoint.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcUnixDomainSocketEndPoint.cs new file mode 100644 index 0000000000..8a8aa072cb --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcUnixDomainSocketEndPoint.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal sealed class IpcUnixDomainSocketEndPoint + { + public string Path { get; } + public EndPoint EndPoint { get; } + + public IpcUnixDomainSocketEndPoint(string endPoint) + { + Path = endPoint; + EndPoint = CreateEndPoint(endPoint); + } + + public static implicit operator EndPoint(IpcUnixDomainSocketEndPoint endPoint) => endPoint.EndPoint; + + private static EndPoint CreateEndPoint(string endPoint) + { +#if NETCOREAPP + return new UnixDomainSocketEndPoint(endPoint); +#elif NETSTANDARD2_0 + // UnixDomainSocketEndPoint is not part of .NET Standard 2.0 + var type = typeof(Socket).Assembly.GetType("System.Net.Sockets.UnixDomainSocketEndPoint") + ?? Type.GetType("System.Net.Sockets.UnixDomainSocketEndPoint, System.Core"); + if (type == null) + { + throw new PlatformNotSupportedException("Current process is not running a compatible .NET runtime."); + } + var ctor = type.GetConstructor(new[] { typeof(string) }); + return (EndPoint)ctor.Invoke(new object[] { endPoint }); +#endif + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs new file mode 100644 index 0000000000..f0e3d9f264 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -0,0 +1,872 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.IO.Pipes; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using System.Net; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal class RuntimeTimeoutException : TimeoutException + { + public RuntimeTimeoutException(int TimeoutMs) + : base(string.Format("No new runtime endpoints connected, waited {0} ms", TimeoutMs)) + { } + } + + internal class BackendStreamTimeoutException : TimeoutException + { + public BackendStreamTimeoutException(int TimeoutMs) + : base(string.Format("No new backend streams available, waited {0} ms", TimeoutMs)) + { } + } + + /// + /// Base class representing a Diagnostics Server router factory. + /// + internal class DiagnosticsServerRouterFactory + { + protected readonly ILogger _logger; + + public DiagnosticsServerRouterFactory(ILogger logger) + { + _logger = logger; + } + + public ILogger Logger + { + get { return _logger; } + } + + public virtual void Start() + { + throw new NotImplementedException(); + } + + public virtual Task Stop() + { + throw new NotImplementedException(); + } + + public virtual void Reset() + { + throw new NotImplementedException(); + } + + public virtual Task CreateRouterAsync(CancellationToken token) + { + throw new NotImplementedException(); + } + } + + /// + /// This class represent a TCP/IP server endpoint used when building up router instances. + /// + internal class TcpServerRouterFactory : DiagnosticsServerRouterFactory + { + protected string _tcpServerAddress; + + protected ReversedDiagnosticsServer _tcpServer; + protected IpcEndpointInfo _tcpServerEndpointInfo; + + protected bool _auto_shutdown; + + protected int RuntimeTimeoutMs { get; set; } = 60000; + protected int TcpServerTimeoutMs { get; set; } = 5000; + protected int IsStreamConnectedTimeoutMs { get; set; } = 500; + + public Guid RuntimeInstanceId + { + get { return _tcpServerEndpointInfo.RuntimeInstanceCookie; } + } + + public int RuntimeProcessId + { + get { return _tcpServerEndpointInfo.ProcessId; } + } + + public string TcpServerAddress + { + get { return _tcpServerAddress; } + } + + protected TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger logger) + : base(logger) + { + _tcpServerAddress = IpcTcpSocketEndPoint.NormalizeTcpIpEndPoint(string.IsNullOrEmpty(tcpServer) ? "127.0.0.1:0" : tcpServer); + + _auto_shutdown = runtimeTimeoutMs != Timeout.Infinite; + if (runtimeTimeoutMs != Timeout.Infinite) + RuntimeTimeoutMs = runtimeTimeoutMs; + + _tcpServer = new ReversedDiagnosticsServer(_tcpServerAddress, enableTcpIpProtocol : true); + _tcpServerEndpointInfo = new IpcEndpointInfo(); + } + + public override void Start() + { + _tcpServer.Start(); + } + + public override async Task Stop() + { + await _tcpServer.DisposeAsync().ConfigureAwait(false); + } + + public override void Reset() + { + if (_tcpServerEndpointInfo.Endpoint != null) + { + _tcpServer.RemoveConnection(_tcpServerEndpointInfo.RuntimeInstanceCookie); + _tcpServerEndpointInfo = new IpcEndpointInfo(); + } + } + + protected async Task AcceptTcpStreamAsync(CancellationToken token) + { + Stream tcpServerStream; + + Logger.LogDebug($"Waiting for a new tcp connection at endpoint \"{_tcpServerAddress}\"."); + + if (_tcpServerEndpointInfo.Endpoint == null) + { + using var acceptTimeoutTokenSource = new CancellationTokenSource(); + using var acceptTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, acceptTimeoutTokenSource.Token); + + try + { + // If no new runtime instance connects, timeout. + acceptTimeoutTokenSource.CancelAfter(RuntimeTimeoutMs); + _tcpServerEndpointInfo = await _tcpServer.AcceptAsync(acceptTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + if (acceptTimeoutTokenSource.IsCancellationRequested) + { + Logger.LogDebug("No runtime instance connected before timeout."); + + if (_auto_shutdown) + throw new RuntimeTimeoutException(RuntimeTimeoutMs); + } + + throw; + } + } + + using var connectTimeoutTokenSource = new CancellationTokenSource(); + using var connectTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, connectTimeoutTokenSource.Token); + + try + { + // Get next connected tcp stream. Should timeout if no endpoint appears within timeout. + // If that happens we need to remove endpoint since it might indicate a unresponsive runtime. + connectTimeoutTokenSource.CancelAfter(TcpServerTimeoutMs); + tcpServerStream = await _tcpServerEndpointInfo.Endpoint.ConnectAsync(connectTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + if (connectTimeoutTokenSource.IsCancellationRequested) + { + Logger.LogDebug("No tcp stream connected before timeout."); + throw new BackendStreamTimeoutException(TcpServerTimeoutMs); + } + + throw; + } + + if (tcpServerStream != null) + Logger.LogDebug($"Successfully connected tcp stream, runtime id={RuntimeInstanceId}, runtime pid={RuntimeProcessId}."); + + return tcpServerStream; + } + + 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 creates IPC Server - TCP Server router instances. + /// Supports NamedPipes/UnixDomainSocket server and TCP/IP server. + /// + internal class IpcServerTcpServerRouterFactory : TcpServerRouterFactory, IIpcServerTransportCallbackInternal + { + readonly string _ipcServerPath; + + IpcServerTransport _ipcServer; + + protected int IpcServerTimeoutMs { get; set; } = Timeout.Infinite; + + public IpcServerTcpServerRouterFactory(string ipcServer, string tcpServer, int runtimeTimeoutMs, ILogger logger) + : base(tcpServer, runtimeTimeoutMs, logger) + { + _ipcServerPath = ipcServer; + if (string.IsNullOrEmpty(_ipcServerPath)) + _ipcServerPath = GetDefaultIpcServerPath(); + + _ipcServer = IpcServerTransport.Create(_ipcServerPath, IpcServerTransport.MaxAllowedConnections, false); + _tcpServer.TransportCallback = this; + } + + public static string GetDefaultIpcServerPath() + { + int processId = Process.GetCurrentProcess().Id; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}"); + } + else + { + 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"); + } + } + + public override void Start() + { + base.Start(); + _logger.LogInformation($"Starting IPC server ({_ipcServerPath}) <--> TCP server ({_tcpServerAddress}) router."); + } + + public override Task Stop() + { + _ipcServer?.Dispose(); + return base.Stop(); + } + + public override async Task CreateRouterAsync(CancellationToken token) + { + Stream tcpServerStream = null; + Stream ipcServerStream = null; + + 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); + + // Get new ipc server endpoint. + using var ipcServerStreamTask = AcceptIpcStreamAsync(cancelRouter.Token); + + await Task.WhenAny(ipcServerStreamTask, tcpServerStreamTask).ConfigureAwait(false); + + if (IsCompletedSuccessfully(ipcServerStreamTask) && IsCompletedSuccessfully(tcpServerStreamTask)) + { + ipcServerStream = ipcServerStreamTask.Result; + tcpServerStream = tcpServerStreamTask.Result; + } + else if (IsCompletedSuccessfully(ipcServerStreamTask)) + { + ipcServerStream = ipcServerStreamTask.Result; + + // We have a valid ipc stream and a pending tcp accept. 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(tcpServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); + + // Cancel out any pending tasks not yet completed. + cancelRouter.Cancel(); + + try + { + await Task.WhenAll(tcpServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); + } + catch (Exception) + { + // Check if we have an accepted tcp stream. + if (IsCompletedSuccessfully(tcpServerStreamTask)) + tcpServerStreamTask.Result?.Dispose(); + + if (checkIpcStreamTask.IsFaulted) + { + Logger.LogInformation("Broken ipc connection detected, aborting tcp connection."); + checkIpcStreamTask.GetAwaiter().GetResult(); + } + + throw; + } + + tcpServerStream = tcpServerStreamTask.Result; + } + else if (IsCompletedSuccessfully(tcpServerStreamTask)) + { + tcpServerStream = tcpServerStreamTask.Result; + + // We have a valid tcp stream and a pending ipc accept. Wait for completion + // or disconnect of tcp stream. + using var checkTcpStreamTask = IsStreamConnectedAsync(tcpServerStream, cancelRouter.Token); + + // Wait for at least completion of one task. + await Task.WhenAny(ipcServerStreamTask, checkTcpStreamTask).ConfigureAwait(false); + + // Cancel out any pending tasks not yet completed. + cancelRouter.Cancel(); + + try + { + await Task.WhenAll(ipcServerStreamTask, checkTcpStreamTask).ConfigureAwait(false); + } + catch (Exception) + { + // Check if we have an accepted ipc stream. + if (IsCompletedSuccessfully(ipcServerStreamTask)) + ipcServerStreamTask.Result?.Dispose(); + + if (checkTcpStreamTask.IsFaulted) + { + Logger.LogInformation("Broken tcp connection detected, aborting ipc connection."); + checkTcpStreamTask.GetAwaiter().GetResult(); + } + + throw; + } + + ipcServerStream = ipcServerStreamTask.Result; + } + else + { + // Error case, cancel out. wait and throw exception. + cancelRouter.Cancel(); + try + { + await Task.WhenAll(ipcServerStreamTask, tcpServerStreamTask).ConfigureAwait(false); + } + catch (Exception) + { + // Check if we have an ipc stream. + if (IsCompletedSuccessfully(ipcServerStreamTask)) + ipcServerStreamTask.Result?.Dispose(); + throw; + } + } + } + catch (Exception) + { + Logger.LogDebug("Failed creating new router instance."); + + // Cleanup and rethrow. + ipcServerStream?.Dispose(); + tcpServerStream?.Dispose(); + + throw; + } + + // Create new router. + Logger.LogDebug("New router instance successfully created."); + + return new Router(ipcServerStream, tcpServerStream, Logger); + } + + protected 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) + { + Logger.LogDebug("No ipc stream connected, timing out."); + throw new TimeoutException(); + } + + throw; + } + + if (ipcServerStream != null) + Logger.LogDebug("Successfully connected ipc stream."); + + return ipcServerStream; + } + + public void CreatedNewServer(EndPoint localEP) + { + if (localEP is IPEndPoint ipEP) + _tcpServerAddress = _tcpServerAddress.Replace(":0", string.Format(":{0}", ipEP.Port)); + } + } + + /// + /// This class creates IPC Client - TCP Server router instances. + /// Supports NamedPipes/UnixDomainSocket client and TCP/IP server. + /// + internal class IpcClientTcpServerRouterFactory : TcpServerRouterFactory, IIpcServerTransportCallbackInternal + { + readonly string _ipcClientPath; + + protected int IpcClientTimeoutMs { get; set; } = Timeout.Infinite; + + protected int IpcClientRetryTimeoutMs { get; set; } = 500; + + public IpcClientTcpServerRouterFactory(string ipcClient, string tcpServer, int runtimeTimeoutMs, ILogger logger) + : base(tcpServer, runtimeTimeoutMs, logger) + { + _ipcClientPath = ipcClient; + _tcpServer.TransportCallback = this; + } + + public override void Start() + { + base.Start(); + _logger.LogInformation($"Starting IPC client ({_ipcClientPath}) <--> TCP server ({_tcpServerAddress}) router."); + } + + public override async Task CreateRouterAsync(CancellationToken token) + { + Stream tcpServerStream = null; + Stream ipcClientStream = null; + + 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); + + // Get new client endpoint. + using var ipcClientStreamTask = ConnectIpcStreamAsync(cancelRouter.Token); + + // We have a valid tcp stream and a pending ipc stream. Wait for completion + // or disconnect of tcp stream. + using var checkTcpStreamTask = IsStreamConnectedAsync(tcpServerStream, cancelRouter.Token); + + // Wait for at least completion of one task. + await Task.WhenAny(ipcClientStreamTask, checkTcpStreamTask).ConfigureAwait(false); + + // Cancel out any pending tasks not yet completed. + cancelRouter.Cancel(); + + try + { + await Task.WhenAll(ipcClientStreamTask, checkTcpStreamTask).ConfigureAwait(false); + } + catch (Exception) + { + // Check if we have an accepted ipc stream. + if (IsCompletedSuccessfully(ipcClientStreamTask)) + ipcClientStreamTask.Result?.Dispose(); + + if (checkTcpStreamTask.IsFaulted) + { + Logger.LogInformation("Broken tcp connection detected, aborting ipc connection."); + checkTcpStreamTask.GetAwaiter().GetResult(); + } + + throw; + } + + 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); + } + 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; + 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."); + + ipcClientStream?.Dispose(); + throw; + } + + if (ipcClientStream != null) + Logger.LogDebug("Successfully connected ipc stream."); + + return ipcClientStream; + } + + public void CreatedNewServer(EndPoint localEP) + { + if (localEP is IPEndPoint ipEP) + _tcpServerAddress = _tcpServerAddress.Replace(":0", string.Format(":{0}", ipEP.Port)); + } + } + + internal class Router : IDisposable + { + readonly ILogger _logger; + + Stream _frontendStream = null; + Stream _backendStream = null; + + Task _backendReadFrontendWriteTask = null; + Task _frontendReadBackendWriteTask = null; + + CancellationTokenSource _cancelRouterTokenSource = null; + + bool _disposed = false; + + ulong _backendToFrontendByteTransfer; + ulong _frontendToBackendByteTransfer; + + static int s_routerInstanceCount; + + public TaskCompletionSource RouterTaskCompleted { get; } + + public Router(Stream frontendStream, Stream backendStream, ILogger logger, ulong initBackendToFrontendByteTransfer = 0, ulong initFrontendToBackendByteTransfer = 0) + { + _logger = logger; + + _frontendStream = frontendStream; + _backendStream = backendStream; + + _cancelRouterTokenSource = new CancellationTokenSource(); + + RouterTaskCompleted = new TaskCompletionSource(); + + _backendToFrontendByteTransfer = initBackendToFrontendByteTransfer; + _frontendToBackendByteTransfer = initFrontendToBackendByteTransfer; + + Interlocked.Increment(ref s_routerInstanceCount); + } + + public void Start() + { + if (_backendReadFrontendWriteTask != null || _frontendReadBackendWriteTask != null || _disposed) + throw new InvalidOperationException(); + + _backendReadFrontendWriteTask = BackendReadFrontendWrite(_cancelRouterTokenSource.Token); + _frontendReadBackendWriteTask = FrontendReadBackendWrite(_cancelRouterTokenSource.Token); + } + + public async void Stop() + { + if (_disposed) + throw new ObjectDisposedException(nameof(Router)); + + _cancelRouterTokenSource.Cancel(); + + List runningTasks = new List(); + + if (_backendReadFrontendWriteTask != null) + runningTasks.Add(_backendReadFrontendWriteTask); + + if (_frontendReadBackendWriteTask != null) + runningTasks.Add(_frontendReadBackendWriteTask); + + await Task.WhenAll(runningTasks.ToArray()).ConfigureAwait(false); + + _backendReadFrontendWriteTask?.Dispose(); + _frontendReadBackendWriteTask?.Dispose(); + + RouterTaskCompleted?.TrySetResult(true); + + _backendReadFrontendWriteTask = null; + _frontendReadBackendWriteTask = null; + } + + public bool IsRunning { + get + { + if (_backendReadFrontendWriteTask == null || _frontendReadBackendWriteTask == null || _disposed) + return false; + + return !_backendReadFrontendWriteTask.IsCompleted && !_frontendReadBackendWriteTask.IsCompleted; + } + } + public void Dispose() + { + if (!_disposed) + { + Stop(); + + _cancelRouterTokenSource.Dispose(); + + _backendStream?.Dispose(); + _frontendStream?.Dispose(); + + _disposed = true; + + Interlocked.Decrement(ref s_routerInstanceCount); + + _logger.LogTrace($"Diposed stats: Backend->Frontend {_backendToFrontendByteTransfer} bytes, Frontend->Backend {_frontendToBackendByteTransfer} bytes."); + _logger.LogTrace($"Active instances: {s_routerInstanceCount}"); + } + } + + async Task BackendReadFrontendWrite(CancellationToken token) + { + try + { + byte[] buffer = new byte[1024]; + while (!token.IsCancellationRequested) + { + _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."); + + // Check for end of stream indicating that remote end hung-up. + if (bytesRead == 0) + { + _logger.LogTrace("Backend hung up."); + break; + } + + _backendToFrontendByteTransfer += (ulong)bytesRead; + + _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."); + } + } + catch (Exception) + { + // 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."); + } + + RouterTaskCompleted?.TrySetResult(true); + } + + async Task FrontendReadBackendWrite(CancellationToken token) + { + try + { + byte[] buffer = new byte[1024]; + while (!token.IsCancellationRequested) + { + _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."); + + // Check for end of stream indicating that remote end hung-up. + if (bytesRead == 0) + { + _logger.LogTrace("Frontend hung up."); + break; + } + + _frontendToBackendByteTransfer += (ulong)bytesRead; + + _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."); + } + } + catch (Exception) + { + // 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."); + } + + RouterTaskCompleted?.TrySetResult(true); + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs new file mode 100644 index 0000000000..1625c18db5 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + /// + /// Class used to run different flavours of Diagnostics Server routers. + /// + internal class DiagnosticsServerRouterRunner + { + internal interface Callbacks + { + void OnRouterStarted(string boundTcpServerAddress); + void OnRouterStopped(); + } + + public static async Task runIpcClientTcpServerRouter(CancellationToken token, string ipcClient, string tcpServer, int runtimeTimeoutMs, ILogger logger, Callbacks callbacks) + { + return await runRouter(token, new IpcClientTcpServerRouterFactory(ipcClient, tcpServer, runtimeTimeoutMs, logger), callbacks).ConfigureAwait(false); + } + + public static async Task runIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeoutMs, ILogger logger, Callbacks callbacks) + { + return await runRouter(token, new IpcServerTcpServerRouterFactory(ipcServer, tcpServer, runtimeTimeoutMs, logger), callbacks).ConfigureAwait(false); + } + + public static bool isLoopbackOnly(string address) + { + bool isLooback = false; + + try + { + var value = new IpcTcpSocketEndPoint(address); + isLooback = IPAddress.IsLoopback(value.EndPoint.Address); + } + catch { } + + return isLooback; + } + + async static Task runRouter(CancellationToken token, TcpServerRouterFactory routerFactory, Callbacks callbacks) + { + List runningTasks = new List(); + List runningRouters = new List(); + + try + { + routerFactory.Start(); + callbacks?.OnRouterStarted(routerFactory.TcpServerAddress); + + while (!token.IsCancellationRequested) + { + Task routerTask = null; + Router router = null; + + try + { + routerTask = routerFactory.CreateRouterAsync(token); + + do + { + // Search list and clean up dead router instances before continue waiting on new instances. + runningRouters.RemoveAll(IsRouterDead); + + runningTasks.Clear(); + foreach (var runningRouter in runningRouters) + runningTasks.Add(runningRouter.RouterTaskCompleted.Task); + runningTasks.Add(routerTask); + } + while (await Task.WhenAny(runningTasks.ToArray()).ConfigureAwait(false) != routerTask); + + if (routerTask.IsFaulted || routerTask.IsCanceled) + { + //Throw original exception. + routerTask.GetAwaiter().GetResult(); + } + + if (routerTask.IsCompleted) + { + router = routerTask.Result; + router.Start(); + + // Add to list of running router instances. + runningRouters.Add(router); + router = null; + } + + routerTask.Dispose(); + routerTask = null; + } + catch (Exception ex) + { + router?.Dispose(); + router = null; + + routerTask?.Dispose(); + routerTask = null; + + // Timing out on accepting new streams could mean that either the frontend holds an open connection + // alive (but currently not using it), or we have a dead backend. If there are no running + // routers we assume a dead backend. Reset current backend endpoint and see if we get + // reconnect using same or different runtime instance. + if (ex is BackendStreamTimeoutException && runningRouters.Count == 0) + { + routerFactory.Logger.LogDebug("No backend stream available before timeout."); + routerFactory.Reset(); + } + + // Timing out on accepting a new runtime connection means there is no runtime alive. + // 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."); + throw; + } + } + } + } + catch (Exception ex) + { + routerFactory.Logger.LogInformation($"Shutting down due to error: {ex.Message}"); + } + finally + { + if (token.IsCancellationRequested) + routerFactory.Logger.LogInformation("Shutting down due to cancelation request."); + + runningRouters.RemoveAll(IsRouterDead); + runningRouters.Clear(); + + await routerFactory?.Stop(); + callbacks?.OnRouterStopped(); + + routerFactory.Logger.LogInformation("Router stopped."); + } + return 0; + } + + static bool IsRouterDead(Router router) + { + bool isRunning = router.IsRunning && !router.RouterTaskCompleted.Task.IsCompleted; + if (!isRunning) + router.Dispose(); + return !isRunning; + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj index afe8d0cfa3..e53d0b81d0 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj +++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj @@ -15,10 +15,12 @@ + + diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index 548dc49cc4..0c15a60394 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -27,23 +27,46 @@ internal sealed class ReversedDiagnosticsServer : IAsyncDisposable private readonly CancellationTokenSource _disposalSource = new CancellationTokenSource(); private readonly HandleableCollection _endpointInfos = new HandleableCollection(); private readonly ConcurrentDictionary> _streamCollections = new ConcurrentDictionary>(); - private readonly string _transportPath; + private readonly string _address; private bool _disposed = false; private Task _listenTask; + private bool _enableTcpIpProtocol = false; /// /// Constructs the instance with an endpoint bound - /// to the location specified by . + /// to the location specified by . /// - /// - /// The path of the server endpoint. + /// + /// The server endpoint. /// On Windows, this can be a full pipe path or the name without the "\\.\pipe\" prefix. /// On all other systems, this must be the full file path of the socket. /// - public ReversedDiagnosticsServer(string transportPath) + public ReversedDiagnosticsServer(string address) { - _transportPath = transportPath; + _address = address; + } + + /// + /// Constructs the instance with an endpoint bound + /// to the location specified by . + /// + /// + /// The server endpoint. + /// On Windows, this can be a full pipe path or the name without the "\\.\pipe\" prefix. + /// On all other systems, this must be the full file path of the socket. + /// When TcpIp is enabled, this can also be host:port of the listening socket. + /// + /// + /// Add TcpIp as a supported protocol for ReversedDiagnosticServer. When enabled, address will + /// be analyzed and if on format host:port, ReversedDiagnosticServer will try to bind + /// a TcpIp listener to host and port. + /// + /// + public ReversedDiagnosticsServer(string address, bool enableTcpIpProtocol) + { + _address = address; + _enableTcpIpProtocol = enableTcpIpProtocol; } public async ValueTask DisposeAsync() @@ -80,7 +103,7 @@ public async ValueTask DisposeAsync() } /// - /// Starts listening at the transport path for new connections. + /// Starts listening at the address for new connections. /// public void Start() { @@ -88,7 +111,7 @@ public void Start() } /// - /// Starts listening at the transport path for new connections. + /// Starts listening at the address for new connections. /// /// The maximum number of connections the server will support. public void Start(int maxConnections) @@ -101,6 +124,8 @@ public void Start(int maxConnections) } _listenTask = ListenAsync(maxConnections, _disposalSource.Token); + if (_listenTask.IsFaulted) + _listenTask.Wait(); // Rethrow aggregated exception. } /// @@ -164,17 +189,15 @@ private void VerifyIsStarted() } /// - /// Listens at the transport path for new connections. + /// Listens at the address for new connections. /// /// The maximum number of connections the server will support. /// The token to monitor for cancellation requests. - /// A task that completes when the server is no longer listening at the transport path. + /// A task that completes when the server is no longer listening at the address. private async Task ListenAsync(int maxConnections, CancellationToken token) { // This disposal shuts down the transport in case an exception is thrown. - using var transport = IpcServerTransport.Create(_transportPath, maxConnections); - // Set transport callback for testing purposes. - transport.SetCallback(TransportCallback); + using var transport = IpcServerTransport.Create(_address, maxConnections, _enableTcpIpProtocol, TransportCallback); // This disposal shuts down the transport in case of cancellation; causes the transport // to not recreate the server stream before the AcceptAsync call observes the cancellation. using var _ = token.Register(() => transport.Dispose()); diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs new file mode 100644 index 0000000000..47520d44f4 --- /dev/null +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Internal.Common.Utils; +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter +{ + public class DiagnosticsServerRouterLauncher : DiagnosticsServerRouterRunner.Callbacks + { + public CancellationToken CommandToken { get; set; } + public bool SuspendProcess { get; set; } + public bool Verbose { get; set; } + + public void OnRouterStarted(string boundTcpServerAddress) + { + if (ProcessLauncher.Launcher.HasChildProc) + { + string diagnosticPorts = boundTcpServerAddress + (SuspendProcess ? ",suspend" : ",nosuspend"); + 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); + diagnosticPorts = ""; + } + + ProcessLauncher.Launcher.Start(diagnosticPorts, CommandToken, Verbose, Verbose); + } + } + + public void OnRouterStopped() + { + ProcessLauncher.Launcher.Cleanup(); + } + } + + public class DiagnosticsServerRouterCommands + { + public static DiagnosticsServerRouterLauncher Launcher { get; } = new DiagnosticsServerRouterLauncher(); + + public DiagnosticsServerRouterCommands() + { + } + + public async Task RunIpcClientTcpServerRouter(CancellationToken token, string ipcClient, string tcpServer, int runtimeTimeout, string verbose) + { + checkLoopbackOnly(tcpServer); + + 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 = true; + Launcher.Verbose = logLevel != LogLevel.Information; + Launcher.CommandToken = token; + + var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, tcpServer, 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; + } + + public async Task RunIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeout, string verbose) + { + checkLoopbackOnly(tcpServer); + + 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 = true; + Launcher.Verbose = logLevel != LogLevel.Information; + Launcher.CommandToken = token; + + var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, tcpServer, 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)) + { + StringBuilder message = new StringBuilder(); + + message.Append("WARNING: Binding tcp server endpoint to anything except loopback interface "); + message.Append("(localhost, 127.0.0.1 or [::1]) is NOT recommended. Any connections towards "); + message.Append("tcp server endpoint will be unauthenticated and unencrypted. This component "); + message.Append("is intented for development use and should only be run in development and "); + message.Append("testing environments."); + message.AppendLine(); + + var currentColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(message.ToString()); + Console.ForegroundColor = currentColor; + } + } + } +} diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs new file mode 100644 index 0000000000..a186bec91b --- /dev/null +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text; +using System.CommandLine; +using System.CommandLine.Binding; +using System.CommandLine.Builder; +using System.CommandLine.Parsing; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Tools.Common; +using Microsoft.Internal.Common.Utils; + +namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter +{ + internal class Program + { + 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); + + private static Command IpcClientTcpServerRouterCommand() => + new Command( + name: "client-server", + description: "Start a .NET application Diagnostics Server routing local IPC server <--> remote TCP client. " + + "Router is configured using an IPC client (connecting diagnostic tool IPC server) " + + "and a TCP/IP server (accepting runtime TCP client).") + { + // Handler + HandlerDescriptor.FromDelegate((DiagnosticsServerIpcClientTcpServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcClientTcpServerRouter).GetCommandHandler(), + // Options + IpcClientAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption() + }; + + private static Command IpcServerTcpServerRouterCommand() => + new Command( + name: "server-server", + description: "Start a .NET application Diagnostics Server routing local IPC client <--> remote TCP client. " + + "Router is configured using an IPC server (connecting to by diagnostic tools) " + + "and a TCP/IP server (accepting runtime TCP client).") + { + // Handler + HandlerDescriptor.FromDelegate((DiagnosticsServerIpcClientTcpServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerTcpServerRouter).GetCommandHandler(), + // Options + IpcServerAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption() + }; + + private static Option IpcClientAddressOption() => + new Option( + aliases: new[] { "--ipc-client", "-ipcc" }, + description: "The diagnostic tool diagnostics server ipc address (--diagnostic-port argument). " + + "Router connects diagnostic tool ipc server when establishing a " + + "new route between runtime and diagnostic tool.") + { + Argument = new Argument(name: "ipcClient", getDefaultValue: () => "") + }; + + private static Option IpcServerAddressOption() => + new Option( + aliases: new[] { "--ipc-server", "-ipcs" }, + description: "The diagnostics server ipc address to route. Router accepts ipc connections from diagnostic tools " + + "establishing a new route between runtime and diagnostic tool. If not specified " + + "router will use default ipc diagnostics server path.") + { + Argument = new Argument(name: "ipcServer", getDefaultValue: () => "") + }; + + private static Option TcpServerAddressOption() => + new Option( + aliases: new[] { "--tcp-server", "-tcps" }, + description: "The router TCP/IP address using format [host]:[port]. " + + "Router can bind one (127.0.0.1, [::1], 0.0.0.0, [::], ipv4 address, ipv6 address, hostname) " + + "or all (*) interfaces. Launch runtime using DOTNET_DiagnosticPorts environment variable " + + "connecting router TCP server during startup.") + { + Argument = new Argument(name: "tcpServer", getDefaultValue: () => "") + }; + + private static Option RuntimeTimeoutOption() => + new Option( + aliases: new[] { "--runtime-timeout", "-rt" }, + description: "Automatically shutdown router if no runtime connects to it before specified timeout (seconds)." + + "If not specified, router won't trigger an automatic shutdown.") + { + Argument = new Argument(name: "runtimeTimeout", getDefaultValue: () => Timeout.Infinite) + }; + + private static Option VerboseOption() => + new Option( + aliases: new[] { "--verbose", "-v" }, + description: "Enable verbose logging (debug|trace)") + { + Argument = new Argument(name: "verbose", getDefaultValue: () => "") + }; + + private static int Main(string[] args) + { + StringBuilder message = new StringBuilder(); + + var currentColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("WARNING: dotnet-dsrouter is an experimental development tool not intended for production environments." + Environment.NewLine); + Console.ForegroundColor = currentColor; + + var parser = new CommandLineBuilder() + .AddCommand(IpcClientTcpServerRouterCommand()) + .AddCommand(IpcServerTcpServerRouterCommand()) + .UseDefaults() + .Build(); + + ParseResult parseResult = parser.Parse(args); + + if (parseResult.UnparsedTokens.Count > 0) + { + ProcessLauncher.Launcher.PrepareChildProcess(args); + } + + return parser.InvokeAsync(args).Result; + } + } +} diff --git a/src/Tools/dotnet-dsrouter/README.md b/src/Tools/dotnet-dsrouter/README.md new file mode 100644 index 0000000000..6eb0ee052b --- /dev/null +++ b/src/Tools/dotnet-dsrouter/README.md @@ -0,0 +1 @@ +# dotnet-dsrouter diff --git a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj new file mode 100644 index 0000000000..47d66f5953 --- /dev/null +++ b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj @@ -0,0 +1,31 @@ + + + + netcoreapp3.1 + netcoreapp2.1 + true + dotnet-dsrouter + Microsoft.Diagnostics.Tools.DiagnosticsServerRouter + .NET Performance Diagnostic Server Router Tool + Diagnostic + $(Description) + false + + + + + + + + + + + + + + + + + + + diff --git a/src/Tools/dotnet-dsrouter/runtimeconfig.template.json b/src/Tools/dotnet-dsrouter/runtimeconfig.template.json new file mode 100644 index 0000000000..f022b7ffce --- /dev/null +++ b/src/Tools/dotnet-dsrouter/runtimeconfig.template.json @@ -0,0 +1,3 @@ +{ + "rollForwardOnNoCandidateFx": 2 +} \ No newline at end of file diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/ReversedServerTests.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/ReversedServerTests.cs index 96227a222f..a0d931ae6c 100644 --- a/src/tests/Microsoft.Diagnostics.NETCore.Client/ReversedServerTests.cs +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/ReversedServerTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics.Tracing; using System.IO; +using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.Diagnostics.Tracing; @@ -657,7 +658,7 @@ public IpcServerTransportCallback() _transportVersionTimer = new Timer(NotifyStableTransportVersion, this, Timeout.Infinite, 0); } - public void CreatedNewServer() + public void CreatedNewServer(EndPoint localEp) { _semaphore.Wait(StableTransportSemaphoreTimeout); try