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