Add default iOS/Android commands to dotnet-dsrouter. (#4090)
authorJohan Lorensson <lateralusx.github@gmail.com>
Tue, 29 Aug 2023 18:18:47 +0000 (20:18 +0200)
committerGitHub <noreply@github.com>
Tue, 29 Aug 2023 18:18:47 +0000 (18:18 +0000)
dotnet-dsrouter can be a little hard to configure for mobile use cases
since it needs a number of arguments, both to setup its local IPC
client|server and the corresponding TCP client|server and what arguments
to use depends on what mobile platform and scenario the user runs.

There are currently a number of different scenarios described in
different sources of documentation:

Runtime docs:

https://github.com/dotnet/runtime/blob/main/docs/design/mono/diagnostics-tracing.md

iOS SDK docs:

https://github.com/xamarin/xamarin-macios/wiki/Profiling

Android SDK docs:

https://github.com/xamarin/xamarin-android/blob/main/Documentation/guides/tracing.md

They all fall into a number of predefined scenarios and a number of
"default" parameters that should be used.

This PR creates 4 new commands in dotnet-dsrouter to explicitly use the
defaults described in the documentation, ios-sim, ios, android-emu,
android. They all fallback to default IPC server listener for dsrouter
and in order to make that simpler in use with diagnostic tooling,
changes done in https://github.com/dotnet/diagnostics/pull/4081 is also
needed to simplify the process.

So lets take an example form the docs, running an app on iOS simulator.
Before this PR the following dsrouter command needs to be run:

```
dotnet-dsrouter client-server -ipcc ~/my-sim-port -tcps 127.0.0.1:9000

launch app with DOTNET_DiagnosticPorts=127.0.0.1:9000

dotnet-trace collect --diagnostic-port ~/my-sim-port --format speedscope
```

With this PR (and https://github.com/dotnet/diagnostics/pull/4081) the
above will look like:

```
dotnet-dsrouter ios-sim

launch app with DOTNET_DiagnosticPorts=127.0.0.1:9000

dotnet-trace collect -p:<dsrouter pid> --format speedscope
```

dontet-dsrouter will output both its pid as well as what
DOTNET_DiagnosticPorts that can be used to connect to it on startup.

Using a different mobile platform/scenario, like android emulator is
pretty much identical, just switch ios-sim to android-emu.
dotnet-dsrouter will output the exact DOTNET_DiagnosticPorts that should
be used with any of the configured scenarios.

src/Tools/dotnet-dsrouter/ADBTcpRouterFactory.cs
src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs
src/Tools/dotnet-dsrouter/Program.cs

index e910be887f3c02e5e398a4ee1ff9cb54419f933d..5177e6b30711eea67750b0cab164ae0b1846b039 100644 (file)
@@ -4,6 +4,7 @@
 using System;
 using System.Diagnostics;
 using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Diagnostics.NETCore.Client;
 using Microsoft.Extensions.Logging;
@@ -91,8 +92,9 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             {
                 processStartedResult = process.Start();
             }
-            catch (Exception)
+            catch (Exception ex)
             {
+                logger.LogError($"Failed executing {adbTool} {command}. Error: {ex.Message}.");
             }
 
             if (processStartedResult)
@@ -107,12 +109,12 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
 
                 if (!string.IsNullOrEmpty(stdout))
                 {
-                    logger.LogTrace($"stdout: {stdout}");
+                    logger.LogTrace($"stdout: {stdout.TrimEnd()}");
                 }
 
                 if (!string.IsNullOrEmpty(stderr))
                 {
-                    logger.LogError($"stderr: {stderr}");
+                    logger.LogError($"stderr: {stderr.TrimEnd()}");
                 }
             }
 
@@ -130,6 +132,8 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
     {
         private readonly int _port;
         private bool _ownsPortReverse;
+        private Task _portReverseTask;
+        private CancellationTokenSource _portReverseTaskCancelToken;
 
         public static TcpServerRouterFactory CreateADBInstance(string tcpServer, int runtimeTimeoutMs, ILogger logger)
         {
@@ -147,6 +151,19 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             // Enable port reverse.
             _ownsPortReverse = ADBCommandExec.AdbAddPortReverse(_port, Logger);
 
+            _portReverseTaskCancelToken = new CancellationTokenSource();
+            _portReverseTask = Task.Run(async () => {
+                using PeriodicTimer timer = new(TimeSpan.FromSeconds(5));
+                while (await timer.WaitForNextTickAsync(_portReverseTaskCancelToken.Token).ConfigureAwait(false) && !_portReverseTaskCancelToken.Token.IsCancellationRequested)
+                {
+                    // Make sure reverse port configuration is still active.
+                    if (ADBCommandExec.AdbAddPortReverse(_port, Logger) && !_ownsPortReverse)
+                    {
+                        _ownsPortReverse = true;
+                    }
+                }
+            }, _portReverseTaskCancelToken.Token);
+
             base.Start();
         }
 
@@ -154,6 +171,13 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
         {
             await base.Stop().ConfigureAwait(false);
 
+            try
+            {
+                _portReverseTaskCancelToken.Cancel();
+                await _portReverseTask.ConfigureAwait(false);
+            }
+            catch { }
+
             // Disable port reverse.
             ADBCommandExec.AdbRemovePortReverse(_port, _ownsPortReverse, Logger);
             _ownsPortReverse = false;
@@ -164,6 +188,8 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
     {
         private readonly int _port;
         private bool _ownsPortForward;
+        private Task _portForwardTask;
+        private CancellationTokenSource _portForwardTaskCancelToken;
 
         public static TcpClientRouterFactory CreateADBInstance(string tcpClient, int runtimeTimeoutMs, ILogger logger)
         {
@@ -180,10 +206,30 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
         {
             // Enable port forwarding.
             _ownsPortForward = ADBCommandExec.AdbAddPortForward(_port, _logger);
+
+            _portForwardTaskCancelToken = new CancellationTokenSource();
+            _portForwardTask = Task.Run(async () => {
+                using PeriodicTimer timer = new(TimeSpan.FromSeconds(5));
+                while (await timer.WaitForNextTickAsync(_portForwardTaskCancelToken.Token).ConfigureAwait(false) && !_portForwardTaskCancelToken.Token.IsCancellationRequested)
+                {
+                    // Make sure forward port configuration is still active.
+                    if (ADBCommandExec.AdbAddPortForward(_port, _logger) && !_ownsPortForward)
+                    {
+                        _ownsPortForward = true;
+                    }
+                }
+            }, _portForwardTaskCancelToken.Token);
         }
 
         public override void Stop()
         {
+            try
+            {
+                _portForwardTaskCancelToken.Cancel();
+                _portForwardTask.Wait();
+            }
+            catch { }
+
             // Disable port forwarding.
             ADBCommandExec.AdbRemovePortForward(_port, _ownsPortForward, _logger);
             _ownsPortForward = false;
index 59692a9e103c53a4d24a05b6fdc028077ff52994..5b83ceda81fcbf7564a1e632daeef8682aa429fa 100644 (file)
@@ -347,6 +347,30 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             }
         }
 
+        public async Task<int> RunIpcServerIOSSimulatorRouter(CancellationToken token, int runtimeTimeout, string verbose)
+        {
+            logDiagnosticPortsConfiguration("ios simulator", "127.0.0.1:9000", false, verbose);
+            return await RunIpcServerTcpServerRouter(token, "", "127.0.0.1:9000", runtimeTimeout, verbose, "").ConfigureAwait(false);
+        }
+
+        public async Task<int> RunIpcServerIOSRouter(CancellationToken token, int runtimeTimeout, string verbose)
+        {
+            logDiagnosticPortsConfiguration("ios device", "127.0.0.1:9000", true, verbose);
+            return await RunIpcServerTcpClientRouter(token, "", "127.0.0.1:9000", runtimeTimeout, verbose, "iOS").ConfigureAwait(false);
+        }
+
+        public async Task<int> RunIpcServerAndroidEmulatorRouter(CancellationToken token, int runtimeTimeout, string verbose)
+        {
+            logDiagnosticPortsConfiguration("android emulator", "10.0.2.2:9000", false, verbose);
+            return await RunIpcServerTcpServerRouter(token, "", "127.0.0.1:9000", runtimeTimeout, verbose, "").ConfigureAwait(false);
+        }
+
+        public async Task<int> RunIpcServerAndroidRouter(CancellationToken token, int runtimeTimeout, string verbose)
+        {
+            logDiagnosticPortsConfiguration("android emulator", "127.0.0.1:9000", false, verbose);
+            return await RunIpcServerTcpServerRouter(token, "", "127.0.0.1:9000", runtimeTimeout, verbose, "Android").ConfigureAwait(false);
+        }
+
         private static string GetDefaultIpcServerPath(ILogger logger)
         {
             string path = string.Empty;
@@ -412,6 +436,23 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
             return tcpServerRouterFactory;
         }
 
+        private static void logDiagnosticPortsConfiguration(string deviceName, string deviceTcpIpAddress, bool deviceListenMode, string verbose)
+        {
+            StringBuilder message = new();
+
+            if (!string.IsNullOrEmpty(verbose))
+            {
+                deviceName = !string.IsNullOrEmpty(deviceName) ? $" on {deviceName} " : " ";
+                message.AppendLine($"Start an application{deviceName}with one of the following environment variables set:");
+            }
+
+            string listenMode = deviceListenMode ? ",listen" : ",connect";
+            message.AppendLine($"DOTNET_DiagnosticPorts={deviceTcpIpAddress},nosuspend{listenMode}");
+            message.AppendLine($"DOTNET_DiagnosticPorts={deviceTcpIpAddress},suspend{listenMode}");
+
+            Console.WriteLine(message.ToString());
+        }
+
         private static void checkLoopbackOnly(string tcpServer)
         {
             if (!string.IsNullOrEmpty(tcpServer) && !DiagnosticsServerRouterRunner.isLoopbackOnly(tcpServer))
index c5fe75b1f4d37fd38fe6848b46154c933e210834..4ed1a3ee98c8963054305ebc6269def02ef0d86d 100644 (file)
@@ -27,6 +27,14 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
 
         private delegate Task<int> DiagnosticsServerIpcClientWebSocketServerRouterDelegate(CancellationToken ct, string ipcClient, string webSocket, int runtimeTimeoutS, string verbose);
 
+        private delegate Task<int> DiagnosticsServerIpcServerIOSSimulatorRouterDelegate(CancellationToken ct, int runtimeTimeoutS, string verbose);
+
+        private delegate Task<int> DiagnosticsServerIpcServerIOSRouterDelegate(CancellationToken ct, int runtimeTimeoutS, string verbose);
+
+        private delegate Task<int> DiagnosticsServerIpcServerAndroidEmulatorRouterDelegate(CancellationToken ct, int runtimeTimeoutS, string verbose);
+
+        private delegate Task<int> DiagnosticsServerIpcServerAndroidRouterDelegate(CancellationToken ct, int runtimeTimeoutS, string verbose);
+
         private static Command IpcClientTcpServerRouterCommand() =>
             new(
                 name: "client-server",
@@ -104,6 +112,58 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
                 IpcClientAddressOption(), TcpClientAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption()
             };
 
+        private static Command IOSSimulatorRouterCommand() =>
+            new(
+                name: "ios-sim",
+                description: "Start a .NET application Diagnostics Server routing local IPC server <--> iOS Simulator. " +
+                                "Router is configured using an IPC server (connecting to by diagnostic tools) " +
+                                "and a TCP/IP server (accepting runtime TCP client).")
+            {
+                // Handler
+                HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerIOSSimulatorRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerIOSSimulatorRouter).GetCommandHandler(),
+                // Options
+                RuntimeTimeoutOption(), VerboseOption()
+            };
+
+        private static Command IOSRouterCommand() =>
+            new(
+                name: "ios",
+                description: "Start a .NET application Diagnostics Server routing local IPC server <--> iOS Device over usbmux. " +
+                                "Router is configured using an IPC server (connecting to by diagnostic tools) " +
+                                "and a TCP/IP client (connecting runtime TCP server over usbmux).")
+            {
+                        // Handler
+                        HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerIOSRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerIOSRouter).GetCommandHandler(),
+                        // Options
+                        RuntimeTimeoutOption(), VerboseOption()
+            };
+
+        private static Command AndroidEmulatorRouterCommand() =>
+            new(
+                name: "android-emu",
+                description: "Start a .NET application Diagnostics Server routing local IPC server <--> Android Emulator. " +
+                                "Router is configured using an IPC server (connecting to by diagnostic tools) " +
+                                "and a TCP/IP server (accepting runtime TCP client).")
+            {
+                        // Handler
+                        HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerAndroidEmulatorRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerAndroidEmulatorRouter).GetCommandHandler(),
+                        // Options
+                        RuntimeTimeoutOption(), VerboseOption()
+            };
+
+        private static Command AndroidRouterCommand() =>
+            new(
+                name: "android",
+                description: "Start a .NET application Diagnostics Server routing local IPC server <--> Android Device. " +
+                                "Router is configured using an IPC server (connecting to by diagnostic tools) " +
+                                "and a TCP/IP server (accepting runtime TCP client).")
+            {
+                        // Handler
+                        HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerAndroidRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerAndroidRouter).GetCommandHandler(),
+                        // Options
+                        RuntimeTimeoutOption(), VerboseOption()
+            };
+
         private static Option IpcClientAddressOption() =>
             new(
                 aliases: new[] { "--ipc-client", "-ipcc" },
@@ -193,6 +253,10 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter
                 .AddCommand(IpcClientTcpClientRouterCommand())
                 .AddCommand(IpcServerWebSocketServerRouterCommand())
                 .AddCommand(IpcClientWebSocketServerRouterCommand())
+                .AddCommand(IOSSimulatorRouterCommand())
+                .AddCommand(IOSRouterCommand())
+                .AddCommand(AndroidEmulatorRouterCommand())
+                .AddCommand(AndroidRouterCommand())
                 .UseDefaults()
                 .Build();