From d147761f20896785ab1ba35d7e70cb358dd668f2 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 12 Jul 2022 10:29:12 -0400 Subject: [PATCH 01/30] Add "dsrouter server-websocket" command boilerplate --- .../DiagnosticsServerRouterCommands.cs | 30 +++++++++++++++++++ src/Tools/dotnet-dsrouter/Program.cs | 24 +++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 0563cc3b4a..1760e7044f 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -293,6 +293,36 @@ public async Task RunIpcClientTcpClientRouter(CancellationToken token, stri return routerTask.Result; } + public async Task RunIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocketURL, int runtimeTimeout, string verbose, string forwardPort) + { + checkLoopbackOnly(webSocketURL); + + using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); + using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); + + LogLevel logLevel = LogLevel.Information; + if (string.Compare(verbose, "debug", StringComparison.OrdinalIgnoreCase) == 0) + logLevel = LogLevel.Debug; + else if (string.Compare(verbose, "trace", StringComparison.OrdinalIgnoreCase) == 0) + logLevel = LogLevel.Trace; + + using var factory = new LoggerFactory(); + factory.AddConsole(logLevel, false); + + Launcher.SuspendProcess = false; + Launcher.ConnectMode = true; + Launcher.Verbose = logLevel != LogLevel.Information; + Launcher.CommandToken = token; + + var logger = factory.CreateLogger("dotnet-dsrouter"); + + Console.WriteLine("blah webSocketURL is '{0}'", webSocketURL == null ? "null" : webSocketURL); + logger.LogInformation("started with options: '{ipcServer}' '{webSocketURL}' '{runtimeTimeout}' '{verbose}' '{forwardPort}'", ipcServer, webSocketURL, runtimeTimeout, verbose, forwardPort); + + await Task.Delay(0); + return 0; + } + static string GetDefaultIpcServerPath(ILogger logger) { int processId = Process.GetCurrentProcess().Id; diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index 8e0f922ea3..04588ff553 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -22,6 +22,8 @@ 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, string forwardPort); + private static Command IpcClientTcpServerRouterCommand() => new Command( name: "client-server", @@ -61,6 +63,18 @@ 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(), ForwardPortOption() + }; + private static Command IpcClientTcpClientRouterCommand() => new Command( name: "client-client", @@ -115,6 +129,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] or wss://[host]:[port]. " + + "Launch app with WasmExtraConfig property specifying diagnostic_options with a server connect_url") + { + Argument = new Argument(name: "webSocket", getDefaultValue: () => "") + }; + private static Option RuntimeTimeoutOption() => new Option( aliases: new[] { "--runtime-timeout", "-rt" }, @@ -154,6 +177,7 @@ private static int Main(string[] args) .AddCommand(IpcServerTcpServerRouterCommand()) .AddCommand(IpcServerTcpClientRouterCommand()) .AddCommand(IpcClientTcpClientRouterCommand()) + .AddCommand(IpcServerWebSocketServerRouterCommand()) .UseDefaults() .Build(); From cb2d059c7839892e327a293fa2671b1d96ad816f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 13 Jul 2022 15:12:13 -0400 Subject: [PATCH 02/30] WIP: server-websocket mode, now with web server --- .../DiagnosticsIpc/IpcServerTransport.cs | 8 +- .../DiagnosticsIpc/IpcWebSocketEndPoint.cs | 76 ++++ .../IpcWebSocketServerTransport.cs | 111 ++++++ .../DiagnosticsServerRouterFactory.cs | 342 +++++++++++++++++- .../DiagnosticsServerRouterRunner.cs | 5 + ...icrosoft.Diagnostics.NETCore.Client.csproj | 1 + .../ReversedDiagnosticsServer.cs | 22 +- .../WebSocketServer/IWebSocketServer.cs | 19 + .../DiagnosticsServerRouterCommands.cs | 88 ++++- src/Tools/dotnet-dsrouter/Program.cs | 8 +- src/Tools/dotnet-dsrouter/WebSocketServer.cs | 293 +++++++++++++++ .../dotnet-dsrouter/WebSocketStreamAdapter.cs | 93 +++++ .../dotnet-dsrouter/dotnet-dsrouter.csproj | 10 +- 13 files changed, 1043 insertions(+), 33 deletions(-) create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketEndPoint.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs create mode 100644 src/Tools/dotnet-dsrouter/WebSocketServer.cs create mode 100644 src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs 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/IpcWebSocketEndPoint.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketEndPoint.cs new file mode 100644 index 0000000000..72ec73d80e --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketEndPoint.cs @@ -0,0 +1,76 @@ +// 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.Net; +using System.Net.Sockets; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal sealed class IpcWebSocketEndPoint + { + public Uri EndPoint { get; } + + public static bool IsWebSocketEndPoint(string endPoint) + { + bool result = true; + + try + { + ParseWebSocketEndPoint(endPoint, out _); + } + catch (Exception) + { + result = false; + } + + return result; + } + + public IpcWebSocketEndPoint(string endPoint) + { + ParseWebSocketEndPoint(endPoint, out Uri uri); + EndPoint = uri; + } + + private static void ParseWebSocketEndPoint(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)); + } + } + } +} 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..e292d34567 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs @@ -0,0 +1,111 @@ +// 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 +{ + private static Singleton singleton = new Singleton(); + private readonly CancellationTokenSource _cancellation; + private readonly int _maxConnections; + private readonly IpcWebSocketEndPoint _endPoint; + public IpcWebSocketServerTransport(string address, int maxAllowedConnections, IIpcServerTransportCallbackInternal transportCallback = null) + : base(transportCallback) + { + _maxConnections = maxAllowedConnections; + _endPoint = new IpcWebSocketEndPoint(address); + _cancellation = new CancellationTokenSource(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _cancellation.Cancel(); + + // _stream.Dispose(); + singleton.DropRef(); + + _cancellation.Dispose(); + } + } + + public override async Task AcceptAsync(CancellationToken token) + { + if (singleton.AddRef()) + { + await singleton.StartServer(_endPoint, token); + Console.WriteLine("starting server AAA"); + } + Console.WriteLine("AcceptAsync"); + Stream s = await singleton.AcceptConnection(token); + return s; + } + + internal class Singleton + { + private volatile int _refCount; + public bool ServerRunning { get; internal set; } = false; + public bool ServerStopping { get; internal set; } = false; + public WebSocketServer.IWebSocketServer server { get; internal set; } = null; + internal Singleton() + { + _refCount = 0; + } + + public bool AddRef() + { + return Interlocked.Increment(ref _refCount) == 1; + } + + public void DropRef() + { + if (_refCount == 0) + { + throw new InvalidOperationException("DropRef called more times than AddRef"); + } + if (Interlocked.Decrement(ref _refCount) == 0) + { + Console.WriteLine("stopping server AAA"); + StopServer().Wait(); + } + } + + internal async Task StartServer(IpcWebSocketEndPoint endPoint, CancellationToken token) + { + if (ServerStopping) + { + return; + } + ServerRunning = true; + string typeName = Environment.GetEnvironmentVariable("DIAGNOSTICS_SERVER_WEBSOCKET_SERVER_TYPE"); + Console.WriteLine("typeName: {0}", typeName); + Type t = Type.GetType(typeName); + if (t == null) + { + Console.WriteLine("no type found {0}", typeName); + throw new Exception("Unable to find type " + typeName); + } + server = (WebSocketServer.IWebSocketServer)Activator.CreateInstance(t); + await server.StartServer(endPoint.EndPoint, token); + await Task.Delay(1000); + } + + internal async Task StopServer(CancellationToken token = default) + { + ServerStopping = true; + await server.StopServer(token); + ServerRunning = false; + } + + internal async Task AcceptConnection(CancellationToken token) + { + 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..78c5642864 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -193,7 +193,7 @@ public TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger lo if (runtimeTimeoutMs != Timeout.Infinite) RuntimeTimeoutMs = runtimeTimeoutMs; - _tcpServer = new ReversedDiagnosticsServer(_tcpServerAddress, enableTcpIpProtocol : true); + _tcpServer = new ReversedDiagnosticsServer(_tcpServerAddress, ReversedDiagnosticsServer.Kind.Tcp); _tcpServerEndpointInfo = new IpcEndpointInfo(); _tcpServer.TransportCallback = this; } @@ -282,6 +282,149 @@ public void CreatedNewServer(EndPoint localEP) } } + /// + /// This class represent a WebSocket server endpoint used when building up router instances. + /// + internal class WebSocketServerRouterFactory : IIpcServerTransportCallbackInternal + { + protected readonly ILogger _logger; + + IpcWebSocketEndPoint _webSocketEndPoint; + + ReversedDiagnosticsServer _webSocketServer; + IpcEndpointInfo _webSocketEndpointInfo; + + bool _auto_shutdown; + + int RuntimeTimeoutMs { get; set; } = 60000; + int TcpServerTimeoutMs { get; set; } = 5000; + + public Guid RuntimeInstanceId + { + get { return _webSocketEndpointInfo.RuntimeInstanceCookie; } + } + + public int RuntimeProcessId + { + get { return _webSocketEndpointInfo.ProcessId; } + } + + public Uri WebSocketURL + { + get { return _webSocketEndPoint.EndPoint; } + } + + public delegate WebSocketServerRouterFactory CreateInstanceDelegate(string webSocketURL, int runtimeTimeoutMs, ILogger logger); + + public static WebSocketServerRouterFactory CreateDefaultInstance(string webSocketURL, int runtimeTimeoutMs, ILogger logger) + { + return new WebSocketServerRouterFactory(webSocketURL, runtimeTimeoutMs, logger); + } + + public WebSocketServerRouterFactory(string webSocketURL, int runtimeTimeoutMs, ILogger logger) + { + _logger = logger; + + _webSocketEndPoint = new IpcWebSocketEndPoint(string.IsNullOrEmpty(webSocketURL) ? "ws://127.0.0.1:8088/diagnostics" : webSocketURL); + + _auto_shutdown = runtimeTimeoutMs != Timeout.Infinite; + if (runtimeTimeoutMs != Timeout.Infinite) + RuntimeTimeoutMs = runtimeTimeoutMs; + + _webSocketServer = new ReversedDiagnosticsServer(_webSocketEndPoint.EndPoint.ToString(), ReversedDiagnosticsServer.Kind.WebSocket); + _webSocketEndpointInfo = new IpcEndpointInfo(); + _webSocketServer.TransportCallback = this; + } + + public virtual void Start() + { + _logger.LogInformation("Starting web socket server"); + _webSocketServer.Start(); + } + + public virtual async Task Stop() + { + _logger.LogInformation("Stopping web socket server"); + await _webSocketServer.DisposeAsync().ConfigureAwait(false); + } + + public void Reset() + { + if (_webSocketEndpointInfo.Endpoint != null) + { + _logger.LogInformation("Resetting the web socket server"); + _webSocketServer.RemoveConnection(_webSocketEndpointInfo.RuntimeInstanceCookie); + _webSocketEndpointInfo = new IpcEndpointInfo(); + } + } + + public async Task AcceptWebSocketStreamAsync(CancellationToken token) + { + Stream tcpServerStream; + + _logger?.LogDebug($"Waiting for a new WebSocket connection at endpoint \"{WebSocketURL}\"."); + + if (_webSocketEndpointInfo.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); + _webSocketEndpointInfo = await _webSocketServer.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 _webSocketEndpointInfo.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; + } + + public void CreatedNewServer(EndPoint localEP) + { + // FIXME: anything to do here? + //if (localEP is IPEndPoint ipEP) + // _webSocketEndPoint = _webSocketEndPoint.Replace(":0", string.Format(":{0}", ipEP.Port)); + } + + } + /// /// This class represent a TCP/IP client endpoint used when building up router instances. /// @@ -445,7 +588,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() @@ -1292,6 +1435,201 @@ private async Task UpdateRuntimeInfo(CancellationToken token) } } + /// + /// This class creates IPC Server - WebSocket Server router instances. + /// Supports NamedPipes/UnixDomainSocket server and WebSocket server. + /// + internal class IpcServerWebSocketServerRouterFactory : DiagnosticsServerRouterFactory + { + ILogger _logger; + WebSocketServerRouterFactory _webSocketServerRouterFactory; + IpcServerRouterFactory _ipcServerRouterFactory; + + public IpcServerWebSocketServerRouterFactory(string ipcServer, string webSocketURL, int runtimeTimeoutMs, WebSocketServerRouterFactory.CreateInstanceDelegate factory, ILogger logger) + { + _logger = logger; + _webSocketServerRouterFactory = factory(webSocketURL, runtimeTimeoutMs, logger); + _ipcServerRouterFactory = new IpcServerRouterFactory(ipcServer, logger); + } + + public override string IpcAddress + { + get + { + return _ipcServerRouterFactory.IpcServerPath; + } + } + + public Uri WebSocketURL + { + get + { + return _webSocketServerRouterFactory.WebSocketURL; + } + } + + public override string TcpAddress => WebSocketURL.ToString(); + + public override ILogger Logger + { + get + { + return _logger; + } + } + + public override Task Start(CancellationToken token) + { + _webSocketServerRouterFactory.Start(); + _ipcServerRouterFactory.Start(); + + _logger?.LogInformation($"Starting IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> WebSocket server ({_webSocketServerRouterFactory.WebSocketURL}) router."); + + return Task.CompletedTask; + } + + public override Task Stop() + { + _logger?.LogInformation($"Stopping IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> WebSocket server ({_webSocketServerRouterFactory.WebSocketURL}) router."); + _ipcServerRouterFactory.Stop(); + return _webSocketServerRouterFactory.Stop(); + } + + public override void Reset() + { + _webSocketServerRouterFactory.Reset(); + } + + public override async Task CreateRouterAsync(CancellationToken token) + { + Stream websocketServerStream = 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 webSocketServerStreamTask = _webSocketServerRouterFactory.AcceptWebSocketStreamAsync(cancelRouter.Token); + + // Get new ipc server endpoint. + using var ipcServerStreamTask = _ipcServerRouterFactory.AcceptIpcStreamAsync(cancelRouter.Token); + + await Task.WhenAny(ipcServerStreamTask, webSocketServerStreamTask).ConfigureAwait(false); + + if (IsCompletedSuccessfully(ipcServerStreamTask) && IsCompletedSuccessfully(webSocketServerStreamTask)) + { + ipcServerStream = ipcServerStreamTask.Result; + websocketServerStream = webSocketServerStreamTask.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(webSocketServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); + + // Cancel out any pending tasks not yet completed. + cancelRouter.Cancel(); + + try + { + await Task.WhenAll(webSocketServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); + } + catch (Exception) + { + // Check if we have an accepted tcp stream. + if (IsCompletedSuccessfully(webSocketServerStreamTask)) + webSocketServerStreamTask.Result?.Dispose(); + + if (checkIpcStreamTask.IsFaulted) + { + _logger?.LogInformation("Broken ipc connection detected, aborting web socket connection."); + checkIpcStreamTask.GetAwaiter().GetResult(); + } + + throw; + } + + websocketServerStream = webSocketServerStreamTask.Result; + } + else if (IsCompletedSuccessfully(webSocketServerStreamTask)) + { + websocketServerStream = webSocketServerStreamTask.Result; + + // We have a valid tcp stream and a pending ipc accept. Wait for completion + // or disconnect of tcp stream. + using var checkWebSocketStreamTask = IsStreamConnectedAsync(websocketServerStream, cancelRouter.Token); + + // Wait for at least completion of one task. + await Task.WhenAny(ipcServerStreamTask, checkWebSocketStreamTask).ConfigureAwait(false); + + // Cancel out any pending tasks not yet completed. + cancelRouter.Cancel(); + + try + { + await Task.WhenAll(ipcServerStreamTask, checkWebSocketStreamTask).ConfigureAwait(false); + } + catch (Exception) + { + // Check if we have an accepted ipc stream. + if (IsCompletedSuccessfully(ipcServerStreamTask)) + ipcServerStreamTask.Result?.Dispose(); + + if (checkWebSocketStreamTask.IsFaulted) + { + _logger?.LogInformation("Broken webSocekt connection detected, aborting ipc connection."); + checkWebSocketStreamTask.GetAwaiter().GetResult(); + } + + throw; + } + + ipcServerStream = ipcServerStreamTask.Result; + } + else + { + // Error case, cancel out. wait and throw exception. + cancelRouter.Cancel(); + try + { + await Task.WhenAll(ipcServerStreamTask, webSocketServerStreamTask).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(); + websocketServerStream?.Dispose(); + + throw; + } + + // Create new router. + _logger?.LogDebug("New router instance successfully created."); + + return new Router(ipcServerStream, websocketServerStream, _logger); + } + } + + internal class Router : IDisposable { readonly ILogger _logger; diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs index d7376c480a..80bd9125a7 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs @@ -41,6 +41,11 @@ public static async Task runIpcClientTcpClientRouter(CancellationToken toke return await runRouter(token, new IpcClientTcpClientRouterFactory(ipcClient, tcpClient, runtimeTimeoutMs, tcpClientRouterFactory, logger), callbacks).ConfigureAwait(false); } + public static async Task runIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocketURL, int runtimeTimeoutMs, WebSocketServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory, ILogger logger, Callbacks callbacks) + { + return await runRouter(token, new IpcServerWebSocketServerRouterFactory(ipcServer, webSocketURL, runtimeTimeoutMs, webSocketServerRouterFactory, logger), callbacks).ConfigureAwait(false); + } + public static bool isLoopbackOnly(string address) { bool isLooback = 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..ee5f37b2ee 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj +++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj @@ -40,4 +40,5 @@ + diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index f4a9d4fae1..543dae61c0 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -32,6 +32,14 @@ internal sealed class ReversedDiagnosticsServer : IAsyncDisposable 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 +65,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 +143,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, _enableTcpIpProtocol, TransportCallback); _acceptTransportTask = AcceptTransportAsync(_transport, _disposalSource.Token); @@ -216,6 +225,7 @@ private async Task AcceptTransportAsync(IpcServerTransport transport, Cancellati IpcAdvertise advertise = null; try { + stream = await transport.AcceptAsync(token).ConfigureAwait(false); } catch (OperationCanceledException) 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..dff6ec41ed --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs @@ -0,0 +1,19 @@ +// 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.WebSocketServer; + +public interface IWebSocketServer +{ + public Task StartServer(Uri uri, CancellationToken cancellationToken); + + public Task StopServer(CancellationToken cancellationToken); + + public Task AcceptConnection(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 1760e7044f..53fb30998c 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter { @@ -64,8 +65,11 @@ public async Task RunIpcClientTcpServerRouter(CancellationToken token, stri else if (string.Compare(verbose, "trace", StringComparison.OrdinalIgnoreCase) == 0) logLevel = LogLevel.Trace; - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + using var factory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(logLevel); + builder.AddConsole(); + }); Launcher.SuspendProcess = true; Launcher.ConnectMode = true; @@ -122,8 +126,14 @@ public async Task RunIpcServerTcpServerRouter(CancellationToken token, stri else if (string.Compare(verbose, "trace", StringComparison.OrdinalIgnoreCase) == 0) logLevel = LogLevel.Trace; - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + using var factory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(logLevel); + builder.AddSimpleConsole(configure => + { + configure.IncludeScopes = true; + }); + }); Launcher.SuspendProcess = false; Launcher.ConnectMode = true; @@ -181,8 +191,15 @@ public async Task RunIpcServerTcpClientRouter(CancellationToken token, stri else if (string.Compare(verbose, "trace", StringComparison.OrdinalIgnoreCase) == 0) logLevel = LogLevel.Trace; - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + using var factory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(logLevel); + builder.AddSimpleConsole(configure => + { + configure.IncludeScopes = true; + }); + }); + Launcher.SuspendProcess = false; Launcher.ConnectMode = false; @@ -244,8 +261,14 @@ public async Task RunIpcClientTcpClientRouter(CancellationToken token, stri else if (string.Compare(verbose, "trace", StringComparison.OrdinalIgnoreCase) == 0) logLevel = LogLevel.Trace; - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + using var factory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(logLevel); + builder.AddSimpleConsole(configure => + { + configure.IncludeScopes = true; + }); + }); Launcher.SuspendProcess = true; Launcher.ConnectMode = false; @@ -293,9 +316,14 @@ public async Task RunIpcClientTcpClientRouter(CancellationToken token, stri return routerTask.Result; } - public async Task RunIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocketURL, int runtimeTimeout, string verbose, string forwardPort) + public async Task RunIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocket, int runtimeTimeout, string verbose) { - checkLoopbackOnly(webSocketURL); + // checkLoopbackOnly(webSocket); + + const string magicEnv = "DIAGNOSTICS_SERVER_WEBSOCKET_SERVER_TYPE"; + string serverType = typeof(Microsoft.Diagnostics.NETCore.Client.WebSocketServer.WebSocketServerImpl).AssemblyQualifiedName; + Console.WriteLine("Setting env var to {0}", serverType); + Environment.SetEnvironmentVariable(magicEnv, serverType); using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); @@ -306,8 +334,14 @@ public async Task RunIpcServerWebSocketServerRouter(CancellationToken token else if (string.Compare(verbose, "trace", StringComparison.OrdinalIgnoreCase) == 0) logLevel = LogLevel.Trace; - using var factory = new LoggerFactory(); - factory.AddConsole(logLevel, false); + using var factory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(logLevel); + builder.AddSimpleConsole(configure => + { + configure.IncludeScopes = true; + }); + }); Launcher.SuspendProcess = false; Launcher.ConnectMode = true; @@ -316,11 +350,33 @@ public async Task RunIpcServerWebSocketServerRouter(CancellationToken token var logger = factory.CreateLogger("dotnet-dsrouter"); - Console.WriteLine("blah webSocketURL is '{0}'", webSocketURL == null ? "null" : webSocketURL); - logger.LogInformation("started with options: '{ipcServer}' '{webSocketURL}' '{runtimeTimeout}' '{verbose}' '{forwardPort}'", ipcServer, webSocketURL, runtimeTimeout, verbose, forwardPort); + logger.LogInformation("started with options: '{ipcServer}' '{webSocket}' '{runtimeTimeout}' '{verbose}'", ipcServer, webSocket, runtimeTimeout, verbose); + + WebSocketServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; + + if (string.IsNullOrEmpty(ipcServer)) + ipcServer = GetDefaultIpcServerPath(logger); + + var routerTask = DiagnosticsServerRouterRunner.runIpcServerWebSocketServerRouter(linkedCancelToken.Token, ipcServer, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, Launcher); + + while (!linkedCancelToken.IsCancellationRequested) + { + await Task.WhenAny(routerTask, Task.Delay(250)).ConfigureAwait(false); + if (routerTask.IsCompleted) + break; - await Task.Delay(0); - return 0; + if (!Console.IsInputRedirected && Console.KeyAvailable) + { + ConsoleKey cmd = Console.ReadKey(true).Key; + if (cmd == ConsoleKey.Q) + { + cancelRouterTask.Cancel(); + break; + } + } + } + + return routerTask.Result; } static string GetDefaultIpcServerPath(ILogger logger) diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index 04588ff553..cb0384f8ec 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -22,7 +22,7 @@ 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, string forwardPort); + delegate Task DiagnosticsServerIpcServerWebSocketServerRouterDelegate(CancellationToken ct, string ipcServer, string webSocket, int runtimeTimeoutS, string verbose); private static Command IpcClientTcpServerRouterCommand() => new Command( @@ -72,7 +72,7 @@ private static Command IpcServerWebSocketServerRouterCommand() => { HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerWebSocketServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerWebSocketServerRouter).GetCommandHandler(), // Options - IpcServerAddressOption(), WebSocketURLAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() + IpcServerAddressOption(), WebSocketURLAddressOption(), RuntimeTimeoutOption(), VerboseOption() }; private static Command IpcClientTcpClientRouterCommand() => @@ -132,10 +132,10 @@ private static Option TcpServerAddressOption() => private static Option WebSocketURLAddressOption() => new Option( aliases: new[] { "--web-socket", "-ws" }, - description: "The router WebSocket address using format ws://[host]:[port] or wss://[host]:[port]. " + + 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: "webSocket", getDefaultValue: () => "") + Argument = new Argument(name: "webSocketURI", getDefaultValue: () => "") }; private static Option RuntimeTimeoutOption() => diff --git a/src/Tools/dotnet-dsrouter/WebSocketServer.cs b/src/Tools/dotnet-dsrouter/WebSocketServer.cs new file mode 100644 index 0000000000..954a946cb7 --- /dev/null +++ b/src/Tools/dotnet-dsrouter/WebSocketServer.cs @@ -0,0 +1,293 @@ +// 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 CancellationToken = System.Threading.CancellationToken; +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 LogLevel = Microsoft.Extensions.Logging.LogLevel; +using System.Net.WebSockets; +using HttpContext = Microsoft.AspNetCore.Http.HttpContext; +using System.Linq; + +namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; + + +public class WebSocketServerImpl : IWebSocketServer +{ + private WebSocketServer _server = null; + private readonly Queue _acceptQueue = new Queue(); + + public WebSocketServerImpl() { } + + public async Task StartServer(Uri uri, CancellationToken cancellationToken) + { + WebSocketServer.Options options = new() + { + Scheme = uri.Scheme, + Host = uri.Host, + Port = uri.Port.ToString(), + Path = uri.PathAndQuery, + }; + _server = WebSocketServer.CreateWebServer(options, HandleWebSocket); + + await _server.StartWebServer(cancellationToken); + } + + + public async Task StopServer(CancellationToken cancellationToken) + { + Console.WriteLine("stopping server XYZ"); + await _server.StopWebServer(cancellationToken); + _server = null; + } + + public async Task HandleWebSocket(HttpContext context, WebSocket webSocket, CancellationToken cancellationToken) + { + Console.WriteLine("got a connection on the websocket"); + 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) + { + return _acceptQueue.Dequeue(cancellationToken); + } + + public async Task AcceptConnection(CancellationToken cancellationToken) + { + Conn conn = await GetOrRequestConnection(cancellationToken); + return conn.GetStream(); + } + + // single-element queue + 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(); + } + } + } + + } + + 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(); + } + } +} + +public interface IWebSocketConnectionHandler +{ + Task Handle(HttpContext context, WebSocket webSocket, CancellationToken cancellationToken); +} + +public class WebSocketServer +{ + 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 void Assign(Options other) + { + Scheme = other.Scheme; + Host = other.Host; + Port = other.Port; + Path = other.Path; + } + } + + private readonly IHost _host; + private WebSocketServer(IHost host) + { + _host = host; + } + + private static string[] MakeUrls(string scheme, string host, string port) => new string[] { $"{scheme}://{host}:{port}" }; + public static WebSocketServer CreateWebServer(Options options, Func connectionHandler) + { + var builder = new HostBuilder() + .ConfigureLogging(logging => + { + /* FIXME: delegate to outer host's logging */ + logging.AddConsole().AddFilter(null, LogLevel.Debug); + }) + .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)); + }); + + var host = builder.Build(); + + return new WebSocketServer(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/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs b/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs new file mode 100644 index 0000000000..d86a5c718a --- /dev/null +++ b/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs @@ -0,0 +1,93 @@ +// 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; + +namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; + +internal class WebSocketStreamAdapter : Stream +{ + 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(); + } + } + +} diff --git a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj index 187781edb7..7d5c080720 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 @@ -23,8 +23,12 @@ - - + + + + + + From a8f90c73c142aecee6b8f257bd771b36eb5052b9 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 13 Jul 2022 16:55:22 -0400 Subject: [PATCH 03/30] debug WriteLines --- src/Tools/dotnet-dsrouter/WebSocketServer.cs | 2 ++ src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/Tools/dotnet-dsrouter/WebSocketServer.cs b/src/Tools/dotnet-dsrouter/WebSocketServer.cs index 954a946cb7..6ee456bb6c 100644 --- a/src/Tools/dotnet-dsrouter/WebSocketServer.cs +++ b/src/Tools/dotnet-dsrouter/WebSocketServer.cs @@ -74,7 +74,9 @@ internal Task GetOrRequestConnection(CancellationToken cancellationToken) public async Task AcceptConnection(CancellationToken cancellationToken) { + Console.WriteLine("WebSocketServer waiting to AcceptConnection"); Conn conn = await GetOrRequestConnection(cancellationToken); + Console.WriteLine("returning a WebSocketStreamAdapter"); return conn.GetStream(); } diff --git a/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs b/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs index d86a5c718a..1d384fbf58 100644 --- a/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs +++ b/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs @@ -61,23 +61,29 @@ public override void Write(byte[] buffer, int offset, int count) public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + Console.WriteLine("WebSocket stream adapter ReadAsync"); var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer, offset, count), cancellationToken); + Console.WriteLine("WebSocket stream adapter read {0} bytes", result.Count); return result.Count; } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) { + Console.WriteLine("WebSocket stream adapter ReadAsync"); var result = await _webSocket.ReceiveAsync(buffer, cancellationToken); + Console.WriteLine("WebSocket stream adapter read {0} bytes", result.Count); return result.Count; } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + Console.WriteLine("WebSocket stream adapter WriteAsync {0} bytes", count); return _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory memory, CancellationToken cancellationToken) { + Console.WriteLine("WebSocket stream adapter WriteAsync {0} bytes", memory.Length); return _webSocket.SendAsync(memory, WebSocketMessageType.Binary, true, cancellationToken); } @@ -85,6 +91,7 @@ protected override void Dispose(bool disposing) { if (disposing) { + Console.WriteLine("WebSocket stream adapter Dispose(true)"); _onDispose(); _webSocket.Dispose(); } From f692c3736f17ea483b9d1f3fcead0a647903bbe8 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 14 Jul 2022 16:42:37 -0400 Subject: [PATCH 04/30] More WebSocketStreamAdapter work add an interface for it in the Client library, and expose a way to check that it's conncted. --- .../DiagnosticsServerRouterFactory.cs | 13 +++++++------ .../ReversedServer/ReversedDiagnosticsServer.cs | 3 +++ .../WebSocketServer/IWebSocketStreamAdapter.cs | 9 +++++++++ src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs | 4 +++- 4 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index 78c5642864..2d945a3246 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -360,7 +360,7 @@ public void Reset() public async Task AcceptWebSocketStreamAsync(CancellationToken token) { - Stream tcpServerStream; + Stream webSocketServerStream; _logger?.LogDebug($"Waiting for a new WebSocket connection at endpoint \"{WebSocketURL}\"."); @@ -397,23 +397,23 @@ public async Task AcceptWebSocketStreamAsync(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 _webSocketEndpointInfo.Endpoint.ConnectAsync(connectTokenSource.Token).ConfigureAwait(false); + webSocketServerStream = await _webSocketEndpointInfo.Endpoint.ConnectAsync(connectTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { if (connectTimeoutTokenSource.IsCancellationRequested) { - _logger?.LogDebug("No tcp stream connected before timeout."); + _logger?.LogDebug("No WebSocket stream connected before timeout."); throw new BackendStreamTimeoutException(TcpServerTimeoutMs); } throw; } - if (tcpServerStream != null) - _logger?.LogDebug($"Successfully connected tcp stream, runtime id={RuntimeInstanceId}, runtime pid={RuntimeProcessId}."); + if (webSocketServerStream != null) + _logger?.LogDebug($"Successfully connected WebSocket stream, runtime id={RuntimeInstanceId}, runtime pid={RuntimeProcessId}."); - return tcpServerStream; + return webSocketServerStream; } public void CreatedNewServer(EndPoint localEP) @@ -1565,6 +1565,7 @@ public override async Task CreateRouterAsync(CancellationToken token) // We have a valid tcp stream and a pending ipc accept. Wait for completion // or disconnect of tcp stream. + _logger?.LogInformation("webSocketServerStreamTask completed successfully."); using var checkWebSocketStreamTask = IsStreamConnectedAsync(websocketServerStream, cancelRouter.Token); // Wait for at least completion of one task. diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index 543dae61c0..33fc759dae 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -381,6 +381,9 @@ private static bool TestStream(Stream stream) IntPtr.Zero, IntPtr.Zero); } + else if (stream is WebSocketServer.IWebSocketStreamAdapter adapter) + { + bool connected = adapter.IsConnected; return false; } 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..14e904ed35 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs @@ -0,0 +1,9 @@ +// 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; + +public interface IWebSocketStreamAdapter +{ + public bool IsConnected { get; } +} \ No newline at end of file diff --git a/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs b/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs index 1d384fbf58..ab436af310 100644 --- a/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs +++ b/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs @@ -8,7 +8,7 @@ namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; -internal class WebSocketStreamAdapter : Stream +internal class WebSocketStreamAdapter : Stream, IWebSocketStreamAdapter { private readonly WebSocket _webSocket; private readonly Action _onDispose; @@ -97,4 +97,6 @@ protected override void Dispose(bool disposing) } } + bool IWebSocketStreamAdapter.IsConnected => _webSocket.State == WebSocketState.Open; + } From b5aabb806557ec0917189c05dea9e5130b8712be Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 15 Jul 2022 16:52:10 -0400 Subject: [PATCH 05/30] fix comment --- .../DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index 2d945a3246..444764ccf2 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -1612,9 +1612,9 @@ public override async Task CreateRouterAsync(CancellationToken token) } } } - catch (Exception) + catch (Exception e) { - _logger?.LogDebug("Failed creating new router instance."); + _logger?.LogDebug("Failed creating new router instance. {exn}", e); // Cleanup and rethrow. ipcServerStream?.Dispose(); From 7689a22b8f2448782a7de45f3a47d5675f7b6a7d Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 18 Jul 2022 10:55:08 -0400 Subject: [PATCH 06/30] One more IWebSocketStreamAdapter dynamic cast --- .../DiagnosticsServerRouterFactory.cs | 9 ++++++++- .../ReversedServer/ReversedDiagnosticsServer.cs | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index 444764ccf2..e8599ab7cd 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -107,8 +107,15 @@ protected bool IsStreamConnected(Stream stream, CancellationToken token) networkStream.Socket.Blocking = blockingState; } } + else if (stream is WebSocketServer.IWebSocketStreamAdapter adapter) + { + Console.WriteLine("Testing WebSocket stream."); + connected = adapter.IsConnected; + Console.WriteLine("WebSocket stream is {0}connected.", connected ? string.Empty : "not "); + } else { + Console.WriteLine("Unkonwn stream type {0}, assuming disconnected", stream.GetType()); connected = false; } @@ -394,7 +401,7 @@ public async Task AcceptWebSocketStreamAsync(CancellationToken token) try { - // Get next connected tcp stream. Should timeout if no endpoint appears within timeout. + // Get next connected WebSocket 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); webSocketServerStream = await _webSocketEndpointInfo.Endpoint.ConnectAsync(connectTokenSource.Token).ConfigureAwait(false); diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index 33fc759dae..b378f0ba58 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -383,7 +383,11 @@ private static bool TestStream(Stream stream) } else if (stream is WebSocketServer.IWebSocketStreamAdapter adapter) { + Console.WriteLine("Testing WebSocket stream."); bool connected = adapter.IsConnected; + Console.WriteLine("WebSocket stream is {0}connected.", connected ? string.Empty : "not "); + } + Console.WriteLine("unkonwn stream type {0}", stream.GetType().Name); return false; } From 19e62c46c13d73e39dd66253ce923b92024a61e5 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 18 Jul 2022 15:49:31 -0400 Subject: [PATCH 07/30] remove debug WriteLines --- .../DiagnosticsIpc/IpcWebSocketServerTransport.cs | 3 --- .../DiagnosticsServerRouterFactory.cs | 3 --- .../ReversedServer/ReversedDiagnosticsServer.cs | 5 +---- src/Tools/dotnet-dsrouter/WebSocketServer.cs | 3 --- src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs | 6 ------ 5 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs index e292d34567..97205e8dc9 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs @@ -40,9 +40,7 @@ public override async Task AcceptAsync(CancellationToken token) if (singleton.AddRef()) { await singleton.StartServer(_endPoint, token); - Console.WriteLine("starting server AAA"); } - Console.WriteLine("AcceptAsync"); Stream s = await singleton.AcceptConnection(token); return s; } @@ -71,7 +69,6 @@ public void DropRef() } if (Interlocked.Decrement(ref _refCount) == 0) { - Console.WriteLine("stopping server AAA"); StopServer().Wait(); } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index e8599ab7cd..91f76d3fdc 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -109,13 +109,10 @@ protected bool IsStreamConnected(Stream stream, CancellationToken token) } else if (stream is WebSocketServer.IWebSocketStreamAdapter adapter) { - Console.WriteLine("Testing WebSocket stream."); connected = adapter.IsConnected; - Console.WriteLine("WebSocket stream is {0}connected.", connected ? string.Empty : "not "); } else { - Console.WriteLine("Unkonwn stream type {0}, assuming disconnected", stream.GetType()); connected = false; } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index b378f0ba58..a4b397c0ab 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -383,11 +383,8 @@ private static bool TestStream(Stream stream) } else if (stream is WebSocketServer.IWebSocketStreamAdapter adapter) { - Console.WriteLine("Testing WebSocket stream."); - bool connected = adapter.IsConnected; - Console.WriteLine("WebSocket stream is {0}connected.", connected ? string.Empty : "not "); + return adapter.IsConnected; } - Console.WriteLine("unkonwn stream type {0}", stream.GetType().Name); return false; } diff --git a/src/Tools/dotnet-dsrouter/WebSocketServer.cs b/src/Tools/dotnet-dsrouter/WebSocketServer.cs index 6ee456bb6c..d989bcaeef 100644 --- a/src/Tools/dotnet-dsrouter/WebSocketServer.cs +++ b/src/Tools/dotnet-dsrouter/WebSocketServer.cs @@ -47,7 +47,6 @@ public async Task StartServer(Uri uri, CancellationToken cancellationToken) public async Task StopServer(CancellationToken cancellationToken) { - Console.WriteLine("stopping server XYZ"); await _server.StopWebServer(cancellationToken); _server = null; } @@ -74,9 +73,7 @@ internal Task GetOrRequestConnection(CancellationToken cancellationToken) public async Task AcceptConnection(CancellationToken cancellationToken) { - Console.WriteLine("WebSocketServer waiting to AcceptConnection"); Conn conn = await GetOrRequestConnection(cancellationToken); - Console.WriteLine("returning a WebSocketStreamAdapter"); return conn.GetStream(); } diff --git a/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs b/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs index ab436af310..31ae5b2dea 100644 --- a/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs +++ b/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs @@ -61,29 +61,23 @@ public override void Write(byte[] buffer, int offset, int count) public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - Console.WriteLine("WebSocket stream adapter ReadAsync"); var result = await _webSocket.ReceiveAsync(new ArraySegment(buffer, offset, count), cancellationToken); - Console.WriteLine("WebSocket stream adapter read {0} bytes", result.Count); return result.Count; } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) { - Console.WriteLine("WebSocket stream adapter ReadAsync"); var result = await _webSocket.ReceiveAsync(buffer, cancellationToken); - Console.WriteLine("WebSocket stream adapter read {0} bytes", result.Count); return result.Count; } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - Console.WriteLine("WebSocket stream adapter WriteAsync {0} bytes", count); return _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Binary, true, cancellationToken); } public override ValueTask WriteAsync(ReadOnlyMemory memory, CancellationToken cancellationToken) { - Console.WriteLine("WebSocket stream adapter WriteAsync {0} bytes", memory.Length); return _webSocket.SendAsync(memory, WebSocketMessageType.Binary, true, cancellationToken); } From c4971df10c6cbd5b4ed49227d9dd6b82083f5dac Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 13 Oct 2022 13:38:58 -0400 Subject: [PATCH 08/30] update after rebase --- .../ReversedServer/ReversedDiagnosticsServer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index a4b397c0ab..b60fed0ad1 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -30,7 +30,6 @@ internal sealed class ReversedDiagnosticsServer : IAsyncDisposable private bool _disposed = false; private Task _acceptTransportTask; - private bool _enableTcpIpProtocol = false; private IpcServerTransport _transport; private Kind _kind = Kind.Ipc; @@ -143,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, _kind, _enableTcpIpProtocol, TransportCallback); + _transport = IpcServerTransport.Create(_address, maxConnections, _kind, TransportCallback); _acceptTransportTask = AcceptTransportAsync(_transport, _disposalSource.Token); From 6f9897bfd7cdd051b16a9c24da95b7e58f0bda4e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 13 Oct 2022 15:16:18 -0400 Subject: [PATCH 09/30] Move WebSocketServer bits to a separate assembly outside dotnet-dsrouter --- ...crosoft.Diagnostics.WebSocketServer.csproj | 29 +++++++++++++++++++ .../WebSocketServer.cs | 0 .../WebSocketStreamAdapter.cs | 0 .../dotnet-dsrouter/dotnet-dsrouter.csproj | 5 +--- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj rename src/{Tools/dotnet-dsrouter => Microsoft.Diagnostics.WebSocketServer}/WebSocketServer.cs (100%) rename src/{Tools/dotnet-dsrouter => Microsoft.Diagnostics.WebSocketServer}/WebSocketStreamAdapter.cs (100%) 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..ec7f3988c8 --- /dev/null +++ b/src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj @@ -0,0 +1,29 @@ + + + + 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/Tools/dotnet-dsrouter/WebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs similarity index 100% rename from src/Tools/dotnet-dsrouter/WebSocketServer.cs rename to src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs diff --git a/src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs similarity index 100% rename from src/Tools/dotnet-dsrouter/WebSocketStreamAdapter.cs rename to src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs diff --git a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj index 7d5c080720..f895b4a2c4 100644 --- a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj +++ b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj @@ -18,6 +18,7 @@ + @@ -27,8 +28,4 @@ - - - - From 873adf06895edc33434b1200bcca50f87f051640 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 13 Oct 2022 15:54:41 -0400 Subject: [PATCH 10/30] fix up names --- .../Microsoft.Diagnostics.WebSocketServer.csproj | 5 +---- src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs | 3 ++- .../WebSocketStreamAdapter.cs | 3 ++- src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj b/src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj index ec7f3988c8..873bb470ba 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj +++ b/src/Microsoft.Diagnostics.WebSocketServer/Microsoft.Diagnostics.WebSocketServer.csproj @@ -10,14 +10,11 @@ $(Description) false true - + false true - - - diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs index d989bcaeef..5f9543d709 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Routing; +using Microsoft.Diagnostics.NETCore.Client.WebSocketServer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -20,7 +21,7 @@ using HttpContext = Microsoft.AspNetCore.Http.HttpContext; using System.Linq; -namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; +namespace Microsoft.Diagnostics.WebSocketServer; public class WebSocketServerImpl : IWebSocketServer diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs index 31ae5b2dea..5753654aca 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs @@ -5,8 +5,9 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using Microsoft.Diagnostics.NETCore.Client.WebSocketServer; -namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; +namespace Microsoft.Diagnostics.WebSocketServer; internal class WebSocketStreamAdapter : Stream, IWebSocketStreamAdapter { diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 53fb30998c..05af06a5a2 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -321,7 +321,7 @@ public async Task RunIpcServerWebSocketServerRouter(CancellationToken token // checkLoopbackOnly(webSocket); const string magicEnv = "DIAGNOSTICS_SERVER_WEBSOCKET_SERVER_TYPE"; - string serverType = typeof(Microsoft.Diagnostics.NETCore.Client.WebSocketServer.WebSocketServerImpl).AssemblyQualifiedName; + string serverType = typeof(Microsoft.Diagnostics.WebSocketServer.WebSocketServerImpl).AssemblyQualifiedName; Console.WriteLine("Setting env var to {0}", serverType); Environment.SetEnvironmentVariable(magicEnv, serverType); From 50494acea00447e68013abdcc89b9d550832f085 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 14 Oct 2022 14:37:07 -0400 Subject: [PATCH 11/30] use a factory for creating WebSocketServer instances, instead of env var --- .../IpcWebSocketServerTransport.cs | 11 ++--------- .../WebSocketServer/IWebSocketServer.cs | 1 + .../WebSocketServer/WebSocketServerFactory.cs | 19 +++++++++++++++++++ .../WebSocketServer.cs | 3 +++ .../DiagnosticsServerRouterCommands.cs | 12 +++++------- 5 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs index 97205e8dc9..df7d2ed8b1 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs @@ -80,15 +80,8 @@ internal async Task StartServer(IpcWebSocketEndPoint endPoint, CancellationToken return; } ServerRunning = true; - string typeName = Environment.GetEnvironmentVariable("DIAGNOSTICS_SERVER_WEBSOCKET_SERVER_TYPE"); - Console.WriteLine("typeName: {0}", typeName); - Type t = Type.GetType(typeName); - if (t == null) - { - Console.WriteLine("no type found {0}", typeName); - throw new Exception("Unable to find type " + typeName); - } - server = (WebSocketServer.IWebSocketServer)Activator.CreateInstance(t); + WebSocketServer.IWebSocketServer newServer = WebSocketServer.WebSocketServerFactory.CreateWebSocketServer(); + server = newServer; ; await server.StartServer(endPoint.EndPoint, token); await Task.Delay(1000); } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs index dff6ec41ed..2b07a31f0f 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs @@ -5,6 +5,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs new file mode 100644 index 0000000000..95f9c88da0 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; + +internal class WebSocketServerFactory +{ + internal static void SetBuilder(Func builder) + { + _builder = builder; + } + + internal static IWebSocketServer CreateWebSocketServer() + { + return _builder(); + } + + private static Func _builder; +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs index 5f9543d709..6ea35689a6 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs @@ -33,6 +33,7 @@ public WebSocketServerImpl() { } public async Task StartServer(Uri uri, CancellationToken cancellationToken) { + Console.WriteLine("Starting web socket server on {0}", uri); WebSocketServer.Options options = new() { Scheme = uri.Scheme, @@ -43,6 +44,8 @@ public async Task StartServer(Uri uri, CancellationToken cancellationToken) _server = WebSocketServer.CreateWebServer(options, HandleWebSocket); await _server.StartWebServer(cancellationToken); + Console.WriteLine("Started web socket server on {0}", uri); + } diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 05af06a5a2..ea8511ee6d 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -318,15 +318,13 @@ public async Task RunIpcClientTcpClientRouter(CancellationToken token, stri public async Task RunIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocket, int runtimeTimeout, string verbose) { - // checkLoopbackOnly(webSocket); - - const string magicEnv = "DIAGNOSTICS_SERVER_WEBSOCKET_SERVER_TYPE"; - string serverType = typeof(Microsoft.Diagnostics.WebSocketServer.WebSocketServerImpl).AssemblyQualifiedName; - Console.WriteLine("Setting env var to {0}", serverType); - Environment.SetEnvironmentVariable(magicEnv, serverType); - using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); + NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => + { + Console.WriteLine("building a new web socket server"); + return new WebSocketServer.WebSocketServerImpl(); + }); LogLevel logLevel = LogLevel.Information; if (string.Compare(verbose, "debug", StringComparison.OrdinalIgnoreCase) == 0) From ac3d70700a6b8a72dbbc8bcc5031213baa3822a6 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 17 Oct 2022 15:31:28 -0400 Subject: [PATCH 12/30] base class for TcpServerRouterFactory and WebSocketServerRouterFactory Less copy/paste, more sharing --- .../DiagnosticsServerRouterFactory.cs | 268 ++++++++---------- .../dotnet-dsrouter/ADBTcpRouterFactory.cs | 4 +- 2 files changed, 116 insertions(+), 156 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index 91f76d3fdc..f3ada0eb49 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -149,85 +149,66 @@ 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; + private readonly ILogger _logger; + private IpcEndpointInfo _netServerEndpointInfo; + public abstract void CreatedNewServer(EndPoint localEP); - string _tcpServerAddress; - ReversedDiagnosticsServer _tcpServer; - IpcEndpointInfo _tcpServerEndpointInfo; + protected ILogger Logger => _logger; - bool _auto_shutdown; + protected int RuntimeTimeoutMs { get; private set; } = 60000; + protected int NetServerTimeoutMs { get; set; } = 5000; - int RuntimeTimeoutMs { get; set; } = 60000; - int TcpServerTimeoutMs { get; set; } = 5000; + private bool _auto_shutdown; - public Guid RuntimeInstanceId - { - get { return _tcpServerEndpointInfo.RuntimeInstanceCookie; } - } + protected bool IsAutoShutdown => _auto_shutdown; - public int RuntimeProcessId + protected IpcEndpointInfo NetServerEndpointInfo { - get { return _tcpServerEndpointInfo.ProcessId; } + get => _netServerEndpointInfo; + private set { _netServerEndpointInfo = value; } } - public string TcpServerAddress - { - get { return _tcpServerAddress; } - } - public delegate TcpServerRouterFactory CreateInstanceDelegate(string tcpServer, int runtimeTimeoutMs, ILogger logger); + protected IpcEndpoint Endpoint => NetServerEndpointInfo.Endpoint; + public Guid RuntimeInstanceId => NetServerEndpointInfo.RuntimeInstanceCookie; + public int RuntimeProcessId => NetServerEndpointInfo.ProcessId; - public static TcpServerRouterFactory CreateDefaultInstance(string tcpServer, int runtimeTimeoutMs, ILogger logger) + 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, ReversedDiagnosticsServer.Kind.Tcp); - _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); - } + protected abstract string ServerAddress { get; } + protected 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 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); @@ -236,15 +217,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); } @@ -259,60 +240,101 @@ 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; } - public 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. + /// This class represent a TCP/IP server endpoint used when building up router instances. /// - internal class WebSocketServerRouterFactory : IIpcServerTransportCallbackInternal + internal class TcpServerRouterFactory : NetServerRouterFactory { - protected readonly ILogger _logger; - IpcWebSocketEndPoint _webSocketEndPoint; + string _tcpServerAddress; - ReversedDiagnosticsServer _webSocketServer; - IpcEndpointInfo _webSocketEndpointInfo; + ReversedDiagnosticsServer _tcpServer; + + public string TcpServerAddress + { + get { return _tcpServerAddress; } + } + + public delegate TcpServerRouterFactory CreateInstanceDelegate(string tcpServer, int runtimeTimeoutMs, ILogger logger); + + public static TcpServerRouterFactory CreateDefaultInstance(string tcpServer, int runtimeTimeoutMs, ILogger logger) + { + return new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger); + } - bool _auto_shutdown; + public TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger logger) : base(runtimeTimeoutMs, logger) + { - int RuntimeTimeoutMs { get; set; } = 60000; - int TcpServerTimeoutMs { get; set; } = 5000; + _tcpServerAddress = IpcTcpSocketEndPoint.NormalizeTcpIpEndPoint(string.IsNullOrEmpty(tcpServer) ? "127.0.0.1:0" : tcpServer); - public Guid RuntimeInstanceId + _tcpServer = new ReversedDiagnosticsServer(_tcpServerAddress, ReversedDiagnosticsServer.Kind.Tcp); + _tcpServer.TransportCallback = this; + } + + public virtual void Start() { - get { return _webSocketEndpointInfo.RuntimeInstanceCookie; } + _tcpServer.Start(); } - public int RuntimeProcessId + public virtual async Task Stop() { - get { return _webSocketEndpointInfo.ProcessId; } + await _tcpServer.DisposeAsync().ConfigureAwait(false); } + public void Reset() + { + if (Endpoint != null) + { + _tcpServer.RemoveConnection(NetServerEndpointInfo.RuntimeInstanceCookie); + ResetEnpointInfo(); + } + } + + protected override Task AcceptAsyncImpl(CancellationToken token) => _tcpServer.AcceptAsync(token); + protected override string ServerAddress => _tcpServerAddress; + protected 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 + { + + IpcWebSocketEndPoint _webSocketEndPoint; + + ReversedDiagnosticsServer _webSocketServer; + public Uri WebSocketURL { get { return _webSocketEndPoint.EndPoint; } @@ -325,106 +347,44 @@ public static WebSocketServerRouterFactory CreateDefaultInstance(string webSocke return new WebSocketServerRouterFactory(webSocketURL, runtimeTimeoutMs, logger); } - public WebSocketServerRouterFactory(string webSocketURL, int runtimeTimeoutMs, ILogger logger) + public WebSocketServerRouterFactory(string webSocketURL, int runtimeTimeoutMs, ILogger logger) : base(runtimeTimeoutMs, logger) { - _logger = logger; _webSocketEndPoint = new IpcWebSocketEndPoint(string.IsNullOrEmpty(webSocketURL) ? "ws://127.0.0.1:8088/diagnostics" : webSocketURL); - _auto_shutdown = runtimeTimeoutMs != Timeout.Infinite; - if (runtimeTimeoutMs != Timeout.Infinite) - RuntimeTimeoutMs = runtimeTimeoutMs; - _webSocketServer = new ReversedDiagnosticsServer(_webSocketEndPoint.EndPoint.ToString(), ReversedDiagnosticsServer.Kind.WebSocket); - _webSocketEndpointInfo = new IpcEndpointInfo(); _webSocketServer.TransportCallback = this; } public virtual void Start() { - _logger.LogInformation("Starting web socket server"); + Logger.LogInformation("Starting web socket server"); _webSocketServer.Start(); } public virtual async Task Stop() { - _logger.LogInformation("Stopping web socket server"); + Logger.LogInformation("Stopping web socket server"); await _webSocketServer.DisposeAsync().ConfigureAwait(false); } public void Reset() { - if (_webSocketEndpointInfo.Endpoint != null) + if (Endpoint != null) { - _logger.LogInformation("Resetting the web socket server"); - _webSocketServer.RemoveConnection(_webSocketEndpointInfo.RuntimeInstanceCookie); - _webSocketEndpointInfo = new IpcEndpointInfo(); + Logger.LogInformation("Resetting the web socket server"); + _webSocketServer.RemoveConnection(NetServerEndpointInfo.RuntimeInstanceCookie); + ResetEnpointInfo(); } } - public async Task AcceptWebSocketStreamAsync(CancellationToken token) - { - Stream webSocketServerStream; - - _logger?.LogDebug($"Waiting for a new WebSocket connection at endpoint \"{WebSocketURL}\"."); - - if (_webSocketEndpointInfo.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); - _webSocketEndpointInfo = await _webSocketServer.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 WebSocket 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); - webSocketServerStream = await _webSocketEndpointInfo.Endpoint.ConnectAsync(connectTokenSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - if (connectTimeoutTokenSource.IsCancellationRequested) - { - _logger?.LogDebug("No WebSocket stream connected before timeout."); - throw new BackendStreamTimeoutException(TcpServerTimeoutMs); - } - - throw; - } - - if (webSocketServerStream != null) - _logger?.LogDebug($"Successfully connected WebSocket stream, runtime id={RuntimeInstanceId}, runtime pid={RuntimeProcessId}."); - - return webSocketServerStream; - } + protected override Task AcceptAsyncImpl(CancellationToken token) => _webSocketServer.AcceptAsync(token); + protected override string ServerAddress => WebSocketURL.ToString(); + protected override string ServerTransportName => "WebSocket"; - public void CreatedNewServer(EndPoint localEP) + public override void CreatedNewServer(EndPoint localEP) { - // FIXME: anything to do here? - //if (localEP is IPEndPoint ipEP) - // _webSocketEndPoint = _webSocketEndPoint.Replace(":0", string.Format(":{0}", ipEP.Port)); + // anything to do here? } } @@ -818,7 +778,7 @@ 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 tcpServerStreamTask = _tcpServerRouterFactory.AcceptNetStreamAsync(cancelRouter.Token); // Get new ipc server endpoint. using var ipcServerStreamTask = _ipcServerRouterFactory.AcceptIpcStreamAsync(cancelRouter.Token); @@ -1135,7 +1095,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); @@ -1516,7 +1476,7 @@ public override async Task CreateRouterAsync(CancellationToken token) using CancellationTokenSource cancelRouter = CancellationTokenSource.CreateLinkedTokenSource(token); // Get new tcp server endpoint. - using var webSocketServerStreamTask = _webSocketServerRouterFactory.AcceptWebSocketStreamAsync(cancelRouter.Token); + using var webSocketServerStreamTask = _webSocketServerRouterFactory.AcceptNetStreamAsync(cancelRouter.Token); // Get new ipc server endpoint. using var ipcServerStreamTask = _ipcServerRouterFactory.AcceptIpcStreamAsync(cancelRouter.Token); 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; } } From d42f90f9c16824becb2f4751aff6b36dabc6155a Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 17 Oct 2022 16:31:52 -0400 Subject: [PATCH 13/30] Remove IpcServerWebSocketServerRouterFactory Use IpcServerTcpServerRouterFactory since everything is sufficiently abstracted --- .../DiagnosticsServerRouterFactory.cs | 291 ++++-------------- .../DiagnosticsServerRouterRunner.cs | 4 +- 2 files changed, 54 insertions(+), 241 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index f3ada0eb49..ff8f8f61b0 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -156,6 +156,8 @@ protected bool IsCompletedSuccessfully(Task t) /// internal abstract class NetServerRouterFactory : IIpcServerTransportCallbackInternal { + public delegate NetServerRouterFactory CreateInstanceDelegate(string webSocketURL, int runtimeTimeoutMs, ILogger logger); + private readonly ILogger _logger; private IpcEndpointInfo _netServerEndpointInfo; public abstract void CreatedNewServer(EndPoint localEP); @@ -197,11 +199,22 @@ protected NetServerRouterFactory(int runtimeTimeoutMs, ILogger logger) } - protected abstract string ServerAddress { get; } - protected abstract string ServerTransportName { get; } + /// + /// 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; } protected abstract Task AcceptAsyncImpl(CancellationToken token); + public abstract void Start(); + public abstract Task Stop(); + public abstract void Reset(); + public async Task AcceptNetStreamAsync(CancellationToken token) { Stream netServerStream; @@ -278,8 +291,6 @@ public string TcpServerAddress get { return _tcpServerAddress; } } - public delegate TcpServerRouterFactory CreateInstanceDelegate(string tcpServer, int runtimeTimeoutMs, ILogger logger); - public static TcpServerRouterFactory CreateDefaultInstance(string tcpServer, int runtimeTimeoutMs, ILogger logger) { return new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger); @@ -294,17 +305,17 @@ public TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger lo _tcpServer.TransportCallback = this; } - public virtual void Start() + public override void Start() { _tcpServer.Start(); } - public virtual async Task Stop() + public override async Task Stop() { await _tcpServer.DisposeAsync().ConfigureAwait(false); } - public void Reset() + public override void Reset() { if (Endpoint != null) { @@ -314,8 +325,8 @@ public void Reset() } protected override Task AcceptAsyncImpl(CancellationToken token) => _tcpServer.AcceptAsync(token); - protected override string ServerAddress => _tcpServerAddress; - protected override string ServerTransportName => "tcp"; + public override string ServerAddress => _tcpServerAddress; + public override string ServerTransportName => "TCP"; public override void CreatedNewServer(EndPoint localEP) @@ -340,8 +351,6 @@ public Uri WebSocketURL get { return _webSocketEndPoint.EndPoint; } } - public delegate WebSocketServerRouterFactory CreateInstanceDelegate(string webSocketURL, int runtimeTimeoutMs, ILogger logger); - public static WebSocketServerRouterFactory CreateDefaultInstance(string webSocketURL, int runtimeTimeoutMs, ILogger logger) { return new WebSocketServerRouterFactory(webSocketURL, runtimeTimeoutMs, logger); @@ -356,19 +365,19 @@ public WebSocketServerRouterFactory(string webSocketURL, int runtimeTimeoutMs, I _webSocketServer.TransportCallback = this; } - public virtual void Start() + public override void Start() { Logger.LogInformation("Starting web socket server"); _webSocketServer.Start(); } - public virtual async Task Stop() + public override async Task Stop() { Logger.LogInformation("Stopping web socket server"); await _webSocketServer.DisposeAsync().ConfigureAwait(false); } - public void Reset() + public override void Reset() { if (Endpoint != null) { @@ -379,8 +388,8 @@ public void Reset() } protected override Task AcceptAsyncImpl(CancellationToken token) => _webSocketServer.AcceptAsync(token); - protected override string ServerAddress => WebSocketURL.ToString(); - protected override string ServerTransportName => "WebSocket"; + public override string ServerAddress => WebSocketURL.ToString(); + public override string ServerTransportName => "WebSocket"; public override void CreatedNewServer(EndPoint localEP) { @@ -710,13 +719,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); } @@ -732,7 +741,7 @@ public override string TcpAddress { get { - return _tcpServerRouterFactory.TcpServerAddress; + return _netServerRouterFactory.ServerAddress; } } @@ -746,24 +755,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) @@ -778,17 +787,17 @@ public override async Task CreateRouterAsync(CancellationToken token) using CancellationTokenSource cancelRouter = CancellationTokenSource.CreateLinkedTokenSource(token); // Get new tcp server endpoint. - using var tcpServerStreamTask = _tcpServerRouterFactory.AcceptNetStreamAsync(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)) { @@ -799,35 +808,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. @@ -851,7 +860,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(); } @@ -866,7 +875,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) { @@ -1027,9 +1036,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); @@ -1048,7 +1057,7 @@ public override string TcpAddress { get { - return _tcpServerRouterFactory.TcpServerAddress; + return _tcpServerRouterFactory.ServerAddress; } } @@ -1067,14 +1076,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(); } @@ -1399,202 +1408,6 @@ private async Task UpdateRuntimeInfo(CancellationToken token) } } - /// - /// This class creates IPC Server - WebSocket Server router instances. - /// Supports NamedPipes/UnixDomainSocket server and WebSocket server. - /// - internal class IpcServerWebSocketServerRouterFactory : DiagnosticsServerRouterFactory - { - ILogger _logger; - WebSocketServerRouterFactory _webSocketServerRouterFactory; - IpcServerRouterFactory _ipcServerRouterFactory; - - public IpcServerWebSocketServerRouterFactory(string ipcServer, string webSocketURL, int runtimeTimeoutMs, WebSocketServerRouterFactory.CreateInstanceDelegate factory, ILogger logger) - { - _logger = logger; - _webSocketServerRouterFactory = factory(webSocketURL, runtimeTimeoutMs, logger); - _ipcServerRouterFactory = new IpcServerRouterFactory(ipcServer, logger); - } - - public override string IpcAddress - { - get - { - return _ipcServerRouterFactory.IpcServerPath; - } - } - - public Uri WebSocketURL - { - get - { - return _webSocketServerRouterFactory.WebSocketURL; - } - } - - public override string TcpAddress => WebSocketURL.ToString(); - - public override ILogger Logger - { - get - { - return _logger; - } - } - - public override Task Start(CancellationToken token) - { - _webSocketServerRouterFactory.Start(); - _ipcServerRouterFactory.Start(); - - _logger?.LogInformation($"Starting IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> WebSocket server ({_webSocketServerRouterFactory.WebSocketURL}) router."); - - return Task.CompletedTask; - } - - public override Task Stop() - { - _logger?.LogInformation($"Stopping IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> WebSocket server ({_webSocketServerRouterFactory.WebSocketURL}) router."); - _ipcServerRouterFactory.Stop(); - return _webSocketServerRouterFactory.Stop(); - } - - public override void Reset() - { - _webSocketServerRouterFactory.Reset(); - } - - public override async Task CreateRouterAsync(CancellationToken token) - { - Stream websocketServerStream = 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 webSocketServerStreamTask = _webSocketServerRouterFactory.AcceptNetStreamAsync(cancelRouter.Token); - - // Get new ipc server endpoint. - using var ipcServerStreamTask = _ipcServerRouterFactory.AcceptIpcStreamAsync(cancelRouter.Token); - - await Task.WhenAny(ipcServerStreamTask, webSocketServerStreamTask).ConfigureAwait(false); - - if (IsCompletedSuccessfully(ipcServerStreamTask) && IsCompletedSuccessfully(webSocketServerStreamTask)) - { - ipcServerStream = ipcServerStreamTask.Result; - websocketServerStream = webSocketServerStreamTask.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(webSocketServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); - - // Cancel out any pending tasks not yet completed. - cancelRouter.Cancel(); - - try - { - await Task.WhenAll(webSocketServerStreamTask, checkIpcStreamTask).ConfigureAwait(false); - } - catch (Exception) - { - // Check if we have an accepted tcp stream. - if (IsCompletedSuccessfully(webSocketServerStreamTask)) - webSocketServerStreamTask.Result?.Dispose(); - - if (checkIpcStreamTask.IsFaulted) - { - _logger?.LogInformation("Broken ipc connection detected, aborting web socket connection."); - checkIpcStreamTask.GetAwaiter().GetResult(); - } - - throw; - } - - websocketServerStream = webSocketServerStreamTask.Result; - } - else if (IsCompletedSuccessfully(webSocketServerStreamTask)) - { - websocketServerStream = webSocketServerStreamTask.Result; - - // We have a valid tcp stream and a pending ipc accept. Wait for completion - // or disconnect of tcp stream. - _logger?.LogInformation("webSocketServerStreamTask completed successfully."); - using var checkWebSocketStreamTask = IsStreamConnectedAsync(websocketServerStream, cancelRouter.Token); - - // Wait for at least completion of one task. - await Task.WhenAny(ipcServerStreamTask, checkWebSocketStreamTask).ConfigureAwait(false); - - // Cancel out any pending tasks not yet completed. - cancelRouter.Cancel(); - - try - { - await Task.WhenAll(ipcServerStreamTask, checkWebSocketStreamTask).ConfigureAwait(false); - } - catch (Exception) - { - // Check if we have an accepted ipc stream. - if (IsCompletedSuccessfully(ipcServerStreamTask)) - ipcServerStreamTask.Result?.Dispose(); - - if (checkWebSocketStreamTask.IsFaulted) - { - _logger?.LogInformation("Broken webSocekt connection detected, aborting ipc connection."); - checkWebSocketStreamTask.GetAwaiter().GetResult(); - } - - throw; - } - - ipcServerStream = ipcServerStreamTask.Result; - } - else - { - // Error case, cancel out. wait and throw exception. - cancelRouter.Cancel(); - try - { - await Task.WhenAll(ipcServerStreamTask, webSocketServerStreamTask).ConfigureAwait(false); - } - catch (Exception) - { - // Check if we have an ipc stream. - if (IsCompletedSuccessfully(ipcServerStreamTask)) - ipcServerStreamTask.Result?.Dispose(); - throw; - } - } - } - catch (Exception e) - { - _logger?.LogDebug("Failed creating new router instance. {exn}", e); - - // Cleanup and rethrow. - ipcServerStream?.Dispose(); - websocketServerStream?.Dispose(); - - throw; - } - - // Create new router. - _logger?.LogDebug("New router instance successfully created."); - - return new Router(ipcServerStream, websocketServerStream, _logger); - } - } - - internal class Router : IDisposable { readonly ILogger _logger; diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs index 80bd9125a7..ee1f359dae 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs @@ -41,9 +41,9 @@ public static async Task runIpcClientTcpClientRouter(CancellationToken toke return await runRouter(token, new IpcClientTcpClientRouterFactory(ipcClient, tcpClient, runtimeTimeoutMs, tcpClientRouterFactory, logger), callbacks).ConfigureAwait(false); } - public static async Task runIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocketURL, int runtimeTimeoutMs, WebSocketServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory, ILogger logger, Callbacks callbacks) + public static async Task runIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocketURL, int runtimeTimeoutMs, NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory, ILogger logger, Callbacks callbacks) { - return await runRouter(token, new IpcServerWebSocketServerRouterFactory(ipcServer, webSocketURL, runtimeTimeoutMs, webSocketServerRouterFactory, logger), callbacks).ConfigureAwait(false); + return await runRouter(token, new IpcServerTcpServerRouterFactory(ipcServer, webSocketURL, runtimeTimeoutMs, webSocketServerRouterFactory, logger), callbacks).ConfigureAwait(false); } public static bool isLoopbackOnly(string address) From b492e6d40b88e76efa1e9924528800bb6a0cbc31 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Mon, 17 Oct 2022 16:45:53 -0400 Subject: [PATCH 14/30] Remove more copy/pasted code --- .../DiagnosticsServerRouterRunner.cs | 7 +------ .../dotnet-dsrouter/DiagnosticsServerRouterCommands.cs | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs index ee1f359dae..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); } @@ -41,11 +41,6 @@ public static async Task runIpcClientTcpClientRouter(CancellationToken toke return await runRouter(token, new IpcClientTcpClientRouterFactory(ipcClient, tcpClient, runtimeTimeoutMs, tcpClientRouterFactory, logger), callbacks).ConfigureAwait(false); } - public static async Task runIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocketURL, int runtimeTimeoutMs, NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory, ILogger logger, Callbacks callbacks) - { - return await runRouter(token, new IpcServerTcpServerRouterFactory(ipcServer, webSocketURL, runtimeTimeoutMs, webSocketServerRouterFactory, logger), callbacks).ConfigureAwait(false); - } - public static bool isLoopbackOnly(string address) { bool isLooback = false; diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index ea8511ee6d..f913fa87e6 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -350,12 +350,12 @@ public async Task RunIpcServerWebSocketServerRouter(CancellationToken token logger.LogInformation("started with options: '{ipcServer}' '{webSocket}' '{runtimeTimeout}' '{verbose}'", ipcServer, webSocket, runtimeTimeout, verbose); - WebSocketServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; + NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; if (string.IsNullOrEmpty(ipcServer)) ipcServer = GetDefaultIpcServerPath(logger); - var routerTask = DiagnosticsServerRouterRunner.runIpcServerWebSocketServerRouter(linkedCancelToken.Token, ipcServer, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, Launcher); + var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, Launcher); while (!linkedCancelToken.IsCancellationRequested) { From a2bffe80a77e40cf9cc618f2a20175e72621564f Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 19 Oct 2022 14:54:29 -0400 Subject: [PATCH 15/30] Suppress generic host startup messages --- src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs index 6ea35689a6..d2aa48409f 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs @@ -221,6 +221,10 @@ public static WebSocketServer CreateWebServer(Options options, Func ConfigureApplication(/*context,*/ app, connectionHandler)); webHostBuilder.UseUrls(MakeUrls(options.Scheme, options.Host, options.Port)); + }) + .UseConsoleLifetime(options => + { + options.SuppressStatusMessages = true; }); var host = builder.Build(); From 56d29e4665ae037a657acf2d01ec1e10c489e703 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 19 Oct 2022 14:55:00 -0400 Subject: [PATCH 16/30] Factor out common run loop for dsrouter commands --- .../DiagnosticsServerRouterCommands.cs | 449 ++++++++---------- 1 file changed, 197 insertions(+), 252 deletions(-) diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index f913fa87e6..420f0954af 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -12,7 +12,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Configuration; namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter { @@ -46,335 +45,242 @@ 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 = LoggerFactory.Create(builder => + protected SpecificRunnerBase(LogLevel logLevel) { - builder.SetMinimumLevel(logLevel); - builder.AddConsole(); - }); + 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; } - public async Task RunIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeout, string verbose, string forwardPort) + class IpcClientTcpServerRunner : SpecificRunnerBase { - 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 = LoggerFactory.Create(builder => - { - builder.SetMinimumLevel(logLevel); - builder.AddSimpleConsole(configure => - { - configure.IncludeScopes = true; - }); - }); - - Launcher.SuspendProcess = false; - Launcher.ConnectMode = true; - Launcher.Verbose = logLevel != LogLevel.Information; - Launcher.CommandToken = token; + public IpcClientTcpServerRunner(string verbose) : base(verbose) { } - var logger = factory.CreateLogger("dotnet-dsrouter"); - - 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 = true; + Launcher.ConnectMode = true; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; } - - if (string.IsNullOrEmpty(ipcServer)) - ipcServer = GetDefaultIpcServerPath(logger); - - var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, Launcher); - - while (!linkedCancelToken.IsCancellationRequested) + public override ILoggerFactory ConfigureLogging() { - 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) + var factory = LoggerFactory.Create(builder => { - cancelRouterTask.Cancel(); - break; - } - } + builder.SetMinimumLevel(LogLevel); + builder.AddConsole(); + }); + return factory; } - - return routerTask.Result; } - public async Task RunIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeout, string verbose, string forwardPort) + public async Task RunIpcClientTcpServerRouter(CancellationToken token, string ipcClient, string tcpServer, int runtimeTimeout, string verbose, string forwardPort) { - using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); - using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); + checkLoopbackOnly(tcpServer); - 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 = LoggerFactory.Create(builder => + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => { - builder.SetMinimumLevel(logLevel); - builder.AddSimpleConsole(configure => - { - configure.IncludeScopes = true; - }); - }); - + NetServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = ChooseTcpServerRouterFactory(forwardPort, logger); - Launcher.SuspendProcess = false; - Launcher.ConnectMode = false; - 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) { } - 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 = 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.runIpcServerTcpClientRouter(linkedCancelToken.Token, ipcServer, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, 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 RunIpcClientTcpClientRouter(CancellationToken token, string ipcClient, 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) { } - 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 override void ConfigureLauncher(CancellationToken cancellationToken) + { + Launcher.SuspendProcess = false; + Launcher.ConnectMode = false; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; + } + } - using var factory = LoggerFactory.Create(builder => + 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) => { - builder.SetMinimumLevel(logLevel); - builder.AddSimpleConsole(configure => - { - configure.IncludeScopes = true; - }); - }); + TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = ChooseTcpClientRouterFactory(forwardPort, logger); - Launcher.SuspendProcess = true; - Launcher.ConnectMode = false; - Launcher.Verbose = logLevel != LogLevel.Information; - Launcher.CommandToken = token; + if (string.IsNullOrEmpty(ipcServer)) + ipcServer = GetDefaultIpcServerPath(logger); - var logger = factory.CreateLogger("dotnet-dsrouter"); + var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpClientRouter(linkedCancelToken.Token, ipcServer, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); + } - TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = TcpClientRouterFactory.CreateDefaultInstance; - if (!string.IsNullOrEmpty(forwardPort)) + class IpcClientTcpClientRunner : SpecificRunnerBase + { + public IpcClientTcpClientRunner(string verbose) : base(verbose) { } + + 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; } + } - var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpClientRouter(linkedCancelToken.Token, ipcClient, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, logger, Launcher); - - while (!linkedCancelToken.IsCancellationRequested) + 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) => { - await Task.WhenAny(routerTask, Task.Delay(250)).ConfigureAwait(false); - if (routerTask.IsCompleted) - break; + TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = ChooseTcpClientRouterFactory(forwardPort, logger); - if (!Console.IsInputRedirected && Console.KeyAvailable) - { - ConsoleKey cmd = Console.ReadKey(true).Key; - if (cmd == ConsoleKey.Q) - { - cancelRouterTask.Cancel(); - break; - } - } - } + var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpClientRouter(linkedCancelToken.Token, ipcClient, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); + } + + class IpcServerWebSocketServerRunner : SpecificRunnerBase + { + public IpcServerWebSocketServerRunner(string verbose) : base(verbose) { } - return routerTask.Result; + public override void ConfigureLauncher(CancellationToken cancellationToken) + { + Launcher.SuspendProcess = false; + Launcher.ConnectMode = true; + Launcher.Verbose = LogLevel != LogLevel.Information; + Launcher.CommandToken = cancellationToken; + } } 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); NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => { Console.WriteLine("building a new web socket server"); return new WebSocketServer.WebSocketServerImpl(); }); - 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 IpcServerWebSocketServerRunner(verbose); - using var factory = LoggerFactory.Create(builder => + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => { - builder.SetMinimumLevel(logLevel); - builder.AddSimpleConsole(configure => - { - configure.IncludeScopes = true; - }); - }); - - Launcher.SuspendProcess = false; - Launcher.ConnectMode = true; - Launcher.Verbose = logLevel != LogLevel.Information; - Launcher.CommandToken = token; - - var logger = factory.CreateLogger("dotnet-dsrouter"); - - logger.LogInformation("started with options: '{ipcServer}' '{webSocket}' '{runtimeTimeout}' '{verbose}'", ipcServer, webSocket, runtimeTimeout, verbose); + logger.LogInformation("started with options: '{ipcServer}' '{webSocket}' '{runtimeTimeout}' '{verbose}'", ipcServer, webSocket, runtimeTimeout, verbose); - NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; + NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; - if (string.IsNullOrEmpty(ipcServer)) - ipcServer = GetDefaultIpcServerPath(logger); + if (string.IsNullOrEmpty(ipcServer)) + ipcServer = GetDefaultIpcServerPath(logger); - var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, 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; + var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); } static string GetDefaultIpcServerPath(ILogger logger) @@ -414,6 +320,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) From 5e837572acf0e2d7ee9c774bcab077aa6b905b6e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Wed, 19 Oct 2022 16:46:52 -0400 Subject: [PATCH 17/30] Add client-webserver mode In this mode the tracing command will start a domain socket and the runtime will connect to it: ``` dotnet trace collect --diagnostic-port /tmp/sock ``` Then ``` dotnet run --project src/Tools/dotnet-dsrouter -- client-websocket -ipcc /tmp/sock -ws http://localhost:8088/diagnostics ``` And finally launch the app in the browser http://localhost:8000 (or whatever you configured) In this mode, the app should be configured without suspending on startup ``` ``` --- .../DiagnosticsServerRouterCommands.cs | 35 +++++++++++++++++++ src/Tools/dotnet-dsrouter/Program.cs | 17 +++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 420f0954af..16e33adb01 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -283,6 +283,41 @@ public async Task RunIpcServerWebSocketServerRouter(CancellationToken token }, token); } + 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; + } + } + + public async Task RunIpcClientWebSocketServerRouter(CancellationToken token, string ipcClient, string webSocket, int runtimeTimeout, string verbose) + { + NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => + { + Console.WriteLine("building a new web socket server"); + return new WebSocketServer.WebSocketServerImpl(); + }); + + var runner = new IpcClientWebSocketServerRunner(verbose); + + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + { + logger.LogInformation("started with options: '{ipcClient}' '{webSocket}' '{runtimeTimeout}' '{verbose}'", ipcClient, webSocket, runtimeTimeout, verbose); + + NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; + + var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, launcherCallbacks); + return routerTask; + }, token); + } + + static string GetDefaultIpcServerPath(ILogger logger) { int processId = Process.GetCurrentProcess().Id; diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index cb0384f8ec..745169f54b 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -24,6 +24,9 @@ internal class Program 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", @@ -75,6 +78,19 @@ private static Command IpcServerWebSocketServerRouterCommand() => IpcServerAddressOption(), WebSocketURLAddressOption(), RuntimeTimeoutOption(), VerboseOption() }; + private static Command IpcClientWebSocketServerRouterCommand() => + new Command( + name: "client-websocket", + description: "Starts a .NET application Diagnostic Server routing local IPC client <--> 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", @@ -178,6 +194,7 @@ private static int Main(string[] args) .AddCommand(IpcServerTcpClientRouterCommand()) .AddCommand(IpcClientTcpClientRouterCommand()) .AddCommand(IpcServerWebSocketServerRouterCommand()) + .AddCommand(IpcClientWebSocketServerRouterCommand()) .UseDefaults() .Build(); From 912f9ea12534bb1ce7f95db76b90a0dd53231380 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 20 Oct 2022 08:58:04 -0400 Subject: [PATCH 18/30] Remove development WriteLines --- .../WebSocketServer.cs | 4 ---- .../WebSocketStreamAdapter.cs | 1 - .../dotnet-dsrouter/DiagnosticsServerRouterCommands.cs | 6 ------ 3 files changed, 11 deletions(-) diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs index d2aa48409f..7985a99916 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs @@ -33,7 +33,6 @@ public WebSocketServerImpl() { } public async Task StartServer(Uri uri, CancellationToken cancellationToken) { - Console.WriteLine("Starting web socket server on {0}", uri); WebSocketServer.Options options = new() { Scheme = uri.Scheme, @@ -44,8 +43,6 @@ public async Task StartServer(Uri uri, CancellationToken cancellationToken) _server = WebSocketServer.CreateWebServer(options, HandleWebSocket); await _server.StartWebServer(cancellationToken); - Console.WriteLine("Started web socket server on {0}", uri); - } @@ -57,7 +54,6 @@ public async Task StopServer(CancellationToken cancellationToken) public async Task HandleWebSocket(HttpContext context, WebSocket webSocket, CancellationToken cancellationToken) { - Console.WriteLine("got a connection on the websocket"); await QueueWebSocketUntilClose(context, webSocket, cancellationToken); } diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs index 5753654aca..eb3593a058 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs @@ -86,7 +86,6 @@ protected override void Dispose(bool disposing) { if (disposing) { - Console.WriteLine("WebSocket stream adapter Dispose(true)"); _onDispose(); _webSocket.Dispose(); } diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 16e33adb01..8bdad55c6b 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -263,7 +263,6 @@ public async Task RunIpcServerWebSocketServerRouter(CancellationToken token { NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => { - Console.WriteLine("building a new web socket server"); return new WebSocketServer.WebSocketServerImpl(); }); @@ -271,8 +270,6 @@ public async Task RunIpcServerWebSocketServerRouter(CancellationToken token return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => { - logger.LogInformation("started with options: '{ipcServer}' '{webSocket}' '{runtimeTimeout}' '{verbose}'", ipcServer, webSocket, runtimeTimeout, verbose); - NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; if (string.IsNullOrEmpty(ipcServer)) @@ -300,7 +297,6 @@ public async Task RunIpcClientWebSocketServerRouter(CancellationToken token { NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => { - Console.WriteLine("building a new web socket server"); return new WebSocketServer.WebSocketServerImpl(); }); @@ -308,8 +304,6 @@ public async Task RunIpcClientWebSocketServerRouter(CancellationToken token return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => { - logger.LogInformation("started with options: '{ipcClient}' '{webSocket}' '{runtimeTimeout}' '{verbose}'", ipcClient, webSocket, runtimeTimeout, verbose); - NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, launcherCallbacks); From 8b4b85618025b4e800dad48f2714016a801f02b9 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 20 Oct 2022 10:57:29 -0400 Subject: [PATCH 19/30] Remove IpcWebSocketEndPointer class it was just encapsulating a single URL parser call. --- .../DiagnosticsIpc/IpcWebSocketEndPoint.cs | 76 ------------------- .../IpcWebSocketServerTransport.cs | 49 +++++++++++- .../DiagnosticsServerRouterFactory.cs | 13 ++-- 3 files changed, 50 insertions(+), 88 deletions(-) delete mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketEndPoint.cs diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketEndPoint.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketEndPoint.cs deleted file mode 100644 index 72ec73d80e..0000000000 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketEndPoint.cs +++ /dev/null @@ -1,76 +0,0 @@ -// 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.Net; -using System.Net.Sockets; - -namespace Microsoft.Diagnostics.NETCore.Client -{ - internal sealed class IpcWebSocketEndPoint - { - public Uri EndPoint { get; } - - public static bool IsWebSocketEndPoint(string endPoint) - { - bool result = true; - - try - { - ParseWebSocketEndPoint(endPoint, out _); - } - catch (Exception) - { - result = false; - } - - return result; - } - - public IpcWebSocketEndPoint(string endPoint) - { - ParseWebSocketEndPoint(endPoint, out Uri uri); - EndPoint = uri; - } - - private static void ParseWebSocketEndPoint(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)); - } - } - } -} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs index df7d2ed8b1..3d68ef5092 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs @@ -13,12 +13,12 @@ internal sealed class IpcWebSocketServerTransport : IpcServerTransport private static Singleton singleton = new Singleton(); private readonly CancellationTokenSource _cancellation; private readonly int _maxConnections; - private readonly IpcWebSocketEndPoint _endPoint; + private readonly Uri _endPoint; public IpcWebSocketServerTransport(string address, int maxAllowedConnections, IIpcServerTransportCallbackInternal transportCallback = null) : base(transportCallback) { _maxConnections = maxAllowedConnections; - _endPoint = new IpcWebSocketEndPoint(address); + ParseWebSocketURL(address, out _endPoint); _cancellation = new CancellationTokenSource(); } @@ -45,6 +45,47 @@ public override async Task AcceptAsync(CancellationToken token) return s; } + 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)); + } + } + + internal class Singleton { private volatile int _refCount; @@ -73,7 +114,7 @@ public void DropRef() } } - internal async Task StartServer(IpcWebSocketEndPoint endPoint, CancellationToken token) + internal async Task StartServer(Uri endPoint, CancellationToken token) { if (ServerStopping) { @@ -82,7 +123,7 @@ internal async Task StartServer(IpcWebSocketEndPoint endPoint, CancellationToken ServerRunning = true; WebSocketServer.IWebSocketServer newServer = WebSocketServer.WebSocketServerFactory.CreateWebSocketServer(); server = newServer; ; - await server.StartServer(endPoint.EndPoint, token); + await server.StartServer(endPoint, token); await Task.Delay(1000); } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index ff8f8f61b0..2a29684f0a 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -342,14 +342,11 @@ public override void CreatedNewServer(EndPoint localEP) internal class WebSocketServerRouterFactory : NetServerRouterFactory { - IpcWebSocketEndPoint _webSocketEndPoint; + private readonly string _webSocketURL; ReversedDiagnosticsServer _webSocketServer; - public Uri WebSocketURL - { - get { return _webSocketEndPoint.EndPoint; } - } + public string WebSocketURL => _webSocketURL; public static WebSocketServerRouterFactory CreateDefaultInstance(string webSocketURL, int runtimeTimeoutMs, ILogger logger) { @@ -359,9 +356,9 @@ public static WebSocketServerRouterFactory CreateDefaultInstance(string webSocke public WebSocketServerRouterFactory(string webSocketURL, int runtimeTimeoutMs, ILogger logger) : base(runtimeTimeoutMs, logger) { - _webSocketEndPoint = new IpcWebSocketEndPoint(string.IsNullOrEmpty(webSocketURL) ? "ws://127.0.0.1:8088/diagnostics" : webSocketURL); + _webSocketURL = string.IsNullOrEmpty(webSocketURL) ? "ws://127.0.0.1:8088/diagnostics" : webSocketURL; - _webSocketServer = new ReversedDiagnosticsServer(_webSocketEndPoint.EndPoint.ToString(), ReversedDiagnosticsServer.Kind.WebSocket); + _webSocketServer = new ReversedDiagnosticsServer(_webSocketURL, ReversedDiagnosticsServer.Kind.WebSocket); _webSocketServer.TransportCallback = this; } @@ -388,7 +385,7 @@ public override void Reset() } protected override Task AcceptAsyncImpl(CancellationToken token) => _webSocketServer.AcceptAsync(token); - public override string ServerAddress => WebSocketURL.ToString(); + public override string ServerAddress => WebSocketURL; public override string ServerTransportName => "WebSocket"; public override void CreatedNewServer(EndPoint localEP) From 9260d0c363e013609a05ee7cdb42099e2fd55a4e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 20 Oct 2022 11:02:19 -0400 Subject: [PATCH 20/30] Pass LogLevel from dotnet-dsrouter to the webserver --- .../WebSocketServer.cs | 12 +++++++++--- .../DiagnosticsServerRouterCommands.cs | 11 ++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs index 7985a99916..958f40785e 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs @@ -28,8 +28,12 @@ public class WebSocketServerImpl : IWebSocketServer { private WebSocketServer _server = null; private readonly Queue _acceptQueue = new Queue(); + private readonly LogLevel _logLevel; - public WebSocketServerImpl() { } + public WebSocketServerImpl(LogLevel logLevel) + { + _logLevel = logLevel; + } public async Task StartServer(Uri uri, CancellationToken cancellationToken) { @@ -39,6 +43,7 @@ public async Task StartServer(Uri uri, CancellationToken cancellationToken) Host = uri.Host, Port = uri.Port.ToString(), Path = uri.PathAndQuery, + LogLevel = _logLevel, }; _server = WebSocketServer.CreateWebServer(options, HandleWebSocket); @@ -174,6 +179,7 @@ public record Options 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) @@ -197,8 +203,8 @@ public static WebSocketServer CreateWebServer(Options options, Func { - /* FIXME: delegate to outer host's logging */ - logging.AddConsole().AddFilter(null, LogLevel.Debug); + /* FIXME: use a delegating provider that sends the output to the dotnet-dsrouter LoggerFactory */ + logging.AddConsole().AddFilter(null, options.LogLevel); }) .ConfigureServices((ctx, services) => { diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index 8bdad55c6b..fb2039662e 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -261,12 +261,13 @@ public override void ConfigureLauncher(CancellationToken cancellationToken) public async Task RunIpcServerWebSocketServerRouter(CancellationToken token, string ipcServer, string webSocket, int runtimeTimeout, string verbose) { + var runner = new IpcServerWebSocketServerRunner(verbose); + NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => { - return new WebSocketServer.WebSocketServerImpl(); + return new WebSocketServer.WebSocketServerImpl(runner.LogLevel); }); - var runner = new IpcServerWebSocketServerRunner(verbose); return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => { @@ -295,13 +296,13 @@ public override void ConfigureLauncher(CancellationToken cancellationToken) public async Task RunIpcClientWebSocketServerRouter(CancellationToken token, string ipcClient, string webSocket, int runtimeTimeout, string verbose) { + var runner = new IpcClientWebSocketServerRunner(verbose); + NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => { - return new WebSocketServer.WebSocketServerImpl(); + return new WebSocketServer.WebSocketServerImpl(runner.LogLevel); }); - var runner = new IpcClientWebSocketServerRunner(verbose); - return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => { NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; From 836fe65f998fb2248edf8f9fba89744c1bd99500 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 20 Oct 2022 13:09:33 -0400 Subject: [PATCH 21/30] Add comments, remove unused interface --- .../WebSocketServer.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs index 958f40785e..ef3fba2e53 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs @@ -27,6 +27,10 @@ namespace Microsoft.Diagnostics.WebSocketServer; public class WebSocketServerImpl : IWebSocketServer { private WebSocketServer _server = null; + + // 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; @@ -59,6 +63,8 @@ public async Task StopServer(CancellationToken cancellationToken) 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); } @@ -73,6 +79,8 @@ internal async Task QueueWebSocketUntilClose(HttpContext context, WebSocket webS 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); } @@ -82,7 +90,8 @@ public async Task AcceptConnection(CancellationToken cancellationToken) return conn.GetStream(); } - // single-element queue + // 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; @@ -143,6 +152,7 @@ public async Task Dequeue(CancellationToken cancellationToken) } + // An abstraction encapsulating an open websocket connection. internal class Conn { private readonly WebSocket _webSocket; @@ -166,11 +176,6 @@ private void OnStreamDispose() } } -public interface IWebSocketConnectionHandler -{ - Task Handle(HttpContext context, WebSocket webSocket, CancellationToken cancellationToken); -} - public class WebSocketServer { public record Options From e0d928cc0962613f72f8c3593fd0f2e79498946d Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 20 Oct 2022 13:23:07 -0400 Subject: [PATCH 22/30] code organization cleanup rename the class that encapsulated IHost and the embedded Kestrel instance to EmbeddedWebServer add comments explaining the various pieces --- .../WebSocketServer/IWebSocketServer.cs | 2 + .../IWebSocketStreamAdapter.cs | 2 + .../WebSocketServer/WebSocketServerFactory.cs | 2 + .../EmbeddedWebSocketServer.cs | 154 ++++++++++++++++++ ...SocketServer.cs => WebSocketServerImpl.cs} | 153 +---------------- 5 files changed, 168 insertions(+), 145 deletions(-) create mode 100644 src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs rename src/Microsoft.Diagnostics.WebSocketServer/{WebSocketServer.cs => WebSocketServerImpl.cs} (51%) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs index 2b07a31f0f..0a918f886a 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs @@ -10,6 +10,8 @@ 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. public interface IWebSocketServer { public Task StartServer(Uri uri, CancellationToken cancellationToken); diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs index 14e904ed35..78e1bf667a 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs @@ -3,6 +3,8 @@ 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. public interface IWebSocketStreamAdapter { public bool IsConnected { get; } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs index 95f9c88da0..00a6f7c0e7 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs @@ -3,6 +3,8 @@ 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 WebSocketServerFactory { internal static void SetBuilder(Func builder) diff --git a/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs new file mode 100644 index 0000000000..c984a2cef8 --- /dev/null +++ b/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.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. + +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/WebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs similarity index 51% rename from src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs rename to src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs index ef3fba2e53..bf727a8e13 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServer.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs @@ -5,28 +5,21 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using CancellationToken = System.Threading.CancellationToken; -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.Diagnostics.NETCore.Client.WebSocketServer; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using LogLevel = Microsoft.Extensions.Logging.LogLevel; + using System.Net.WebSockets; using HttpContext = Microsoft.AspNetCore.Http.HttpContext; -using System.Linq; -namespace Microsoft.Diagnostics.WebSocketServer; +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 WebSocketServer _server = null; + private EmbeddedWebSocketServer _server = null; // 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 @@ -41,7 +34,7 @@ public WebSocketServerImpl(LogLevel logLevel) public async Task StartServer(Uri uri, CancellationToken cancellationToken) { - WebSocketServer.Options options = new() + EmbeddedWebSocketServer.Options options = new() { Scheme = uri.Scheme, Host = uri.Host, @@ -49,7 +42,7 @@ public async Task StartServer(Uri uri, CancellationToken cancellationToken) Path = uri.PathAndQuery, LogLevel = _logLevel, }; - _server = WebSocketServer.CreateWebServer(options, HandleWebSocket); + _server = EmbeddedWebSocketServer.CreateWebServer(options, HandleWebSocket); await _server.StartWebServer(cancellationToken); } @@ -175,133 +168,3 @@ private void OnStreamDispose() } } } - -public class WebSocketServer -{ - 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 WebSocketServer(IHost host) - { - _host = host; - } - - private static string[] MakeUrls(string scheme, string host, string port) => new string[] { $"{scheme}://{host}:{port}" }; - public static WebSocketServer 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 WebSocketServer(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); - } -} From 45fd0aa379e16056a76c2ea161aefded54a79dca Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 20 Oct 2022 13:30:48 -0400 Subject: [PATCH 23/30] more comments --- .../DiagnosticsIpc/IpcWebSocketServerTransport.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs index 3d68ef5092..6ca906621f 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs @@ -28,7 +28,6 @@ protected override void Dispose(bool disposing) { _cancellation.Cancel(); - // _stream.Dispose(); singleton.DropRef(); _cancellation.Dispose(); @@ -86,6 +85,11 @@ private static void ParseWebSocketURL(string endPoint, out Uri uri) } + // a coordination class that ensures we only start a single webserver even as the + // diagnostic server protocol requires us to accept multiple connections. + // while it is alive, each connection owns one reference to this singleton. + // when the reference count goes from 0->1, we start the server. + // when the references drops back to zero, we let the webserver stop. internal class Singleton { private volatile int _refCount; From 11ad7b4a7ec5fc0a08f424bc1779811d16476b58 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 20 Oct 2022 13:35:50 -0400 Subject: [PATCH 24/30] fix line damage --- .../Microsoft.Diagnostics.NETCore.Client.csproj | 1 - 1 file changed, 1 deletion(-) 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 ee5f37b2ee..2553b9e0d7 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj +++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj @@ -40,5 +40,4 @@ - From 5dca4f294e3637a2ccd84ea78256c441cf263c1a Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Thu, 20 Oct 2022 13:38:26 -0400 Subject: [PATCH 25/30] make the IWebSocketServer interface internal and IVT for Microsoft.Diagnostics.WebSocketServer that holds the implementation --- .../Microsoft.Diagnostics.NETCore.Client.csproj | 1 + .../WebSocketServer/IWebSocketServer.cs | 2 +- .../WebSocketServer/IWebSocketStreamAdapter.cs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) 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/WebSocketServer/IWebSocketServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs index 0a918f886a..f9c8ca83b7 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs @@ -12,7 +12,7 @@ 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. -public interface IWebSocketServer +internal interface IWebSocketServer { public Task StartServer(Uri uri, CancellationToken cancellationToken); diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs index 78e1bf667a..4cb6fb3d6c 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketStreamAdapter.cs @@ -5,7 +5,7 @@ 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. -public interface IWebSocketStreamAdapter +internal interface IWebSocketStreamAdapter { public bool IsConnected { get; } } \ No newline at end of file From d18b37ee7b7acfe3009606f23ea3d0567ff3025d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksey=20Kliger=20=28=CE=BBgeek=29?= Date: Fri, 28 Oct 2022 12:54:04 -0400 Subject: [PATCH 26/30] Fix formatting and whitespace Co-authored-by: Johan Lorensson --- .../DiagnosticsServerRouterFactory.cs | 5 ----- .../ReversedServer/ReversedDiagnosticsServer.cs | 1 - .../WebSocketServer/IWebSocketServer.cs | 1 - .../EmbeddedWebSocketServer.cs | 3 --- .../WebSocketServerImpl.cs | 4 ++-- .../WebSocketStreamAdapter.cs | 1 - src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs | 1 + src/Tools/dotnet-dsrouter/Program.cs | 1 - 8 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index 2a29684f0a..0326647b27 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -272,8 +272,6 @@ public async Task AcceptNetStreamAsync(CancellationToken token) return netServerStream; } - - } /// @@ -298,7 +296,6 @@ public static TcpServerRouterFactory CreateDefaultInstance(string tcpServer, int 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); @@ -328,7 +325,6 @@ public override void Reset() public override string ServerAddress => _tcpServerAddress; public override string ServerTransportName => "TCP"; - public override void CreatedNewServer(EndPoint localEP) { if (localEP is IPEndPoint ipEP) @@ -355,7 +351,6 @@ public static WebSocketServerRouterFactory CreateDefaultInstance(string webSocke 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); diff --git a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs index b60fed0ad1..becaf95f34 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/ReversedServer/ReversedDiagnosticsServer.cs @@ -224,7 +224,6 @@ private async Task AcceptTransportAsync(IpcServerTransport transport, Cancellati IpcAdvertise advertise = null; try { - stream = await transport.AcceptAsync(token).ConfigureAwait(false); } catch (OperationCanceledException) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs index f9c8ca83b7..d12cc737dc 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs @@ -7,7 +7,6 @@ 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 diff --git a/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs b/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs index c984a2cef8..c8b0834f83 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/EmbeddedWebSocketServer.cs @@ -33,7 +33,6 @@ public record Options public string Port { get; set; } = default; public LogLevel LogLevel { get; set; } = LogLevel.Information; - public void Assign(Options other) { Scheme = other.Scheme; @@ -103,7 +102,6 @@ private static void ConfigureApplication(/*WebHostBuilderContext context,*/ IApp var options = router.ServiceProvider.GetRequiredService>().Value; router.MapGet(options.Path, (context) => OnWebSocketGet(context, connectionHandler)); }); - } public async Task StartWebServer(CancellationToken ct = default) @@ -117,7 +115,6 @@ public async Task StartWebServer(CancellationToken ct = default) .FirstOrDefault(); logger.LogInformation("ip address is {IpAddressSecure}", ipAddressSecure); - } public async Task StopWebServer(CancellationToken ct = default) diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs index bf727a8e13..8a369a9db7 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs @@ -11,7 +11,6 @@ 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. @@ -142,7 +141,6 @@ public async Task Dequeue(CancellationToken cancellationToken) } } } - } // An abstraction encapsulating an open websocket connection. @@ -151,12 +149,14 @@ 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); diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs index eb3593a058..89beacc54b 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketStreamAdapter.cs @@ -92,5 +92,4 @@ protected override void Dispose(bool disposing) } bool IWebSocketStreamAdapter.IsConnected => _webSocket.State == WebSocketState.Open; - } diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index fb2039662e..b3b2645788 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -136,6 +136,7 @@ public override void ConfigureLauncher(CancellationToken cancellationToken) Launcher.Verbose = LogLevel != LogLevel.Information; Launcher.CommandToken = cancellationToken; } + public override ILoggerFactory ConfigureLogging() { var factory = LoggerFactory.Create(builder => diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index 745169f54b..ec7df2cdea 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -26,7 +26,6 @@ internal class Program delegate Task DiagnosticsServerIpcClientWebSocketServerRouterDelegate(CancellationToken ct, string ipcClient, string webSocket, int runtimeTimeoutS, string verbose); - private static Command IpcClientTcpServerRouterCommand() => new Command( name: "client-server", From 5124c7fa86789e891cc2edd6bed498737af40528 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 28 Oct 2022 13:26:09 -0400 Subject: [PATCH 27/30] Remove unnecessary logging --- .../DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index 0326647b27..a114940184 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -359,13 +359,11 @@ public WebSocketServerRouterFactory(string webSocketURL, int runtimeTimeoutMs, I public override void Start() { - Logger.LogInformation("Starting web socket server"); _webSocketServer.Start(); } public override async Task Stop() { - Logger.LogInformation("Stopping web socket server"); await _webSocketServer.DisposeAsync().ConfigureAwait(false); } @@ -373,7 +371,6 @@ public override void Reset() { if (Endpoint != null) { - Logger.LogInformation("Resetting the web socket server"); _webSocketServer.RemoveConnection(NetServerEndpointInfo.RuntimeInstanceCookie); ResetEnpointInfo(); } @@ -385,7 +382,6 @@ public override void Reset() public override void CreatedNewServer(EndPoint localEP) { - // anything to do here? } } From 958b3d469673c922e1fc07319f81f9878aa0acab Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Fri, 28 Oct 2022 14:23:22 -0400 Subject: [PATCH 28/30] Start/Stop the web server from the toplevel Instead of trying to start the webserver when the diagnostics code first calls AcceptConnection, just start the webserver when we start running, and stop it when the router is done. This also matches the expected use by WasmAppHost (`dotnet run`) which will start/stop the webserver itself, and we will just provide middleware for diagnostics --- .../IpcWebSocketServerTransport.cs | 123 +----------------- .../WebSocketServer/IWebSocketServer.cs | 4 - ...rFactory.cs => WebSocketServerProvider.cs} | 12 +- .../WebSocketServerImpl.cs | 59 ++++++++- .../DiagnosticsServerRouterCommands.cs | 55 +++++--- 5 files changed, 100 insertions(+), 153 deletions(-) rename src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/{WebSocketServerFactory.cs => WebSocketServerProvider.cs} (53%) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs index 6ca906621f..0139b86b8b 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcWebSocketServerTransport.cs @@ -10,137 +10,18 @@ namespace Microsoft.Diagnostics.NETCore.Client; internal sealed class IpcWebSocketServerTransport : IpcServerTransport { - private static Singleton singleton = new Singleton(); - private readonly CancellationTokenSource _cancellation; - private readonly int _maxConnections; - private readonly Uri _endPoint; public IpcWebSocketServerTransport(string address, int maxAllowedConnections, IIpcServerTransportCallbackInternal transportCallback = null) : base(transportCallback) { - _maxConnections = maxAllowedConnections; - ParseWebSocketURL(address, out _endPoint); - _cancellation = new CancellationTokenSource(); } protected override void Dispose(bool disposing) { - if (disposing) - { - _cancellation.Cancel(); - - singleton.DropRef(); - - _cancellation.Dispose(); - } } public override async Task AcceptAsync(CancellationToken token) { - if (singleton.AddRef()) - { - await singleton.StartServer(_endPoint, token); - } - Stream s = await singleton.AcceptConnection(token); - return s; - } - - 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)); - } - } - - - // a coordination class that ensures we only start a single webserver even as the - // diagnostic server protocol requires us to accept multiple connections. - // while it is alive, each connection owns one reference to this singleton. - // when the reference count goes from 0->1, we start the server. - // when the references drops back to zero, we let the webserver stop. - internal class Singleton - { - private volatile int _refCount; - public bool ServerRunning { get; internal set; } = false; - public bool ServerStopping { get; internal set; } = false; - public WebSocketServer.IWebSocketServer server { get; internal set; } = null; - internal Singleton() - { - _refCount = 0; - } - - public bool AddRef() - { - return Interlocked.Increment(ref _refCount) == 1; - } - - public void DropRef() - { - if (_refCount == 0) - { - throw new InvalidOperationException("DropRef called more times than AddRef"); - } - if (Interlocked.Decrement(ref _refCount) == 0) - { - StopServer().Wait(); - } - } - - internal async Task StartServer(Uri endPoint, CancellationToken token) - { - if (ServerStopping) - { - return; - } - ServerRunning = true; - WebSocketServer.IWebSocketServer newServer = WebSocketServer.WebSocketServerFactory.CreateWebSocketServer(); - server = newServer; ; - await server.StartServer(endPoint, token); - await Task.Delay(1000); - } - - internal async Task StopServer(CancellationToken token = default) - { - ServerStopping = true; - await server.StopServer(token); - ServerRunning = false; - } - - internal async Task AcceptConnection(CancellationToken token) - { - return await server.AcceptConnection(token); - } + WebSocketServer.IWebSocketServer server = WebSocketServer.WebSocketServerProvider.GetWebSocketServerInstance(); + return await server.AcceptConnection(token); } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs index d12cc737dc..0b6c5d2b2b 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/IWebSocketServer.cs @@ -13,9 +13,5 @@ namespace Microsoft.Diagnostics.NETCore.Client.WebSocketServer; // in order to avoid a dependency on ASP.NET in the client library. internal interface IWebSocketServer { - public Task StartServer(Uri uri, CancellationToken cancellationToken); - - public Task StopServer(CancellationToken cancellationToken); - public Task AcceptConnection(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerProvider.cs similarity index 53% rename from src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs rename to src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerProvider.cs index 00a6f7c0e7..312c1a25e2 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/WebSocketServer/WebSocketServerProvider.cs @@ -5,17 +5,17 @@ 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 WebSocketServerFactory +internal class WebSocketServerProvider { - internal static void SetBuilder(Func builder) + internal static void SetProvider(Func provider) { - _builder = builder; + _provider = provider; } - internal static IWebSocketServer CreateWebSocketServer() + internal static IWebSocketServer GetWebSocketServerInstance() { - return _builder(); + return _provider(); } - private static Func _builder; + private static Func _provider; } \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs index 8a369a9db7..1fe85b7f01 100644 --- a/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs +++ b/src/Microsoft.Diagnostics.WebSocketServer/WebSocketServerImpl.cs @@ -19,6 +19,7 @@ namespace Microsoft.Diagnostics.WebSocketServer; 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 @@ -31,8 +32,15 @@ public WebSocketServerImpl(LogLevel logLevel) _logLevel = logLevel; } - public async Task StartServer(Uri uri, CancellationToken cancellationToken) + 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, @@ -46,9 +54,14 @@ public async Task StartServer(Uri uri, CancellationToken cancellationToken) 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; } @@ -143,6 +156,48 @@ public async Task Dequeue(CancellationToken cancellationToken) } } + 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 { diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index b3b2645788..e723464c05 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -264,22 +264,29 @@ public async Task RunIpcServerWebSocketServerRouter(CancellationToken token { var runner = new IpcServerWebSocketServerRunner(verbose); - NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => - { - return new WebSocketServer.WebSocketServerImpl(runner.LogLevel); - }); + WebSocketServer.WebSocketServerImpl server = new(runner.LogLevel); + NETCore.Client.WebSocketServer.WebSocketServerProvider.SetProvider(() => server); - return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + try { - NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; + Task _ = Task.Run(() => server.StartServer(webSocket, token)); - if (string.IsNullOrEmpty(ipcServer)) - ipcServer = GetDefaultIpcServerPath(logger); + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + { + NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; - var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, launcherCallbacks); - return routerTask; - }, token); + 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 + { + await server.StopServer(token); + } } class IpcClientWebSocketServerRunner : SpecificRunnerBase @@ -299,18 +306,26 @@ public async Task RunIpcClientWebSocketServerRouter(CancellationToken token { var runner = new IpcClientWebSocketServerRunner(verbose); - NETCore.Client.WebSocketServer.WebSocketServerFactory.SetBuilder(() => - { - return new WebSocketServer.WebSocketServerImpl(runner.LogLevel); - }); + WebSocketServer.WebSocketServerImpl server = new(runner.LogLevel); - return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + NETCore.Client.WebSocketServer.WebSocketServerProvider.SetProvider(() => server); + + try { - NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; + Task _ = Task.Run(() => server.StartServer(webSocket, token)); - var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, webSocket, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, webSocketServerRouterFactory, logger, launcherCallbacks); - return routerTask; - }, token); + return await runner.CommonRunLoop((logger, launcherCallbacks, linkedCancelToken) => + { + NetServerRouterFactory.CreateInstanceDelegate webSocketServerRouterFactory = WebSocketServerRouterFactory.CreateDefaultInstance; + + 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); + } } From 5b36507dc3e50a2d452fca62281ea57a6baf6cf8 Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 15 Nov 2022 10:00:12 -0500 Subject: [PATCH 29/30] Use net6.0 logging for dotnet-dsrouter --- eng/Versions.props | 3 +++ src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 95e6a75d9e..2f6ac0a534 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/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj index f895b4a2c4..b0a498179f 100644 --- a/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj +++ b/src/Tools/dotnet-dsrouter/dotnet-dsrouter.csproj @@ -24,8 +24,8 @@ - - + + From f851bb3a3dc4a3a760434a382d2183ee08f4b93e Mon Sep 17 00:00:00 2001 From: Aleksey Kliger Date: Tue, 15 Nov 2022 10:00:38 -0500 Subject: [PATCH 30/30] fix help string for dsrouter client-websocket command --- src/Tools/dotnet-dsrouter/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index ec7df2cdea..cd91232494 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -80,7 +80,7 @@ private static Command IpcServerWebSocketServerRouterCommand() => private static Command IpcClientWebSocketServerRouterCommand() => new Command( name: "client-websocket", - description: "Starts a .NET application Diagnostic Server routing local IPC client <--> remote WebSocket client. " + + 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).") {