diff --git a/eng/Versions.props b/eng/Versions.props index 487a58c001..0a52149c8a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -50,6 +50,9 @@ 2.0.64 2.1.1 + + 6.0.0 + 6.0.0 5.0.1 2.0.0-beta1.20468.1 diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcServerTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcServerTransport.cs index 64e99ee666..b423432fe4 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcServerTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcServerTransport.cs @@ -17,9 +17,13 @@ internal abstract class IpcServerTransport : IDisposable private IIpcServerTransportCallbackInternal _callback; private bool _disposed; - public static IpcServerTransport Create(string address, int maxConnections, bool enableTcpIpProtocol, IIpcServerTransportCallbackInternal transportCallback = null) + public static IpcServerTransport Create(string address, int maxConnections, ReversedDiagnosticsServer.Kind kind, IIpcServerTransportCallbackInternal transportCallback = null) { - if (!enableTcpIpProtocol || !IpcTcpSocketEndPoint.IsTcpIpEndPoint(address)) + if (kind == ReversedDiagnosticsServer.Kind.WebSocket) + { + return new IpcWebSocketServerTransport(address, maxConnections, transportCallback); + } + else if (kind == ReversedDiagnosticsServer.Kind.Ipc || !IpcTcpSocketEndPoint.IsTcpIpEndPoint(address)) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs new file mode 100644 index 0000000000..0139b86b8b --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.NETCore.Client; + +internal sealed class IpcWebSocketServerTransport : IpcServerTransport +{ + public IpcWebSocketServerTransport(string address, int maxAllowedConnections, IIpcServerTransportCallbackInternal transportCallback = null) + : base(transportCallback) + { + } + + protected override void Dispose(bool disposing) + { + } + + public override async Task AcceptAsync(CancellationToken token) + { + WebSocketServer.IWebSocketServer server = WebSocketServer.WebSocketServerProvider.GetWebSocketServerInstance(); + return await server.AcceptConnection(token); + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index fbef8756ef..a114940184 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -107,6 +107,10 @@ protected bool IsStreamConnected(Stream stream, CancellationToken token) networkStream.Socket.Blocking = blockingState; } } + else if (stream is WebSocketServer.IWebSocketStreamAdapter adapter) + { + connected = adapter.IsConnected; + } else { connected = false; @@ -145,85 +149,79 @@ protected bool IsCompletedSuccessfully(Task t) } /// - /// This class represent a TCP/IP server endpoint used when building up router instances. + /// This is a common base class for network-based server endpoints used when building router instances. /// - internal class TcpServerRouterFactory : IIpcServerTransportCallbackInternal + /// + /// We have two subclases: for normal TCP/IP sockets, and another for WebSocket connections. + /// + internal abstract class NetServerRouterFactory : IIpcServerTransportCallbackInternal { - protected readonly ILogger _logger; + public delegate NetServerRouterFactory CreateInstanceDelegate(string webSocketURL, int runtimeTimeoutMs, ILogger logger); - string _tcpServerAddress; + private readonly ILogger _logger; + private IpcEndpointInfo _netServerEndpointInfo; + public abstract void CreatedNewServer(EndPoint localEP); - ReversedDiagnosticsServer _tcpServer; - IpcEndpointInfo _tcpServerEndpointInfo; - bool _auto_shutdown; + protected ILogger Logger => _logger; - int RuntimeTimeoutMs { get; set; } = 60000; - int TcpServerTimeoutMs { get; set; } = 5000; + protected int RuntimeTimeoutMs { get; private set; } = 60000; + protected int NetServerTimeoutMs { get; set; } = 5000; - public Guid RuntimeInstanceId - { - get { return _tcpServerEndpointInfo.RuntimeInstanceCookie; } - } + private bool _auto_shutdown; - public int RuntimeProcessId - { - get { return _tcpServerEndpointInfo.ProcessId; } - } + protected bool IsAutoShutdown => _auto_shutdown; - public string TcpServerAddress + protected IpcEndpointInfo NetServerEndpointInfo { - get { return _tcpServerAddress; } + get => _netServerEndpointInfo; + private set { _netServerEndpointInfo = value; } } - public delegate TcpServerRouterFactory CreateInstanceDelegate(string tcpServer, int runtimeTimeoutMs, ILogger logger); - public static TcpServerRouterFactory CreateDefaultInstance(string tcpServer, int runtimeTimeoutMs, ILogger logger) + protected IpcEndpoint Endpoint => NetServerEndpointInfo.Endpoint; + public Guid RuntimeInstanceId => NetServerEndpointInfo.RuntimeInstanceCookie; + public int RuntimeProcessId => NetServerEndpointInfo.ProcessId; + + protected void ResetEnpointInfo() { - return new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger); + NetServerEndpointInfo = new IpcEndpointInfo(); } - public TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger logger) + protected NetServerRouterFactory(int runtimeTimeoutMs, ILogger logger) { _logger = 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(); - _tcpServer.TransportCallback = this; - } + _netServerEndpointInfo = new IpcEndpointInfo(); - public virtual void Start() - { - _tcpServer.Start(); } - public virtual async Task Stop() - { - await _tcpServer.DisposeAsync().ConfigureAwait(false); - } + /// + /// Subclasses should return a human and machine readable address of the server. + /// For TCP this should be something that can be passed as an address in DOTNET_DiagnosticPorts, for WebSocket it could be a URI. + /// + public abstract string ServerAddress { get; } + /// + /// Subclasses should return a human readable description of the server connection type ("tcp", "WebSocket", etc) + /// + public abstract string ServerTransportName { get; } - public void Reset() - { - if (_tcpServerEndpointInfo.Endpoint != null) - { - _tcpServer.RemoveConnection(_tcpServerEndpointInfo.RuntimeInstanceCookie); - _tcpServerEndpointInfo = new IpcEndpointInfo(); - } - } + protected abstract Task AcceptAsyncImpl(CancellationToken token); - public async Task AcceptTcpStreamAsync(CancellationToken token) + public abstract void Start(); + public abstract Task Stop(); + public abstract void Reset(); + + public async Task AcceptNetStreamAsync(CancellationToken token) { - Stream tcpServerStream; + Stream netServerStream; - _logger?.LogDebug($"Waiting for a new tcp connection at endpoint \"{_tcpServerAddress}\"."); + Logger?.LogDebug($"Waiting for a new {ServerTransportName} connection at endpoint \"{ServerAddress}\"."); - if (_tcpServerEndpointInfo.Endpoint == null) + if (Endpoint == null) { using var acceptTimeoutTokenSource = new CancellationTokenSource(); using var acceptTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, acceptTimeoutTokenSource.Token); @@ -232,15 +230,15 @@ public async Task AcceptTcpStreamAsync(CancellationToken token) { // If no new runtime instance connects, timeout. acceptTimeoutTokenSource.CancelAfter(RuntimeTimeoutMs); - _tcpServerEndpointInfo = await _tcpServer.AcceptAsync(acceptTokenSource.Token).ConfigureAwait(false); + NetServerEndpointInfo = await AcceptAsyncImpl(acceptTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { if (acceptTimeoutTokenSource.IsCancellationRequested) { - _logger?.LogDebug("No runtime instance connected before timeout."); + Logger?.LogDebug("No runtime instance connected before timeout."); - if (_auto_shutdown) + if (IsAutoShutdown) throw new RuntimeTimeoutException(RuntimeTimeoutMs); } @@ -255,33 +253,139 @@ public async Task AcceptTcpStreamAsync(CancellationToken token) { // 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); + connectTimeoutTokenSource.CancelAfter(NetServerTimeoutMs); + netServerStream = await Endpoint.ConnectAsync(connectTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { if (connectTimeoutTokenSource.IsCancellationRequested) { - _logger?.LogDebug("No tcp stream connected before timeout."); - throw new BackendStreamTimeoutException(TcpServerTimeoutMs); + Logger?.LogDebug($"No {ServerTransportName} stream connected before timeout."); + throw new BackendStreamTimeoutException(NetServerTimeoutMs); } throw; } - if (tcpServerStream != null) - _logger?.LogDebug($"Successfully connected tcp stream, runtime id={RuntimeInstanceId}, runtime pid={RuntimeProcessId}."); + if (netServerStream != null) + Logger?.LogDebug($"Successfully connected {ServerTransportName} stream, runtime id={RuntimeInstanceId}, runtime pid={RuntimeProcessId}."); - return tcpServerStream; + return netServerStream; } + } + + /// + /// This class represent a TCP/IP server endpoint used when building up router instances. + /// + internal class TcpServerRouterFactory : NetServerRouterFactory + { + + string _tcpServerAddress; + + ReversedDiagnosticsServer _tcpServer; - public void CreatedNewServer(EndPoint localEP) + public string TcpServerAddress + { + get { return _tcpServerAddress; } + } + + public static TcpServerRouterFactory CreateDefaultInstance(string tcpServer, int runtimeTimeoutMs, ILogger logger) + { + return new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger); + } + + public TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger logger) : base(runtimeTimeoutMs, logger) + { + _tcpServerAddress = IpcTcpSocketEndPoint.NormalizeTcpIpEndPoint(string.IsNullOrEmpty(tcpServer) ? "127.0.0.1:0" : tcpServer); + + _tcpServer = new ReversedDiagnosticsServer(_tcpServerAddress, ReversedDiagnosticsServer.Kind.Tcp); + _tcpServer.TransportCallback = this; + } + + public override void Start() + { + _tcpServer.Start(); + } + + public override async Task Stop() + { + await _tcpServer.DisposeAsync().ConfigureAwait(false); + } + + public override void Reset() + { + if (Endpoint != null) + { + _tcpServer.RemoveConnection(NetServerEndpointInfo.RuntimeInstanceCookie); + ResetEnpointInfo(); + } + } + + protected override Task AcceptAsyncImpl(CancellationToken token) => _tcpServer.AcceptAsync(token); + public override string ServerAddress => _tcpServerAddress; + public override string ServerTransportName => "TCP"; + + public override void CreatedNewServer(EndPoint localEP) { if (localEP is IPEndPoint ipEP) _tcpServerAddress = _tcpServerAddress.Replace(":0", string.Format(":{0}", ipEP.Port)); } } + /// + /// This class represent a WebSocket server endpoint used when building up router instances. + /// + internal class WebSocketServerRouterFactory : NetServerRouterFactory + { + + private readonly string _webSocketURL; + + ReversedDiagnosticsServer _webSocketServer; + + public string WebSocketURL => _webSocketURL; + + public static WebSocketServerRouterFactory CreateDefaultInstance(string webSocketURL, int runtimeTimeoutMs, ILogger logger) + { + return new WebSocketServerRouterFactory(webSocketURL, runtimeTimeoutMs, logger); + } + + public WebSocketServerRouterFactory(string webSocketURL, int runtimeTimeoutMs, ILogger logger) : base(runtimeTimeoutMs, logger) + { + _webSocketURL = string.IsNullOrEmpty(webSocketURL) ? "ws://127.0.0.1:8088/diagnostics" : webSocketURL; + + _webSocketServer = new ReversedDiagnosticsServer(_webSocketURL, ReversedDiagnosticsServer.Kind.WebSocket); + _webSocketServer.TransportCallback = this; + } + + public override void Start() + { + _webSocketServer.Start(); + } + + public override async Task Stop() + { + await _webSocketServer.DisposeAsync().ConfigureAwait(false); + } + + public override void Reset() + { + if (Endpoint != null) + { + _webSocketServer.RemoveConnection(NetServerEndpointInfo.RuntimeInstanceCookie); + ResetEnpointInfo(); + } + } + + protected override Task AcceptAsyncImpl(CancellationToken token) => _webSocketServer.AcceptAsync(token); + public override string ServerAddress => WebSocketURL; + public override string ServerTransportName => "WebSocket"; + + public override void CreatedNewServer(EndPoint localEP) + { + } + + } + /// /// This class represent a TCP/IP client endpoint used when building up router instances. /// @@ -445,7 +549,7 @@ public IpcServerRouterFactory(string ipcServer, ILogger logger) _logger = logger; _ipcServerPath = ipcServer; - _ipcServer = IpcServerTransport.Create(_ipcServerPath, IpcServerTransport.MaxAllowedConnections, false); + _ipcServer = IpcServerTransport.Create(_ipcServerPath, IpcServerTransport.MaxAllowedConnections, ReversedDiagnosticsServer.Kind.Ipc); } public void Start() @@ -603,13 +707,13 @@ public async Task ConnectIpcStreamAsync(CancellationToken token) internal class IpcServerTcpServerRouterFactory : DiagnosticsServerRouterFactory { ILogger _logger; - TcpServerRouterFactory _tcpServerRouterFactory; + NetServerRouterFactory _netServerRouterFactory; IpcServerRouterFactory _ipcServerRouterFactory; public IpcServerTcpServerRouterFactory(string ipcServer, string tcpServer, int runtimeTimeoutMs, TcpServerRouterFactory.CreateInstanceDelegate factory, ILogger logger) { _logger = logger; - _tcpServerRouterFactory = factory(tcpServer, runtimeTimeoutMs, logger); + _netServerRouterFactory = factory(tcpServer, runtimeTimeoutMs, logger); _ipcServerRouterFactory = new IpcServerRouterFactory(ipcServer, logger); } @@ -625,7 +729,7 @@ public override string TcpAddress { get { - return _tcpServerRouterFactory.TcpServerAddress; + return _netServerRouterFactory.ServerAddress; } } @@ -639,24 +743,24 @@ public override ILogger Logger public override Task Start(CancellationToken token) { - _tcpServerRouterFactory.Start(); + _netServerRouterFactory.Start(); _ipcServerRouterFactory.Start(); - _logger?.LogInformation($"Starting IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router."); + _logger?.LogInformation($"Starting IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> {_netServerRouterFactory.ServerTransportName} server ({_netServerRouterFactory.ServerAddress}) router."); return Task.CompletedTask; } public override Task Stop() { - _logger?.LogInformation($"Stopping IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router."); + _logger?.LogInformation($"Stopping IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> {_netServerRouterFactory.ServerTransportName} server ({_netServerRouterFactory.ServerAddress}) router."); _ipcServerRouterFactory.Stop(); - return _tcpServerRouterFactory.Stop(); + return _netServerRouterFactory.Stop(); } public override void Reset() { - _tcpServerRouterFactory.Reset(); + _netServerRouterFactory.Reset(); } public override async Task CreateRouterAsync(CancellationToken token) @@ -671,17 +775,17 @@ public override async Task CreateRouterAsync(CancellationToken token) using CancellationTokenSource cancelRouter = CancellationTokenSource.CreateLinkedTokenSource(token); // Get new tcp server endpoint. - using var tcpServerStreamTask = _tcpServerRouterFactory.AcceptTcpStreamAsync(cancelRouter.Token); + using var netServerStreamTask = _netServerRouterFactory.AcceptNetStreamAsync(cancelRouter.Token); // Get new ipc server endpoint. using var ipcServerStreamTask = _ipcServerRouterFactory.AcceptIpcStreamAsync(cancelRouter.Token); - await Task.WhenAny(ipcServerStreamTask, tcpServerStreamTask).ConfigureAwait(false); + await Task.WhenAny(ipcServerStreamTask, netServerStreamTask).ConfigureAwait(false); - if (IsCompletedSuccessfully(ipcServerStreamTask) && IsCompletedSuccessfully(tcpServerStreamTask)) + if (IsCompletedSuccessfully(ipcServerStreamTask) && IsCompletedSuccessfully(netServerStreamTask)) { ipcServerStream = ipcServerStreamTask.Result; - tcpServerStream = tcpServerStreamTask.Result; + tcpServerStream = netServerStreamTask.Result; } else if (IsCompletedSuccessfully(ipcServerStreamTask)) { @@ -692,35 +796,35 @@ public override async Task CreateRouterAsync(CancellationToken token) using var checkIpcStreamTask = IsStreamConnectedAsync(ipcServerStream, cancelRouter.Token); // Wait for at least completion of one task. - await Task.WhenAny(tcpServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); + await Task.WhenAny(netServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); // Cancel out any pending tasks not yet completed. cancelRouter.Cancel(); try { - await Task.WhenAll(tcpServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); + await Task.WhenAll(netServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); } catch (Exception) { // Check if we have an accepted tcp stream. - if (IsCompletedSuccessfully(tcpServerStreamTask)) - tcpServerStreamTask.Result?.Dispose(); + if (IsCompletedSuccessfully(netServerStreamTask)) + netServerStreamTask.Result?.Dispose(); if (checkIpcStreamTask.IsFaulted) { - _logger?.LogInformation("Broken ipc connection detected, aborting tcp connection."); + _logger?.LogInformation($"Broken ipc connection detected, aborting {_netServerRouterFactory.ServerTransportName} connection."); checkIpcStreamTask.GetAwaiter().GetResult(); } throw; } - tcpServerStream = tcpServerStreamTask.Result; + tcpServerStream = netServerStreamTask.Result; } - else if (IsCompletedSuccessfully(tcpServerStreamTask)) + else if (IsCompletedSuccessfully(netServerStreamTask)) { - tcpServerStream = tcpServerStreamTask.Result; + tcpServerStream = netServerStreamTask.Result; // We have a valid tcp stream and a pending ipc accept. Wait for completion // or disconnect of tcp stream. @@ -744,7 +848,7 @@ public override async Task CreateRouterAsync(CancellationToken token) if (checkTcpStreamTask.IsFaulted) { - _logger?.LogInformation("Broken tcp connection detected, aborting ipc connection."); + _logger?.LogInformation($"Broken {_netServerRouterFactory.ServerTransportName} connection detected, aborting ipc connection."); checkTcpStreamTask.GetAwaiter().GetResult(); } @@ -759,7 +863,7 @@ public override async Task CreateRouterAsync(CancellationToken token) cancelRouter.Cancel(); try { - await Task.WhenAll(ipcServerStreamTask, tcpServerStreamTask).ConfigureAwait(false); + await Task.WhenAll(ipcServerStreamTask, netServerStreamTask).ConfigureAwait(false); } catch (Exception) { @@ -920,9 +1024,9 @@ internal class IpcClientTcpServerRouterFactory : DiagnosticsServerRouterFactory { ILogger _logger; IpcClientRouterFactory _ipcClientRouterFactory; - TcpServerRouterFactory _tcpServerRouterFactory; + NetServerRouterFactory _tcpServerRouterFactory; - public IpcClientTcpServerRouterFactory(string ipcClient, string tcpServer, int runtimeTimeoutMs, TcpServerRouterFactory.CreateInstanceDelegate factory, ILogger logger) + public IpcClientTcpServerRouterFactory(string ipcClient, string tcpServer, int runtimeTimeoutMs, NetServerRouterFactory.CreateInstanceDelegate factory, ILogger logger) { _logger = logger; _ipcClientRouterFactory = new IpcClientRouterFactory(ipcClient, logger); @@ -941,7 +1045,7 @@ public override string TcpAddress { get { - return _tcpServerRouterFactory.TcpServerAddress; + return _tcpServerRouterFactory.ServerAddress; } } @@ -960,14 +1064,14 @@ public override Task Start(CancellationToken token) _tcpServerRouterFactory.Start(); - _logger?.LogInformation($"Starting IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router."); + _logger?.LogInformation($"Starting IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> {_tcpServerRouterFactory.ServerTransportName} server ({_tcpServerRouterFactory.ServerAddress}) router."); return Task.CompletedTask; } public override Task Stop() { - _logger?.LogInformation($"Stopping IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router."); + _logger?.LogInformation($"Stopping IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> {_tcpServerRouterFactory.ServerTransportName} server ({_tcpServerRouterFactory.ServerAddress}) router."); return _tcpServerRouterFactory.Stop(); } @@ -988,7 +1092,7 @@ public override async Task CreateRouterAsync(CancellationToken token) using CancellationTokenSource cancelRouter = CancellationTokenSource.CreateLinkedTokenSource(token); // Get new server endpoint. - tcpServerStream = await _tcpServerRouterFactory.AcceptTcpStreamAsync(cancelRouter.Token).ConfigureAwait(false); + tcpServerStream = await _tcpServerRouterFactory.AcceptNetStreamAsync(cancelRouter.Token).ConfigureAwait(false); // Get new client endpoint. using var ipcClientStreamTask = _ipcClientRouterFactory.ConnectIpcStreamAsync(cancelRouter.Token); diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs index d7376c480a..74fe560f74 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs @@ -26,7 +26,7 @@ public static async Task runIpcClientTcpServerRouter(CancellationToken toke return await runRouter(token, new IpcClientTcpServerRouterFactory(ipcClient, tcpServer, runtimeTimeoutMs, tcpServerRouterFactory, logger), callbacks).ConfigureAwait(false); } - public static async Task runIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeoutMs, TcpServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory, ILogger logger, Callbacks callbacks) + public static async Task runIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeoutMs, NetServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory, ILogger logger, Callbacks callbacks) { return await runRouter(token, new IpcServerTcpServerRouterFactory(ipcServer, tcpServer, runtimeTimeoutMs, tcpServerRouterFactory, logger), callbacks).ConfigureAwait(false); } 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 2553b9e0d7..3d097fafcb 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj +++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index f4a9d4fae1..becaf95f34 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -30,8 +30,15 @@ internal sealed class ReversedDiagnosticsServer : IAsyncDisposable private bool _disposed = false; private Task _acceptTransportTask; - private bool _enableTcpIpProtocol = false; private IpcServerTransport _transport; + private Kind _kind = Kind.Ipc; + + public enum Kind + { + Tcp, + Ipc, + WebSocket, + } /// /// Constructs the instance with an endpoint bound @@ -57,16 +64,17 @@ public ReversedDiagnosticsServer(string address) /// 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 + /// + /// If kind is WebSocket, start a Kestrel web server. + /// Otherwise if kind is TcpIp as a supported protocol for ReversedDiagnosticServer. When Kind is Tcp, address will /// be analyzed and if on format host:port, ReversedDiagnosticServer will try to bind - /// a TcpIp listener to host and port. + /// a TcpIp listener to host and port, otherwise it will use a Unix domain socket or a Windows named pipe. /// /// - public ReversedDiagnosticsServer(string address, bool enableTcpIpProtocol) + public ReversedDiagnosticsServer(string address, Kind kind) { _address = address; - _enableTcpIpProtocol = enableTcpIpProtocol; + _kind = kind; } public async ValueTask DisposeAsync() @@ -134,7 +142,7 @@ public void Start(int maxConnections) throw new InvalidOperationException(nameof(ReversedDiagnosticsServer.Start) + " method can only be called once."); } - _transport = IpcServerTransport.Create(_address, maxConnections, _enableTcpIpProtocol, TransportCallback); + _transport = IpcServerTransport.Create(_address, maxConnections, _kind, TransportCallback); _acceptTransportTask = AcceptTransportAsync(_transport, _disposalSource.Token); @@ -371,6 +379,10 @@ private static bool TestStream(Stream stream) IntPtr.Zero, IntPtr.Zero); } + else if (stream is WebSocketServer.IWebSocketStreamAdapter adapter) + { + return adapter.IsConnected; + } return false; } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs new file mode 100644 index 0000000000..0b6c5d2b2b --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; + +// This interface abstracts the web socket server implementation used by dotnet-dsrouter +// in order to avoid a dependency on ASP.NET in the client library. +internal interface IWebSocketServer +{ + public Task AcceptConnection(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs new file mode 100644 index 0000000000..4cb6fb3d6c --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; + +// The streams returned by IWebSocketServer implement the usual .NET Stream class, but they also +// expose a way to check if the underlying websocket connection is still open. +internal interface IWebSocketStreamAdapter +{ + public bool IsConnected { get; } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerProvider.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerProvider.cs new file mode 100644 index 0000000000..312c1a25e2 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerProvider.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; + +// This interface allows dotnet-dsrouter to install a callback that will create IWebSocketServer instances. +// This is used to avoid a dependency on ASP.NET in the client library. +internal class WebSocketServerProvider +{ + internal static void SetProvider(Func provider) + { + _provider = provider; + } + + internal static IWebSocketServer GetWebSocketServerInstance() + { + return _provider(); + } + + private static Func _provider; +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs new file mode 100644 index 0000000000..c8b0834f83 --- /dev/null +++ b/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Net.WebSockets; +using HttpContext = Microsoft.AspNetCore.Http.HttpContext; +using System.Linq; + +namespace Microsoft.Diagnostics.WebSocketServer; + +// This is a simple embedded web server that listens for connections and only accepts web +// socket connections on a given path and hands them off to a handler callback. +// The code here configures a new generic host (IHost) with a lifetime that is controlled by +// the user of this class. +internal class EmbeddedWebSocketServer +{ + public record Options + { + public string Scheme { get; set; } = "http"; + public string Host { get; set; } = default; + public string Path { get; set; } = default!; + public string Port { get; set; } = default; + public LogLevel LogLevel { get; set; } = LogLevel.Information; + + public void Assign(Options other) + { + Scheme = other.Scheme; + Host = other.Host; + Port = other.Port; + Path = other.Path; + } + } + + private readonly IHost _host; + private EmbeddedWebSocketServer(IHost host) + { + _host = host; + } + + private static string[] MakeUrls(string scheme, string host, string port) => new string[] { $"{scheme}://{host}:{port}" }; + public static EmbeddedWebSocketServer CreateWebServer(Options options, Func connectionHandler) + { + var builder = new HostBuilder() + .ConfigureLogging(logging => + { + /* FIXME: use a delegating provider that sends the output to the dotnet-dsrouter LoggerFactory */ + logging.AddConsole().AddFilter(null, options.LogLevel); + }) + .ConfigureServices((ctx, services) => + { + services.AddCors(o => o.AddPolicy("AnyCors", builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .WithExposedHeaders("*"); + })); + services.AddRouting(); + services.Configure(localOptions => localOptions.Assign(options)); + }) + .ConfigureWebHostDefaults(webHostBuilder => + { + webHostBuilder.UseKestrel(); + webHostBuilder.Configure((/*context, */app) => ConfigureApplication(/*context,*/ app, connectionHandler)); + webHostBuilder.UseUrls(MakeUrls(options.Scheme, options.Host, options.Port)); + }) + .UseConsoleLifetime(options => + { + options.SuppressStatusMessages = true; + }); + + var host = builder.Build(); + + return new EmbeddedWebSocketServer(host); + } + + private static void ConfigureApplication(/*WebHostBuilderContext context,*/ IApplicationBuilder app, Func connectionHandler) + { + app.Use((context, next) => + { + context.Response.Headers.Add("Cross-Origin-Embedder-Policy", "require-corp"); + context.Response.Headers.Add("Cross-Origin-Opener-Policy", "same-origin"); + return next(); + }); + + app.UseCors("AnyCors"); + + app.UseWebSockets(); + app.UseRouter(router => + { + var options = router.ServiceProvider.GetRequiredService>().Value; + router.MapGet(options.Path, (context) => OnWebSocketGet(context, connectionHandler)); + }); + } + + public async Task StartWebServer(CancellationToken ct = default) + { + await _host.StartAsync(ct); + var logger = _host.Services.GetRequiredService>(); + var ipAddressSecure = _host.Services.GetRequiredService().Features.Get()?.Addresses + .Where(a => a.StartsWith("http:")) + .Select(a => new Uri(a)) + .Select(uri => $"{uri.Host}:{uri.Port}") + .FirstOrDefault(); + + logger.LogInformation("ip address is {IpAddressSecure}", ipAddressSecure); + } + + public async Task StopWebServer(CancellationToken ct = default) + { + await _host.StopAsync(ct); + } + + private static bool NeedsClose(WebSocketState state) + { + return state switch + { + WebSocketState.Open | WebSocketState.Connecting => true, + WebSocketState.Closed | WebSocketState.CloseReceived | WebSocketState.CloseSent => false, + WebSocketState.Aborted => false, + _ => true + }; + } + + private static async Task OnWebSocketGet(HttpContext context, Func connectionHandler) + { + if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = 400; + return; + } + var socket = await context.WebSockets.AcceptWebSocketAsync(); + if (connectionHandler != null) + await connectionHandler(context, socket, context.RequestAborted); + else + await Task.Delay(250); + if (NeedsClose(socket.State)) + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + } +} diff --git a/src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj b/src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj new file mode 100644 index 0000000000..873bb470ba --- /dev/null +++ b/src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + ;1591;1701 + Provides a WebSocket adapter to allow dotnet-dsrouter to talk to browser-based runtimes + + true + Diagnostic + $(Description) + false + true + + false + true + + + + + + + + + + + diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs new file mode 100644 index 0000000000..1fe85b7f01 --- /dev/null +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs @@ -0,0 +1,225 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Diagnostics.NETCore.Client.WebSocketServer; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +using System.Net.WebSockets; +using HttpContext = Microsoft.AspNetCore.Http.HttpContext; + +namespace Microsoft.Diagnostics.WebSocketServer; + +// This class implements the IWebSocketServer interface exposed by the Microsoft.Diagnostics.NETCore.Client library. +// It is responsible for coordinating between an underlying web server that creates web socket connections and the diagnostic server router that +// is used by dotnet-dsrouter to pass the diagnostic server connections to the diagnostic clients. +public class WebSocketServerImpl : IWebSocketServer +{ + private EmbeddedWebSocketServer _server = null; + private volatile int _started = 0; + + // Used to coordinate between the webserver accepting incoming websocket connections and the diagnostic server waiting for a stream to be available. + // This could be a deeper queue if we wanted to somehow allow multiple browser tabs to connect to the same dsrouter, but it's unclear what to do with them + // since on the other end we have a single IpcStream with a single diagnostic client. + private readonly Queue _acceptQueue = new Queue(); + private readonly LogLevel _logLevel; + + public WebSocketServerImpl(LogLevel logLevel) + { + _logLevel = logLevel; + } + + public async Task StartServer(string endpoint, CancellationToken cancellationToken) + { + if (Interlocked.CompareExchange(ref _started, 1, 0) != 0) + { + throw new InvalidOperationException("Server already started"); + } + + ParseWebSocketURL(endpoint, out Uri uri); + + EmbeddedWebSocketServer.Options options = new() + { + Scheme = uri.Scheme, + Host = uri.Host, + Port = uri.Port.ToString(), + Path = uri.PathAndQuery, + LogLevel = _logLevel, + }; + _server = EmbeddedWebSocketServer.CreateWebServer(options, HandleWebSocket); + + await _server.StartWebServer(cancellationToken); + } + + public async Task StopServer(CancellationToken cancellationToken) + { + if (_started == 0) + { + throw new InvalidOperationException("Server not started"); + } + if (_server == null) + return; + await _server.StopWebServer(cancellationToken); + _server = null; + } + + public async Task HandleWebSocket(HttpContext context, WebSocket webSocket, CancellationToken cancellationToken) + { + // Called by the web server when a new websocket connection is established. We put the connection into our queue of accepted connections + // and wait until someone uses it and disposes of the connection. + await QueueWebSocketUntilClose(context, webSocket, cancellationToken); + } + + internal async Task QueueWebSocketUntilClose(HttpContext context, WebSocket webSocket, CancellationToken cancellationToken) + { + // we have to "keep the middleware alive" until we're done with the websocket. + // make a TCS that will be signaled when the stream is disposed. + var streamDisposedTCS = new TaskCompletionSource(cancellationToken); + await _acceptQueue.Enqueue(new Conn(context, webSocket, streamDisposedTCS), cancellationToken); + await streamDisposedTCS.Task; + } + + internal Task GetOrRequestConnection(CancellationToken cancellationToken) + { + // This is called from the diagnostic server when it is ready to start talking to a connection. We give them back a connection from + // the ones the web server has accepted, or block until the web server queues a new one. + return _acceptQueue.Dequeue(cancellationToken); + } + + public async Task AcceptConnection(CancellationToken cancellationToken) + { + Conn conn = await GetOrRequestConnection(cancellationToken); + return conn.GetStream(); + } + + // Single-element queue where both queueing and dequeueing are async operations that wait until + // the queue has capacity (or an item, respectively). + internal class Queue + { + private T _obj; + private readonly SemaphoreSlim _empty; + private readonly SemaphoreSlim _full; + private readonly SemaphoreSlim _objLock; + + public Queue() + { + _obj = default; + int capacity = 1; + _empty = new SemaphoreSlim(capacity, capacity); + _full = new SemaphoreSlim(0, capacity); + _objLock = new SemaphoreSlim(1, 1); + } + + public async Task Enqueue(T t, CancellationToken cancellationToken) + { + bool locked = false; + try + { + await _empty.WaitAsync(cancellationToken); + await _objLock.WaitAsync(cancellationToken); + locked = true; + _obj = t; + } + finally + { + if (locked) + { + _objLock.Release(); + _full.Release(); + } + } + } + + public async Task Dequeue(CancellationToken cancellationToken) + { + bool locked = false; + try + { + await _full.WaitAsync(cancellationToken); + await _objLock.WaitAsync(cancellationToken); + locked = true; + T t = _obj; + _obj = default; + return t; + } + finally + { + if (locked) + { + _objLock.Release(); + _empty.Release(); + } + } + } + } + + private static void ParseWebSocketURL(string endPoint, out Uri uri) + { + string uriToParse; + // 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) + { + // FIXME: This is a workaround for the fact that Uri.Host is not set for wildcard host. + throw new ArgumentException("Wildcard host is not supported for WebSocket endpoints"); + } + else + { + uriToParse = endPoint; + } + + string[] supportedSchemes = new string[] { "ws", "wss", "http", "https" }; + + if (!string.IsNullOrEmpty(uriToParse) && Uri.TryCreate(uriToParse, UriKind.Absolute, out uri)) + { + bool supported = false; + foreach (string scheme in supportedSchemes) + { + if (string.Compare(uri.Scheme, scheme, StringComparison.InvariantCultureIgnoreCase) == 0) + { + supported = true; + break; + } + } + if (!supported) + { + throw new ArgumentException(string.Format("Unsupported Uri schema, \"{0}\"", uri.Scheme)); + } + return; + } + else + { + throw new ArgumentException(string.Format("Could not parse {0} into host, port", endPoint)); + } + } + + + + // An abstraction encapsulating an open websocket connection. + internal class Conn + { + private readonly WebSocket _webSocket; + private readonly HttpContext _context; + private readonly TaskCompletionSource _streamDisposed; + + public Conn(HttpContext context, WebSocket webSocket, TaskCompletionSource streamDisposed) + { + _context = context; + _webSocket = webSocket; + _streamDisposed = streamDisposed; + } + + public Stream GetStream() + { + return new WebSocketStreamAdapter(_webSocket, OnStreamDispose); + } + + private void OnStreamDispose() + { + _streamDisposed.SetResult(); + } + } +} diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs new file mode 100644 index 0000000000..89beacc54b --- /dev/null +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.IO; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Diagnostics.NETCore.Client.WebSocketServer; + +namespace Microsoft.Diagnostics.WebSocketServer; + +internal class WebSocketStreamAdapter : Stream, IWebSocketStreamAdapter +{ + private readonly WebSocket _webSocket; + private readonly Action _onDispose; + + public WebSocket WebSocket { get => _webSocket; } + public WebSocketStreamAdapter(WebSocket webSocket, Action onDispose) + { + _webSocket = webSocket; + _onDispose = onDispose; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override long Length => throw new NotImplementedException(); + + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer, offset, count), cancellationToken); + return result.Count; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + { + var result = await _webSocket.ReceiveAsync(buffer, cancellationToken); + return result.Count; + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory memory, CancellationToken cancellationToken) + { + return _webSocket.SendAsync(memory, WebSocketMessageType.Binary, true, cancellationToken); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _onDispose(); + _webSocket.Dispose(); + } + } + + bool IWebSocketStreamAdapter.IsConnected => _webSocket.State == WebSocketState.Open; +} diff --git a/src/Tools/dotnet-dsrouter/ADBTcpRouterFactory.cs b/src/Tools/dotnet-dsrouter/ADBTcpRouterFactory.cs index f74159309d..b8bc3ea06d 100644 --- a/src/Tools/dotnet-dsrouter/ADBTcpRouterFactory.cs +++ b/src/Tools/dotnet-dsrouter/ADBTcpRouterFactory.cs @@ -126,7 +126,7 @@ public ADBTcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger public override void Start() { // Enable port reverse. - _ownsPortReverse = ADBCommandExec.AdbAddPortReverse(_port, _logger); + _ownsPortReverse = ADBCommandExec.AdbAddPortReverse(_port, Logger); base.Start(); } @@ -136,7 +136,7 @@ public override async Task Stop() await base.Stop().ConfigureAwait(false); // Disable port reverse. - ADBCommandExec.AdbRemovePortReverse(_port, _ownsPortReverse, _logger); + ADBCommandExec.AdbRemovePortReverse(_port, _ownsPortReverse, Logger); _ownsPortReverse = false; } } diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 0563cc3b4a..e723464c05 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -45,254 +45,290 @@ public void OnRouterStopped() 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, string forwardPort) + // Common behavior for different commands used by CommonRunLoop + internal abstract class SpecificRunnerBase { - checkLoopbackOnly(tcpServer); - - using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); - using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); + public DiagnosticsServerRouterLauncher Launcher { get; } = new DiagnosticsServerRouterLauncher(); - 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; + public LogLevel LogLevel { get; } + // runners can override if necessary + public virtual ILoggerFactory ConfigureLogging() + { + var factory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel); + builder.AddSimpleConsole(configure => + { + configure.IncludeScopes = true; + }); + }); + return factory; + } - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + protected SpecificRunnerBase(LogLevel logLevel) + { + LogLevel = logLevel; + } - Launcher.SuspendProcess = true; - Launcher.ConnectMode = true; - Launcher.Verbose = logLevel != LogLevel.Information; - Launcher.CommandToken = token; + protected SpecificRunnerBase(string logLevel) : this(ParseLogLevel(logLevel)) + { + } - var logger = factory.CreateLogger("dotnet-dsrouter"); + public abstract void ConfigureLauncher(CancellationToken cancellationToken); - TcpServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = TcpServerRouterFactory.CreateDefaultInstance; - if (!string.IsNullOrEmpty(forwardPort)) + protected static LogLevel ParseLogLevel(string verbose) { - if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) - { - tcpServerRouterFactory = ADBTcpServerRouterFactory.CreateADBInstance; - } - else - { - logger.LogError($"Unknown port forwarding argument, {forwardPort}. Only Android port fowarding is supported for TcpServer mode. Ignoring --forward-port argument."); - } + 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; + return logLevel; } - var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, Launcher); - - while (!linkedCancelToken.IsCancellationRequested) + // The basic run loop: configure logging and the launcher, then create the router and run it until it exits or the user interrupts + public async Task CommonRunLoop(Func> createRouterTask, CancellationToken token) { - await Task.WhenAny(routerTask, Task.Delay(250)).ConfigureAwait(false); - if (routerTask.IsCompleted) - break; + using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); + using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); + + using ILoggerFactory loggerFactory = ConfigureLogging(); + + ConfigureLauncher(token); + + var logger = loggerFactory.CreateLogger("dotnet-dsrouter"); - if (!Console.IsInputRedirected && Console.KeyAvailable) + var routerTask = createRouterTask(logger, Launcher, linkedCancelToken); + + while (!linkedCancelToken.IsCancellationRequested) { - ConsoleKey cmd = Console.ReadKey(true).Key; - if (cmd == ConsoleKey.Q) - { - cancelRouterTask.Cancel(); + 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; } + } - return routerTask.Result; + class IpcClientTcpServerRunner : SpecificRunnerBase + { + public IpcClientTcpServerRunner(string verbose) : base(verbose) { } + + public override void ConfigureLauncher(CancellationToken cancellationToken) + { + Launcher.SuspendProcess = true; + Launcher.ConnectMode = true; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; + } + + public override ILoggerFactory ConfigureLogging() + { + var factory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel); + builder.AddConsole(); + }); + return factory; + } } - public async Task RunIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeout, string verbose, string forwardPort) + public async Task RunIpcClientTcpServerRouter(CancellationToken token, string ipcClient, string tcpServer, int runtimeTimeout, string verbose, string forwardPort) { 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; + var runner = new IpcClientTcpServerRunner(verbose); - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + { + NetServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = ChooseTcpServerRouterFactory(forwardPort, logger); - Launcher.SuspendProcess = false; - Launcher.ConnectMode = true; - Launcher.Verbose = logLevel != LogLevel.Information; - Launcher.CommandToken = token; + var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); + } - var logger = factory.CreateLogger("dotnet-dsrouter"); + class IpcServerTcpServerRunner : SpecificRunnerBase + { + public IpcServerTcpServerRunner(string verbose) : base(verbose) { } - TcpServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = TcpServerRouterFactory.CreateDefaultInstance; - if (!string.IsNullOrEmpty(forwardPort)) + public override void ConfigureLauncher(CancellationToken cancellationToken) { - if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) - { - tcpServerRouterFactory = ADBTcpServerRouterFactory.CreateADBInstance; - } - else - { - logger.LogError($"Unknown port forwarding argument, {forwardPort}. Only Android port fowarding is supported for TcpServer mode. Ignoring --forward-port argument."); - } + Launcher.SuspendProcess = false; + Launcher.ConnectMode = true; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; } + } - if (string.IsNullOrEmpty(ipcServer)) - ipcServer = GetDefaultIpcServerPath(logger); + public async Task RunIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeout, string verbose, string forwardPort) + { + checkLoopbackOnly(tcpServer); - var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, Launcher); + var runner = new IpcServerTcpServerRunner(verbose); - while (!linkedCancelToken.IsCancellationRequested) + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => { - await Task.WhenAny(routerTask, Task.Delay(250)).ConfigureAwait(false); - if (routerTask.IsCompleted) - break; + NetServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = ChooseTcpServerRouterFactory(forwardPort, logger); - if (!Console.IsInputRedirected && Console.KeyAvailable) - { - ConsoleKey cmd = Console.ReadKey(true).Key; - if (cmd == ConsoleKey.Q) - { - cancelRouterTask.Cancel(); - break; - } - } - } + if (string.IsNullOrEmpty(ipcServer)) + ipcServer = GetDefaultIpcServerPath(logger); - return routerTask.Result; + var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); } - public async Task RunIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeout, string verbose, string forwardPort) + class IpcServerTcpClientRunner : SpecificRunnerBase { - using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); - using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); + public IpcServerTcpClientRunner(string verbose) : base(verbose) { } + + public override void ConfigureLauncher(CancellationToken cancellationToken) + { + Launcher.SuspendProcess = false; + Launcher.ConnectMode = false; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; + } + } - 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; + public async Task RunIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeout, string verbose, string forwardPort) + { + var runner = new IpcServerTcpClientRunner(verbose); + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + { + TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = ChooseTcpClientRouterFactory(forwardPort, logger); - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + if (string.IsNullOrEmpty(ipcServer)) + ipcServer = GetDefaultIpcServerPath(logger); - 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, tcpClientRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); + } - var logger = factory.CreateLogger("dotnet-dsrouter"); + class IpcClientTcpClientRunner : SpecificRunnerBase + { + public IpcClientTcpClientRunner(string verbose) : base(verbose) { } - TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = TcpClientRouterFactory.CreateDefaultInstance; - if (!string.IsNullOrEmpty(forwardPort)) + public override void ConfigureLauncher(CancellationToken cancellationToken) { - if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) - { - tcpClientRouterFactory = ADBTcpClientRouterFactory.CreateADBInstance; - } - else if (string.Compare(forwardPort, "ios", StringComparison.OrdinalIgnoreCase) == 0) - { - tcpClientRouterFactory = USBMuxTcpClientRouterFactory.CreateUSBMuxInstance; - } - else - { - logger.LogError($"Unknown port forwarding argument, {forwardPort}. Ignoring --forward-port argument."); - } + Launcher.SuspendProcess = true; + Launcher.ConnectMode = false; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; } + } - if (string.IsNullOrEmpty(ipcServer)) - ipcServer = GetDefaultIpcServerPath(logger); + public async Task RunIpcClientTcpClientRouter(CancellationToken token, string ipcClient, string tcpClient, int runtimeTimeout, string verbose, string forwardPort) + { + var runner = new IpcClientTcpClientRunner(verbose); + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + { + TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = ChooseTcpClientRouterFactory(forwardPort, logger); - var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpClientRouter(linkedCancelToken.Token, ipcServer, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, logger, Launcher); + var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpClientRouter(linkedCancelToken.Token, ipcClient, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); + } - while (!linkedCancelToken.IsCancellationRequested) - { - await Task.WhenAny(routerTask, Task.Delay(250)).ConfigureAwait(false); - if (routerTask.IsCompleted) - break; + class IpcServerWebSocketServerRunner : SpecificRunnerBase + { + public IpcServerWebSocketServerRunner(string verbose) : base(verbose) { } - if (!Console.IsInputRedirected && Console.KeyAvailable) - { - ConsoleKey cmd = Console.ReadKey(true).Key; - if (cmd == ConsoleKey.Q) - { - cancelRouterTask.Cancel(); - break; - } - } + public override void ConfigureLauncher(CancellationToken cancellationToken) + { + Launcher.SuspendProcess = false; + Launcher.ConnectMode = true; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; } - - return routerTask.Result; } - public async Task RunIpcClientTcpClientRouter(CancellationToken token, string ipcClient, string tcpClient, int runtimeTimeout, string verbose, string forwardPort) + public async Task RunIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocket, int runtimeTimeout, string verbose) { - using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); - using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); + var runner = new IpcServerWebSocketServerRunner(verbose); - 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; + WebSocketServer.WebSocketServerImpl server = new(runner.LogLevel); - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + NETCore.Client.WebSocketServer.WebSocketServerProvider.SetProvider(() => server); - Launcher.SuspendProcess = true; - Launcher.ConnectMode = false; - Launcher.Verbose = logLevel != LogLevel.Information; - Launcher.CommandToken = token; + try + { + Task _ = Task.Run(() => server.StartServer(webSocket, token)); - var logger = factory.CreateLogger("dotnet-dsrouter"); + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + { + NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; - TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = TcpClientRouterFactory.CreateDefaultInstance; - if (!string.IsNullOrEmpty(forwardPort)) + if (string.IsNullOrEmpty(ipcServer)) + ipcServer = GetDefaultIpcServerPath(logger); + + var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); + } + finally { - if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) - { - tcpClientRouterFactory = ADBTcpClientRouterFactory.CreateADBInstance; - } - else if (string.Compare(forwardPort, "ios", StringComparison.OrdinalIgnoreCase) == 0) - { - tcpClientRouterFactory = USBMuxTcpClientRouterFactory.CreateUSBMuxInstance; - } - else - { - logger.LogError($"Unknown port forwarding argument, {forwardPort}. Ignoring --forward-port argument."); - } + await server.StopServer(token); } + } - var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpClientRouter(linkedCancelToken.Token, ipcClient, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, logger, Launcher); + class IpcClientWebSocketServerRunner : SpecificRunnerBase + { + public IpcClientWebSocketServerRunner(string verbose) : base(verbose) { } + + public override void ConfigureLauncher(CancellationToken cancellationToken) + { + Launcher.SuspendProcess = true; + Launcher.ConnectMode = true; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; + } + } - while (!linkedCancelToken.IsCancellationRequested) + public async Task RunIpcClientWebSocketServerRouter(CancellationToken token, string ipcClient, string webSocket, int runtimeTimeout, string verbose) + { + var runner = new IpcClientWebSocketServerRunner(verbose); + + WebSocketServer.WebSocketServerImpl server = new(runner.LogLevel); + + NETCore.Client.WebSocketServer.WebSocketServerProvider.SetProvider(() => server); + + try { - await Task.WhenAny(routerTask, Task.Delay(250)).ConfigureAwait(false); - if (routerTask.IsCompleted) - break; + Task _ = Task.Run(() => server.StartServer(webSocket, token)); - if (!Console.IsInputRedirected && Console.KeyAvailable) + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => { - ConsoleKey cmd = Console.ReadKey(true).Key; - if (cmd == ConsoleKey.Q) - { - cancelRouterTask.Cancel(); - break; - } - } - } + NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; - return routerTask.Result; + var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); + } + finally + { + await server.StopServer(token); + } } + static string GetDefaultIpcServerPath(ILogger logger) { int processId = Process.GetCurrentProcess().Id; @@ -330,6 +366,45 @@ static string GetDefaultIpcServerPath(ILogger logger) return path; } + + } + + static TcpClientRouterFactory.CreateInstanceDelegate ChooseTcpClientRouterFactory(string forwardPort, ILogger logger) + { + TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = TcpClientRouterFactory.CreateDefaultInstance; + if (!string.IsNullOrEmpty(forwardPort)) + { + if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpClientRouterFactory = ADBTcpClientRouterFactory.CreateADBInstance; + } + else if (string.Compare(forwardPort, "ios", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpClientRouterFactory = USBMuxTcpClientRouterFactory.CreateUSBMuxInstance; + } + else + { + logger.LogError($"Unknown port forwarding argument, {forwardPort}. Ignoring --forward-port argument."); + } + } + return tcpClientRouterFactory; + } + + static NetServerRouterFactory.CreateInstanceDelegate ChooseTcpServerRouterFactory(string forwardPort, ILogger logger) + { + NetServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = TcpServerRouterFactory.CreateDefaultInstance; + if (!string.IsNullOrEmpty(forwardPort)) + { + if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpServerRouterFactory = ADBTcpServerRouterFactory.CreateADBInstance; + } + else + { + logger.LogError($"Unknown port forwarding argument, {forwardPort}. Only Android port fowarding is supported for TcpServer mode. Ignoring --forward-port argument."); + } + } + return tcpServerRouterFactory; } static void checkLoopbackOnly(string tcpServer) diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index 8e0f922ea3..cd91232494 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -22,6 +22,10 @@ internal class Program delegate Task DiagnosticsServerIpcServerTcpClientRouterDelegate(CancellationToken ct, string ipcServer, string tcpClient, int runtimeTimeoutS, string verbose, string forwardPort); delegate Task DiagnosticsServerIpcClientTcpClientRouterDelegate(CancellationToken ct, string ipcClient, string tcpClient, int runtimeTimeoutS, string verbose, string forwardPort); + delegate Task DiagnosticsServerIpcServerWebSocketServerRouterDelegate(CancellationToken ct, string ipcServer, string webSocket, int runtimeTimeoutS, string verbose); + + delegate Task DiagnosticsServerIpcClientWebSocketServerRouterDelegate(CancellationToken ct, string ipcClient, string webSocket, int runtimeTimeoutS, string verbose); + private static Command IpcClientTcpServerRouterCommand() => new Command( name: "client-server", @@ -61,6 +65,31 @@ private static Command IpcServerTcpClientRouterCommand() => IpcServerAddressOption(), TcpClientAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() }; + private static Command IpcServerWebSocketServerRouterCommand() => + new Command( + name: "server-websocket", + description: "Starts a .NET application Diagnostic Server routing local IPC client <--> remote WebSocket client. " + + "Router is configured using an IPC server (connecting to by diagnostic tools) " + + "and a WebSocket server (accepting runtime WebSocket client).") + { + HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerWebSocketServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerWebSocketServerRouter).GetCommandHandler(), + // Options + IpcServerAddressOption(), WebSocketURLAddressOption(), RuntimeTimeoutOption(), VerboseOption() + }; + + private static Command IpcClientWebSocketServerRouterCommand() => + new Command( + name: "client-websocket", + description: "Starts a .NET application Diagnostic Server routing local IPC server <--> remote WebSocket client. " + + "Router is configured using an IPC client (connecting diagnostic tool IPC server) " + + "and a WebSocket server (accepting runtime WebSocket client).") + { + // Handler + HandlerDescriptor.FromDelegate((DiagnosticsServerIpcClientWebSocketServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcClientWebSocketServerRouter).GetCommandHandler(), + // Options + IpcClientAddressOption(), WebSocketURLAddressOption(), RuntimeTimeoutOption(), VerboseOption() + }; + private static Command IpcClientTcpClientRouterCommand() => new Command( name: "client-client", @@ -115,6 +144,15 @@ private static Option TcpServerAddressOption() => Argument = new Argument(name: "tcpServer", getDefaultValue: () => "") }; + private static Option WebSocketURLAddressOption() => + new Option( + aliases: new[] { "--web-socket", "-ws" }, + description: "The router WebSocket address using format ws://[host]:[port]/[path] or wss://[host]:[port]/[path]. " + + "Launch app with WasmExtraConfig property specifying diagnostic_options with a server connect_url") + { + Argument = new Argument(name: "webSocketURI", getDefaultValue: () => "") + }; + private static Option RuntimeTimeoutOption() => new Option( aliases: new[] { "--runtime-timeout", "-rt" }, @@ -154,6 +192,8 @@ private static int Main(string[] args) .AddCommand(IpcServerTcpServerRouterCommand()) .AddCommand(IpcServerTcpClientRouterCommand()) .AddCommand(IpcClientTcpClientRouterCommand()) + .AddCommand(IpcServerWebSocketServerRouterCommand()) + .AddCommand(IpcClientWebSocketServerRouterCommand()) .UseDefaults() .Build(); diff --git a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj index 187781edb7..b0a498179f 100644 --- a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj +++ b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6.0 true dotnet-dsrouter Microsoft.Diagnostics.Tools.DiagnosticsServerRouter @@ -18,13 +18,14 @@ + - - + +