From 80af309e7a9e61c2ed6043bbba5c98b55751c43b Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 13 Mar 2026 09:19:43 +0000 Subject: [PATCH 01/11] Add ADB reverse port forwarding support to AdbRunner Add ReversePortAsync, RemoveReversePortAsync, RemoveAllReversePortsAsync, and ListReversePortsAsync methods to AdbRunner for managing reverse port forwarding rules. These APIs enable the MAUI DevTools CLI to manage hot-reload tunnels without going through ServiceHub. New type AdbReversePortRule represents entries from 'adb reverse --list'. Internal ParseReverseListOutput handles parsing the output format. Includes 14 new tests covering parsing and parameter validation. Closes #303 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 10 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 10 ++ .../Runners/AdbReversePortRule.cs | 20 +++ .../Runners/AdbRunner.cs | 99 +++++++++++ .../AdbRunnerTests.cs | 165 ++++++++++++++++++ 5 files changed, 304 insertions(+) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt index e8f26266..1924a65c 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -177,3 +177,13 @@ Xamarin.Android.Tools.EmulatorRunner.BootEmulatorAsync(string! deviceOrAvdName, Xamarin.Android.Tools.EmulatorRunner.EmulatorRunner(string! emulatorPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void Xamarin.Android.Tools.EmulatorRunner.ListAvdNamesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Xamarin.Android.Tools.EmulatorRunner.LaunchEmulator(string! avdName, bool coldBoot = false, System.Collections.Generic.List? additionalArgs = null) -> System.Diagnostics.Process! +Xamarin.Android.Tools.AdbReversePortRule +Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule() -> void +Xamarin.Android.Tools.AdbReversePortRule.Local.get -> string! +Xamarin.Android.Tools.AdbReversePortRule.Local.init -> void +Xamarin.Android.Tools.AdbReversePortRule.Remote.get -> string! +Xamarin.Android.Tools.AdbReversePortRule.Remote.init -> void +virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, int remotePort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, int remotePort, int localPort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index e8f26266..1924a65c 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -177,3 +177,13 @@ Xamarin.Android.Tools.EmulatorRunner.BootEmulatorAsync(string! deviceOrAvdName, Xamarin.Android.Tools.EmulatorRunner.EmulatorRunner(string! emulatorPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void Xamarin.Android.Tools.EmulatorRunner.ListAvdNamesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Xamarin.Android.Tools.EmulatorRunner.LaunchEmulator(string! avdName, bool coldBoot = false, System.Collections.Generic.List? additionalArgs = null) -> System.Diagnostics.Process! +Xamarin.Android.Tools.AdbReversePortRule +Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule() -> void +Xamarin.Android.Tools.AdbReversePortRule.Local.get -> string! +Xamarin.Android.Tools.AdbReversePortRule.Local.init -> void +Xamarin.Android.Tools.AdbReversePortRule.Remote.get -> string! +Xamarin.Android.Tools.AdbReversePortRule.Remote.init -> void +virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, int remotePort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, int remotePort, int localPort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs new file mode 100644 index 00000000..d1503a00 --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Represents a single ADB reverse port forwarding rule as reported by 'adb reverse --list'. +/// +public class AdbReversePortRule +{ + /// + /// The remote (device-side) socket spec, e.g. "tcp:5000". + /// + public string Remote { get; init; } = string.Empty; + + /// + /// The local (host-side) socket spec, e.g. "tcp:5000". + /// + public string Local { get; init; } = string.Empty; +} diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index 9f91b2e3..a8012233 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -242,6 +242,105 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati return trimmed; } return null; + /// Sets up reverse port forwarding from the device to the host via + /// 'adb -s <serial> reverse tcp:<remotePort> tcp:<localPort>'. + /// This allows the device to connect to a service on the host machine through the specified port. + /// + public virtual async Task ReversePortAsync (string serial, int remotePort, int localPort, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + if (remotePort <= 0 || remotePort > 65535) + throw new ArgumentOutOfRangeException (nameof (remotePort), remotePort, "Port must be between 1 and 65535."); + if (localPort <= 0 || localPort > 65535) + throw new ArgumentOutOfRangeException (nameof (localPort), localPort, "Port must be between 1 and 65535."); + + var remoteSpec = $"tcp:{remotePort}"; + var localSpec = $"tcp:{localPort}"; + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", remoteSpec, localSpec); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse {remoteSpec} {localSpec}", stderr); + } + + /// + /// Removes a specific reverse port forwarding rule via + /// 'adb -s <serial> reverse --remove tcp:<remotePort>'. + /// + public virtual async Task RemoveReversePortAsync (string serial, int remotePort, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + if (remotePort <= 0 || remotePort > 65535) + throw new ArgumentOutOfRangeException (nameof (remotePort), remotePort, "Port must be between 1 and 65535."); + + var remoteSpec = $"tcp:{remotePort}"; + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove", remoteSpec); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --remove {remoteSpec}", stderr); + } + + /// + /// Removes all reverse port forwarding rules via + /// 'adb -s <serial> reverse --remove-all'. + /// + public virtual async Task RemoveAllReversePortsAsync (string serial, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove-all"); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --remove-all", stderr); + } + + /// + /// Lists all active reverse port forwarding rules via + /// 'adb -s <serial> reverse --list'. + /// + public virtual async Task> ListReversePortsAsync (string serial, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); + + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--list"); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --list", stderr); + + return ParseReverseListOutput (stdout.ToString ().Split ('\n')); + } + + /// + /// Parses the output of 'adb reverse --list'. + /// Each line is "(reverse) <remote> <local>", e.g. "(reverse) tcp:5000 tcp:5000". + /// + internal static IReadOnlyList ParseReverseListOutput (IEnumerable lines) + { + var rules = new List (); + + foreach (var line in lines) { + var trimmed = line.Trim (); + if (string.IsNullOrEmpty (trimmed)) + continue; + + // Expected format: "(reverse) tcp:5000 tcp:5000" + if (!trimmed.StartsWith ("(reverse)", StringComparison.Ordinal)) + continue; + + var parts = trimmed.Substring ("(reverse)".Length).Trim ().Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2) { + rules.Add (new AdbReversePortRule { + Remote = parts [0], + Local = parts [1], + }); + } + } + + return rules; } /// diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs index 49b56db6..ab9e6e1e 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -714,6 +714,171 @@ public void FirstNonEmptyLine_PmPathOutput () // adb shell pm path android returns "package:/system/framework/framework-res.apk\n" var output = "package:/system/framework/framework-res.apk\n"; Assert.AreEqual ("package:/system/framework/framework-res.apk", AdbRunner.FirstNonEmptyLine (output)); + // --- ParseReverseListOutput tests --- + // Consumer: MAUI DevTools (via ListReversePortsAsync), vscode-maui ServiceHub replacement + + [Test] + public void ParseReverseListOutput_SingleRule () + { + var output = new [] { + "(reverse) tcp:5000 tcp:5000", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (1, rules.Count); + Assert.AreEqual ("tcp:5000", rules [0].Remote); + Assert.AreEqual ("tcp:5000", rules [0].Local); + } + + [Test] + public void ParseReverseListOutput_MultipleRules () + { + var output = new [] { + "(reverse) tcp:5000 tcp:5000", + "(reverse) tcp:8081 tcp:8081", + "(reverse) tcp:19000 tcp:19001", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (3, rules.Count); + Assert.AreEqual ("tcp:5000", rules [0].Remote); + Assert.AreEqual ("tcp:5000", rules [0].Local); + Assert.AreEqual ("tcp:8081", rules [1].Remote); + Assert.AreEqual ("tcp:8081", rules [1].Local); + Assert.AreEqual ("tcp:19000", rules [2].Remote); + Assert.AreEqual ("tcp:19001", rules [2].Local); + } + + [Test] + public void ParseReverseListOutput_EmptyOutput () + { + var output = new [] { "", " " }; + var rules = AdbRunner.ParseReverseListOutput (output); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseReverseListOutput_NoLines () + { + var rules = AdbRunner.ParseReverseListOutput (Array.Empty ()); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseReverseListOutput_IgnoresNonReverseLines () + { + var output = new [] { + "some random header", + "(reverse) tcp:5000 tcp:5000", + "* daemon started successfully", + "(reverse) tcp:8081 tcp:8081", + "", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual ("tcp:5000", rules [0].Remote); + Assert.AreEqual ("tcp:8081", rules [1].Remote); + } + + [Test] + public void ParseReverseListOutput_MalformedLine_InsufficientParts () + { + var output = new [] { + "(reverse) tcp:5000", // missing local spec + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + Assert.AreEqual (0, rules.Count); + } + + [Test] + public void ParseReverseListOutput_DifferentRemoteAndLocalPorts () + { + var output = new [] { + "(reverse) tcp:8080 tcp:3000", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (1, rules.Count); + Assert.AreEqual ("tcp:8080", rules [0].Remote); + Assert.AreEqual ("tcp:3000", rules [0].Local); + } + + // --- ReversePortAsync parameter validation tests --- + + [Test] + public void ReversePortAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("", 5000, 5000)); + } + + [Test] + public void ReversePortAsync_ZeroRemotePort_ThrowsArgumentOutOfRange () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", 0, 5000)); + } + + [Test] + public void ReversePortAsync_NegativeLocalPort_ThrowsArgumentOutOfRange () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", 5000, -1)); + } + + [Test] + public void ReversePortAsync_PortAbove65535_ThrowsArgumentOutOfRange () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", 70000, 5000)); + } + + // --- RemoveReversePortAsync parameter validation tests --- + + [Test] + public void RemoveReversePortAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveReversePortAsync ("", 5000)); + } + + [Test] + public void RemoveReversePortAsync_ZeroPort_ThrowsArgumentOutOfRange () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveReversePortAsync ("emulator-5554", 0)); + } + + // --- RemoveAllReversePortsAsync parameter validation tests --- + + [Test] + public void RemoveAllReversePortsAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveAllReversePortsAsync ("")); + } + + // --- ListReversePortsAsync parameter validation tests --- + + [Test] + public void ListReversePortsAsync_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ListReversePortsAsync ("")); } // --- GetEmulatorAvdNameAsync + ListDevicesAsync tests --- From 7ed82e0f971a63cb1f9b86ef2bf5a59975d52ab9 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Mon, 16 Mar 2026 14:32:42 +0000 Subject: [PATCH 02/11] Improve ADB reverse port API: record type, string overloads, more tests Based on multi-model code review (GPT-5.1 + Gemini-3-Pro): - Convert AdbReversePortRule from class to positional record for value equality and concise construction - Add string socket-spec overloads for ReversePortAsync and RemoveReversePortAsync to support non-TCP protocols (e.g., localabstract:, localfilesystem:) matching existing patterns in ClientTools.Platform and vscode-maui - Extract ValidatePort helper for consistent validation - Int overloads now delegate to string overloads as convenience wrappers - Add 8 new tests: NonTcpSpecs, WindowsLineEndings, ValueEquality, Deconstruct, ToString, and string overload validation tests (total: 25 reverse-port tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 4 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 4 +- .../Runners/AdbReversePortRule.cs | 16 +-- .../Runners/AdbRunner.cs | 82 +++++++++---- .../AdbRunnerTests.cs | 112 ++++++++++++++++++ 5 files changed, 181 insertions(+), 37 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 1924a65c..b758015e 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -178,7 +178,7 @@ Xamarin.Android.Tools.EmulatorRunner.EmulatorRunner(string! emulatorPath, System Xamarin.Android.Tools.EmulatorRunner.ListAvdNamesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Xamarin.Android.Tools.EmulatorRunner.LaunchEmulator(string! avdName, bool coldBoot = false, System.Collections.Generic.List? additionalArgs = null) -> System.Diagnostics.Process! Xamarin.Android.Tools.AdbReversePortRule -Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule() -> void +Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule(string! Remote, string! Local) -> void Xamarin.Android.Tools.AdbReversePortRule.Local.get -> string! Xamarin.Android.Tools.AdbReversePortRule.Local.init -> void Xamarin.Android.Tools.AdbReversePortRule.Remote.get -> string! @@ -186,4 +186,6 @@ Xamarin.Android.Tools.AdbReversePortRule.Remote.init -> void virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, int remotePort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, string! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, int remotePort, int localPort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, string! remote, string! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 1924a65c..b758015e 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -178,7 +178,7 @@ Xamarin.Android.Tools.EmulatorRunner.EmulatorRunner(string! emulatorPath, System Xamarin.Android.Tools.EmulatorRunner.ListAvdNamesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Xamarin.Android.Tools.EmulatorRunner.LaunchEmulator(string! avdName, bool coldBoot = false, System.Collections.Generic.List? additionalArgs = null) -> System.Diagnostics.Process! Xamarin.Android.Tools.AdbReversePortRule -Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule() -> void +Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule(string! Remote, string! Local) -> void Xamarin.Android.Tools.AdbReversePortRule.Local.get -> string! Xamarin.Android.Tools.AdbReversePortRule.Local.init -> void Xamarin.Android.Tools.AdbReversePortRule.Remote.get -> string! @@ -186,4 +186,6 @@ Xamarin.Android.Tools.AdbReversePortRule.Remote.init -> void virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, int remotePort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, string! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, int remotePort, int localPort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, string! remote, string! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs index d1503a00..cff0719a 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs @@ -5,16 +5,8 @@ namespace Xamarin.Android.Tools; /// /// Represents a single ADB reverse port forwarding rule as reported by 'adb reverse --list'. +/// Uses positional record for value equality and built-in ToString(). /// -public class AdbReversePortRule -{ - /// - /// The remote (device-side) socket spec, e.g. "tcp:5000". - /// - public string Remote { get; init; } = string.Empty; - - /// - /// The local (host-side) socket spec, e.g. "tcp:5000". - /// - public string Local { get; init; } = string.Empty; -} +/// The remote (device-side) socket spec, e.g. "tcp:5000". +/// The local (host-side) socket spec, e.g. "tcp:5000". +public record AdbReversePortRule (string Remote, string Local); diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index a8012233..000d2672 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -242,43 +242,73 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati return trimmed; } return null; + } + + /// /// Sets up reverse port forwarding from the device to the host via - /// 'adb -s <serial> reverse tcp:<remotePort> tcp:<localPort>'. - /// This allows the device to connect to a service on the host machine through the specified port. + /// 'adb -s <serial> reverse <remote> <local>'. + /// Supports any socket spec accepted by adb (tcp:PORT, localabstract:NAME, etc.). + /// This is the core overload; the (int, int) convenience overload delegates here. /// - public virtual async Task ReversePortAsync (string serial, int remotePort, int localPort, CancellationToken cancellationToken = default) + /// Device serial number. + /// Remote (device-side) socket spec, e.g. "tcp:5000" or "localabstract:foo". + /// Local (host-side) socket spec, e.g. "tcp:5000". + /// Cancellation token. + public virtual async Task ReversePortAsync (string serial, string remote, string local, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace (serial)) throw new ArgumentException ("Serial must not be empty.", nameof (serial)); - if (remotePort <= 0 || remotePort > 65535) - throw new ArgumentOutOfRangeException (nameof (remotePort), remotePort, "Port must be between 1 and 65535."); - if (localPort <= 0 || localPort > 65535) - throw new ArgumentOutOfRangeException (nameof (localPort), localPort, "Port must be between 1 and 65535."); - - var remoteSpec = $"tcp:{remotePort}"; - var localSpec = $"tcp:{localPort}"; - var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", remoteSpec, localSpec); + if (string.IsNullOrWhiteSpace (remote)) + throw new ArgumentException ("Remote socket spec must not be empty.", nameof (remote)); + if (string.IsNullOrWhiteSpace (local)) + throw new ArgumentException ("Local socket spec must not be empty.", nameof (local)); + + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", remote, local); using var stderr = new StringWriter (); var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); - ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse {remoteSpec} {localSpec}", stderr); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse {remote} {local}", stderr); + } + + /// + /// TCP convenience overload: sets up reverse port forwarding via + /// 'adb -s <serial> reverse tcp:<remotePort> tcp:<localPort>'. + /// + public virtual Task ReversePortAsync (string serial, int remotePort, int localPort, CancellationToken cancellationToken = default) + { + ValidatePort (remotePort, nameof (remotePort)); + ValidatePort (localPort, nameof (localPort)); + return ReversePortAsync (serial, $"tcp:{remotePort}", $"tcp:{localPort}", cancellationToken); } /// /// Removes a specific reverse port forwarding rule via - /// 'adb -s <serial> reverse --remove tcp:<remotePort>'. + /// 'adb -s <serial> reverse --remove <remote>'. + /// Supports any socket spec accepted by adb. /// - public virtual async Task RemoveReversePortAsync (string serial, int remotePort, CancellationToken cancellationToken = default) + /// Device serial number. + /// Remote (device-side) socket spec to remove, e.g. "tcp:5000". + /// Cancellation token. + public virtual async Task RemoveReversePortAsync (string serial, string remote, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace (serial)) throw new ArgumentException ("Serial must not be empty.", nameof (serial)); - if (remotePort <= 0 || remotePort > 65535) - throw new ArgumentOutOfRangeException (nameof (remotePort), remotePort, "Port must be between 1 and 65535."); + if (string.IsNullOrWhiteSpace (remote)) + throw new ArgumentException ("Remote socket spec must not be empty.", nameof (remote)); - var remoteSpec = $"tcp:{remotePort}"; - var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove", remoteSpec); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove", remote); using var stderr = new StringWriter (); var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); - ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --remove {remoteSpec}", stderr); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --remove {remote}", stderr); + } + + /// + /// TCP convenience overload: removes a specific reverse port forwarding rule via + /// 'adb -s <serial> reverse --remove tcp:<remotePort>'. + /// + public virtual Task RemoveReversePortAsync (string serial, int remotePort, CancellationToken cancellationToken = default) + { + ValidatePort (remotePort, nameof (remotePort)); + return RemoveReversePortAsync (serial, $"tcp:{remotePort}", cancellationToken); } /// @@ -333,16 +363,22 @@ internal static IReadOnlyList ParseReverseListOutput (IEnume var parts = trimmed.Substring ("(reverse)".Length).Trim ().Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) { - rules.Add (new AdbReversePortRule { - Remote = parts [0], - Local = parts [1], - }); + rules.Add (new AdbReversePortRule ( + Remote: parts [0], + Local: parts [1] + )); } } return rules; } + static void ValidatePort (int port, string paramName) + { + if (port <= 0 || port > 65535) + throw new ArgumentOutOfRangeException (paramName, port, "Port must be between 1 and 65535."); + } + /// /// Parses the output lines from 'adb devices -l'. /// Accepts an to avoid allocating a joined string. diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs index ab9e6e1e..2b2f54fd 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -714,6 +714,8 @@ public void FirstNonEmptyLine_PmPathOutput () // adb shell pm path android returns "package:/system/framework/framework-res.apk\n" var output = "package:/system/framework/framework-res.apk\n"; Assert.AreEqual ("package:/system/framework/framework-res.apk", AdbRunner.FirstNonEmptyLine (output)); + } + // --- ParseReverseListOutput tests --- // Consumer: MAUI DevTools (via ListReversePortsAsync), vscode-maui ServiceHub replacement @@ -809,6 +811,72 @@ public void ParseReverseListOutput_DifferentRemoteAndLocalPorts () Assert.AreEqual ("tcp:3000", rules [0].Local); } + [Test] + public void ParseReverseListOutput_NonTcpSpecs () + { + var output = new [] { + "(reverse) localabstract:chrome_devtools_remote tcp:9222", + "(reverse) tcp:5000 tcp:5000", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual ("localabstract:chrome_devtools_remote", rules [0].Remote); + Assert.AreEqual ("tcp:9222", rules [0].Local); + Assert.AreEqual ("tcp:5000", rules [1].Remote); + Assert.AreEqual ("tcp:5000", rules [1].Local); + } + + [Test] + public void ParseReverseListOutput_WindowsLineEndings () + { + // Simulate \r\n line endings (split on \n leaves trailing \r) + var output = new [] { + "(reverse) tcp:5000 tcp:5000\r", + "(reverse) tcp:8081 tcp:8081\r", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual ("tcp:5000", rules [0].Remote); + Assert.AreEqual ("tcp:8081", rules [1].Remote); + } + + [Test] + public void AdbReversePortRule_ValueEquality () + { + var rule1 = new AdbReversePortRule ("tcp:5000", "tcp:5000"); + var rule2 = new AdbReversePortRule ("tcp:5000", "tcp:5000"); + var rule3 = new AdbReversePortRule ("tcp:5000", "tcp:3000"); + + Assert.AreEqual (rule1, rule2); + Assert.AreNotEqual (rule1, rule3); + Assert.IsTrue (rule1 == rule2); + Assert.IsFalse (rule1 == rule3); + } + + [Test] + public void AdbReversePortRule_Deconstruct () + { + var rule = new AdbReversePortRule ("tcp:5000", "tcp:3000"); + var (remote, local) = rule; + + Assert.AreEqual ("tcp:5000", remote); + Assert.AreEqual ("tcp:3000", local); + } + + [Test] + public void AdbReversePortRule_ToString () + { + var rule = new AdbReversePortRule ("tcp:5000", "tcp:3000"); + var str = rule.ToString (); + + Assert.That (str, Does.Contain ("tcp:5000")); + Assert.That (str, Does.Contain ("tcp:3000")); + } + // --- ReversePortAsync parameter validation tests --- [Test] @@ -843,6 +911,32 @@ public void ReversePortAsync_PortAbove65535_ThrowsArgumentOutOfRange () async () => await runner.ReversePortAsync ("emulator-5554", 70000, 5000)); } + // --- ReversePortAsync string overload validation tests --- + + [Test] + public void ReversePortAsync_String_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("", "tcp:5000", "tcp:5000")); + } + + [Test] + public void ReversePortAsync_String_EmptyRemote_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", "", "tcp:5000")); + } + + [Test] + public void ReversePortAsync_String_EmptyLocal_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", "tcp:5000", "")); + } + // --- RemoveReversePortAsync parameter validation tests --- [Test] @@ -861,6 +955,24 @@ public void RemoveReversePortAsync_ZeroPort_ThrowsArgumentOutOfRange () async () => await runner.RemoveReversePortAsync ("emulator-5554", 0)); } + // --- RemoveReversePortAsync string overload validation tests --- + + [Test] + public void RemoveReversePortAsync_String_EmptySerial_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveReversePortAsync ("", "tcp:5000")); + } + + [Test] + public void RemoveReversePortAsync_String_EmptyRemote_ThrowsArgumentException () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveReversePortAsync ("emulator-5554", "")); + } + // --- RemoveAllReversePortsAsync parameter validation tests --- [Test] From db1fe6b45237e27fa64bc73c10dc362c5347e3e7 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 15:04:34 +0000 Subject: [PATCH 03/11] Refactor ADB port forwarding API: strongly typed AdbPortSpec + AdbPortRule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AdbProtocol enum (Tcp, LocalAbstract, LocalReserved, LocalFilesystem) - Add AdbPortSpec record with TryParse/ToSocketSpec for typed port+protocol - Rename AdbReversePortRule → AdbPortRule using AdbPortSpec (not raw strings) - Add AdbPortSpec overloads for ReversePortAsync and RemoveReversePortAsync - Int convenience overloads delegate through AdbPortSpec - ParseReverseListOutput returns typed AdbPortRule via AdbPortSpec.TryParse - Update PublicAPI surface for both TFMs - Add 18 new tests for AdbPortSpec parsing, serialization, and validation Addresses @jonathanpeppers review feedback on PR #305. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 30 ++- .../netstandard2.0/PublicAPI.Unshipped.txt | 30 ++- .../Runners/AdbPortRule.cs | 60 +++++ .../Runners/AdbProtocol.cs | 15 ++ .../Runners/AdbReversePortRule.cs | 12 - .../Runners/AdbRunner.cs | 47 +++- .../AdbRunnerTests.cs | 205 +++++++++++++++--- 7 files changed, 333 insertions(+), 66 deletions(-) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs delete mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt index b758015e..885ca596 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -177,15 +177,31 @@ Xamarin.Android.Tools.EmulatorRunner.BootEmulatorAsync(string! deviceOrAvdName, Xamarin.Android.Tools.EmulatorRunner.EmulatorRunner(string! emulatorPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void Xamarin.Android.Tools.EmulatorRunner.ListAvdNamesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Xamarin.Android.Tools.EmulatorRunner.LaunchEmulator(string! avdName, bool coldBoot = false, System.Collections.Generic.List? additionalArgs = null) -> System.Diagnostics.Process! -Xamarin.Android.Tools.AdbReversePortRule -Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule(string! Remote, string! Local) -> void -Xamarin.Android.Tools.AdbReversePortRule.Local.get -> string! -Xamarin.Android.Tools.AdbReversePortRule.Local.init -> void -Xamarin.Android.Tools.AdbReversePortRule.Remote.get -> string! -Xamarin.Android.Tools.AdbReversePortRule.Remote.init -> void -virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.AdbPortRule +Xamarin.Android.Tools.AdbPortRule.AdbPortRule(Xamarin.Android.Tools.AdbPortSpec! Remote, Xamarin.Android.Tools.AdbPortSpec! Local) -> void +Xamarin.Android.Tools.AdbPortRule.Local.get -> Xamarin.Android.Tools.AdbPortSpec! +Xamarin.Android.Tools.AdbPortRule.Local.init -> void +Xamarin.Android.Tools.AdbPortRule.Remote.get -> Xamarin.Android.Tools.AdbPortSpec! +Xamarin.Android.Tools.AdbPortRule.Remote.init -> void +Xamarin.Android.Tools.AdbPortSpec +Xamarin.Android.Tools.AdbPortSpec.AdbPortSpec(Xamarin.Android.Tools.AdbProtocol Protocol, int Port) -> void +Xamarin.Android.Tools.AdbPortSpec.Port.get -> int +Xamarin.Android.Tools.AdbPortSpec.Port.init -> void +Xamarin.Android.Tools.AdbPortSpec.Protocol.get -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbPortSpec.Protocol.init -> void +Xamarin.Android.Tools.AdbPortSpec.ToSocketSpec() -> string! +Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbProtocol.LocalAbstract = 1 -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbProtocol.LocalFilesystem = 3 -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbProtocol.LocalReserved = 2 -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbProtocol.Tcp = 0 -> Xamarin.Android.Tools.AdbProtocol +override Xamarin.Android.Tools.AdbPortSpec.ToString() -> string! +static Xamarin.Android.Tools.AdbPortSpec.TryParse(string! socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? +virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, int remotePort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, string! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, int remotePort, int localPort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, string! remote, string! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index b758015e..885ca596 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -177,15 +177,31 @@ Xamarin.Android.Tools.EmulatorRunner.BootEmulatorAsync(string! deviceOrAvdName, Xamarin.Android.Tools.EmulatorRunner.EmulatorRunner(string! emulatorPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void Xamarin.Android.Tools.EmulatorRunner.ListAvdNamesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! Xamarin.Android.Tools.EmulatorRunner.LaunchEmulator(string! avdName, bool coldBoot = false, System.Collections.Generic.List? additionalArgs = null) -> System.Diagnostics.Process! -Xamarin.Android.Tools.AdbReversePortRule -Xamarin.Android.Tools.AdbReversePortRule.AdbReversePortRule(string! Remote, string! Local) -> void -Xamarin.Android.Tools.AdbReversePortRule.Local.get -> string! -Xamarin.Android.Tools.AdbReversePortRule.Local.init -> void -Xamarin.Android.Tools.AdbReversePortRule.Remote.get -> string! -Xamarin.Android.Tools.AdbReversePortRule.Remote.init -> void -virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.AdbPortRule +Xamarin.Android.Tools.AdbPortRule.AdbPortRule(Xamarin.Android.Tools.AdbPortSpec! Remote, Xamarin.Android.Tools.AdbPortSpec! Local) -> void +Xamarin.Android.Tools.AdbPortRule.Local.get -> Xamarin.Android.Tools.AdbPortSpec! +Xamarin.Android.Tools.AdbPortRule.Local.init -> void +Xamarin.Android.Tools.AdbPortRule.Remote.get -> Xamarin.Android.Tools.AdbPortSpec! +Xamarin.Android.Tools.AdbPortRule.Remote.init -> void +Xamarin.Android.Tools.AdbPortSpec +Xamarin.Android.Tools.AdbPortSpec.AdbPortSpec(Xamarin.Android.Tools.AdbProtocol Protocol, int Port) -> void +Xamarin.Android.Tools.AdbPortSpec.Port.get -> int +Xamarin.Android.Tools.AdbPortSpec.Port.init -> void +Xamarin.Android.Tools.AdbPortSpec.Protocol.get -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbPortSpec.Protocol.init -> void +Xamarin.Android.Tools.AdbPortSpec.ToSocketSpec() -> string! +Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbProtocol.LocalAbstract = 1 -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbProtocol.LocalFilesystem = 3 -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbProtocol.LocalReserved = 2 -> Xamarin.Android.Tools.AdbProtocol +Xamarin.Android.Tools.AdbProtocol.Tcp = 0 -> Xamarin.Android.Tools.AdbProtocol +override Xamarin.Android.Tools.AdbPortSpec.ToString() -> string! +static Xamarin.Android.Tools.AdbPortSpec.TryParse(string! socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? +virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, int remotePort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, string! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, int remotePort, int localPort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, string! remote, string! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs new file mode 100644 index 00000000..a684e97a --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Xamarin.Android.Tools; + +/// +/// Represents a port and protocol pair for adb forwarding/reverse operations. +/// +public record AdbPortSpec (AdbProtocol Protocol, int Port) +{ + /// + /// Returns the adb socket spec string, e.g. "tcp:5000". + /// + public string ToSocketSpec () => Protocol switch { + AdbProtocol.Tcp => $"tcp:{Port}", + AdbProtocol.LocalAbstract => $"localabstract:{Port}", + AdbProtocol.LocalReserved => $"localreserved:{Port}", + AdbProtocol.LocalFilesystem => $"localfilesystem:{Port}", + _ => $"tcp:{Port}", + }; + + /// + /// Parses an adb socket spec string like "tcp:5000" into an . + /// Returns null if the format is unrecognized. + /// + public static AdbPortSpec? TryParse (string socketSpec) + { + if (string.IsNullOrWhiteSpace (socketSpec)) + return null; + + var colonIndex = socketSpec.IndexOf (':'); + if (colonIndex <= 0 || colonIndex >= socketSpec.Length - 1) + return null; + + var protocolStr = socketSpec.Substring (0, colonIndex); + var portStr = socketSpec.Substring (colonIndex + 1); + + if (!int.TryParse (portStr, out var port) || port <= 0 || port > 65535) + return null; + + var protocol = protocolStr.ToLowerInvariant () switch { + "tcp" => (AdbProtocol?) AdbProtocol.Tcp, + "localabstract" => AdbProtocol.LocalAbstract, + "localreserved" => AdbProtocol.LocalReserved, + "localfilesystem" => AdbProtocol.LocalFilesystem, + _ => null, + }; + + return protocol.HasValue ? new AdbPortSpec (protocol.Value, port) : null; + } + + public override string ToString () => ToSocketSpec (); +} + +/// +/// Represents an adb port forwarding rule as reported by 'adb reverse --list' or 'adb forward --list'. +/// +public record AdbPortRule (AdbPortSpec Remote, AdbPortSpec Local); diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs new file mode 100644 index 00000000..c1044b8e --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools; + +/// +/// Protocol types supported by adb port forwarding and reverse port forwarding. +/// +public enum AdbProtocol +{ + Tcp, + LocalAbstract, + LocalReserved, + LocalFilesystem, +} diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs deleted file mode 100644 index cff0719a..00000000 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Xamarin.Android.Tools; - -/// -/// Represents a single ADB reverse port forwarding rule as reported by 'adb reverse --list'. -/// Uses positional record for value equality and built-in ToString(). -/// -/// The remote (device-side) socket spec, e.g. "tcp:5000". -/// The local (host-side) socket spec, e.g. "tcp:5000". -public record AdbReversePortRule (string Remote, string Local); diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index 000d2672..db3be2a6 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -244,11 +244,12 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati return null; } +#pragma warning disable RS0026, RS0027 // Multiple overloads with optional parameters are intentional for API convenience /// /// Sets up reverse port forwarding from the device to the host via /// 'adb -s <serial> reverse <remote> <local>'. /// Supports any socket spec accepted by adb (tcp:PORT, localabstract:NAME, etc.). - /// This is the core overload; the (int, int) convenience overload delegates here. + /// This is the core overload; the typed and int convenience overloads delegate here. /// /// Device serial number. /// Remote (device-side) socket spec, e.g. "tcp:5000" or "localabstract:foo". @@ -269,6 +270,18 @@ public virtual async Task ReversePortAsync (string serial, string remote, string ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse {remote} {local}", stderr); } + /// + /// Typed overload: sets up reverse port forwarding using values. + /// + public virtual Task ReversePortAsync (string serial, AdbPortSpec remote, AdbPortSpec local, CancellationToken cancellationToken = default) + { + if (remote is null) + throw new ArgumentNullException (nameof (remote)); + if (local is null) + throw new ArgumentNullException (nameof (local)); + return ReversePortAsync (serial, remote.ToSocketSpec (), local.ToSocketSpec (), cancellationToken); + } + /// /// TCP convenience overload: sets up reverse port forwarding via /// 'adb -s <serial> reverse tcp:<remotePort> tcp:<localPort>'. @@ -277,9 +290,11 @@ public virtual Task ReversePortAsync (string serial, int remotePort, int localPo { ValidatePort (remotePort, nameof (remotePort)); ValidatePort (localPort, nameof (localPort)); - return ReversePortAsync (serial, $"tcp:{remotePort}", $"tcp:{localPort}", cancellationToken); + return ReversePortAsync (serial, new AdbPortSpec (AdbProtocol.Tcp, remotePort), new AdbPortSpec (AdbProtocol.Tcp, localPort), cancellationToken); } +#pragma warning restore RS0026, RS0027 +#pragma warning disable RS0026, RS0027 // Multiple overloads with optional parameters are intentional for API convenience /// /// Removes a specific reverse port forwarding rule via /// 'adb -s <serial> reverse --remove <remote>'. @@ -301,6 +316,16 @@ public virtual async Task RemoveReversePortAsync (string serial, string remote, ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --remove {remote}", stderr); } + /// + /// Typed overload: removes a specific reverse port forwarding rule using . + /// + public virtual Task RemoveReversePortAsync (string serial, AdbPortSpec remote, CancellationToken cancellationToken = default) + { + if (remote is null) + throw new ArgumentNullException (nameof (remote)); + return RemoveReversePortAsync (serial, remote.ToSocketSpec (), cancellationToken); + } + /// /// TCP convenience overload: removes a specific reverse port forwarding rule via /// 'adb -s <serial> reverse --remove tcp:<remotePort>'. @@ -308,8 +333,9 @@ public virtual async Task RemoveReversePortAsync (string serial, string remote, public virtual Task RemoveReversePortAsync (string serial, int remotePort, CancellationToken cancellationToken = default) { ValidatePort (remotePort, nameof (remotePort)); - return RemoveReversePortAsync (serial, $"tcp:{remotePort}", cancellationToken); + return RemoveReversePortAsync (serial, new AdbPortSpec (AdbProtocol.Tcp, remotePort), cancellationToken); } +#pragma warning restore RS0026, RS0027 /// /// Removes all reverse port forwarding rules via @@ -330,7 +356,7 @@ public virtual async Task RemoveAllReversePortsAsync (string serial, Cancellatio /// Lists all active reverse port forwarding rules via /// 'adb -s <serial> reverse --list'. /// - public virtual async Task> ListReversePortsAsync (string serial, CancellationToken cancellationToken = default) + public virtual async Task> ListReversePortsAsync (string serial, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace (serial)) throw new ArgumentException ("Serial must not be empty.", nameof (serial)); @@ -347,10 +373,11 @@ public virtual async Task> ListReversePortsAsy /// /// Parses the output of 'adb reverse --list'. /// Each line is "(reverse) <remote> <local>", e.g. "(reverse) tcp:5000 tcp:5000". + /// Lines with unparseable socket specs are skipped. /// - internal static IReadOnlyList ParseReverseListOutput (IEnumerable lines) + internal static IReadOnlyList ParseReverseListOutput (IEnumerable lines) { - var rules = new List (); + var rules = new List (); foreach (var line in lines) { var trimmed = line.Trim (); @@ -363,10 +390,10 @@ internal static IReadOnlyList ParseReverseListOutput (IEnume var parts = trimmed.Substring ("(reverse)".Length).Trim ().Split (new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2) { - rules.Add (new AdbReversePortRule ( - Remote: parts [0], - Local: parts [1] - )); + var remote = AdbPortSpec.TryParse (parts [0]); + var local = AdbPortSpec.TryParse (parts [1]); + if (remote is { } r && local is { } l) + rules.Add (new AdbPortRule (r, l)); } } diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs index 2b2f54fd..fbc2b8c7 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -729,8 +729,10 @@ public void ParseReverseListOutput_SingleRule () var rules = AdbRunner.ParseReverseListOutput (output); Assert.AreEqual (1, rules.Count); - Assert.AreEqual ("tcp:5000", rules [0].Remote); - Assert.AreEqual ("tcp:5000", rules [0].Local); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Local.Protocol); + Assert.AreEqual (5000, rules [0].Local.Port); } [Test] @@ -745,12 +747,13 @@ public void ParseReverseListOutput_MultipleRules () var rules = AdbRunner.ParseReverseListOutput (output); Assert.AreEqual (3, rules.Count); - Assert.AreEqual ("tcp:5000", rules [0].Remote); - Assert.AreEqual ("tcp:5000", rules [0].Local); - Assert.AreEqual ("tcp:8081", rules [1].Remote); - Assert.AreEqual ("tcp:8081", rules [1].Local); - Assert.AreEqual ("tcp:19000", rules [2].Remote); - Assert.AreEqual ("tcp:19001", rules [2].Local); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (5000, rules [0].Local.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); + Assert.AreEqual (8081, rules [1].Local.Port); + Assert.AreEqual (19000, rules [2].Remote.Port); + Assert.AreEqual (19001, rules [2].Local.Port); } [Test] @@ -782,8 +785,8 @@ public void ParseReverseListOutput_IgnoresNonReverseLines () var rules = AdbRunner.ParseReverseListOutput (output); Assert.AreEqual (2, rules.Count); - Assert.AreEqual ("tcp:5000", rules [0].Remote); - Assert.AreEqual ("tcp:8081", rules [1].Remote); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); } [Test] @@ -807,12 +810,14 @@ public void ParseReverseListOutput_DifferentRemoteAndLocalPorts () var rules = AdbRunner.ParseReverseListOutput (output); Assert.AreEqual (1, rules.Count); - Assert.AreEqual ("tcp:8080", rules [0].Remote); - Assert.AreEqual ("tcp:3000", rules [0].Local); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (8080, rules [0].Remote.Port); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Local.Protocol); + Assert.AreEqual (3000, rules [0].Local.Port); } [Test] - public void ParseReverseListOutput_NonTcpSpecs () + public void ParseReverseListOutput_NonTcpSpecs_SkipsUnparseable () { var output = new [] { "(reverse) localabstract:chrome_devtools_remote tcp:9222", @@ -821,11 +826,10 @@ public void ParseReverseListOutput_NonTcpSpecs () var rules = AdbRunner.ParseReverseListOutput (output); - Assert.AreEqual (2, rules.Count); - Assert.AreEqual ("localabstract:chrome_devtools_remote", rules [0].Remote); - Assert.AreEqual ("tcp:9222", rules [0].Local); - Assert.AreEqual ("tcp:5000", rules [1].Remote); - Assert.AreEqual ("tcp:5000", rules [1].Local); + // localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped + Assert.AreEqual (1, rules.Count); + Assert.AreEqual (AdbProtocol.Tcp, rules [0].Remote.Protocol); + Assert.AreEqual (5000, rules [0].Remote.Port); } [Test] @@ -840,16 +844,127 @@ public void ParseReverseListOutput_WindowsLineEndings () var rules = AdbRunner.ParseReverseListOutput (output); Assert.AreEqual (2, rules.Count); - Assert.AreEqual ("tcp:5000", rules [0].Remote); - Assert.AreEqual ("tcp:8081", rules [1].Remote); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); } + // --- AdbPortSpec tests --- + [Test] - public void AdbReversePortRule_ValueEquality () + public void AdbPortSpec_TryParse_ValidTcp () { - var rule1 = new AdbReversePortRule ("tcp:5000", "tcp:5000"); - var rule2 = new AdbReversePortRule ("tcp:5000", "tcp:5000"); - var rule3 = new AdbReversePortRule ("tcp:5000", "tcp:3000"); + var spec = AdbPortSpec.TryParse ("tcp:5000"); + Assert.IsNotNull (spec); + Assert.AreEqual (AdbProtocol.Tcp, spec!.Protocol); + Assert.AreEqual (5000, spec.Port); + } + + [Test] + public void AdbPortSpec_TryParse_ValidLocalAbstract () + { + var spec = AdbPortSpec.TryParse ("localabstract:9222"); + Assert.IsNotNull (spec); + Assert.AreEqual (AdbProtocol.LocalAbstract, spec!.Protocol); + Assert.AreEqual (9222, spec.Port); + } + + [Test] + public void AdbPortSpec_TryParse_ValidLocalReserved () + { + var spec = AdbPortSpec.TryParse ("localreserved:1234"); + Assert.IsNotNull (spec); + Assert.AreEqual (AdbProtocol.LocalReserved, spec!.Protocol); + Assert.AreEqual (1234, spec.Port); + } + + [Test] + public void AdbPortSpec_TryParse_ValidLocalFilesystem () + { + var spec = AdbPortSpec.TryParse ("localfilesystem:8080"); + Assert.IsNotNull (spec); + Assert.AreEqual (AdbProtocol.LocalFilesystem, spec!.Protocol); + Assert.AreEqual (8080, spec.Port); + } + + [Test] + public void AdbPortSpec_TryParse_Null_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse (null!)); + } + + [Test] + public void AdbPortSpec_TryParse_Empty_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("")); + } + + [Test] + public void AdbPortSpec_TryParse_NoColon_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("tcp5000")); + } + + [Test] + public void AdbPortSpec_TryParse_NonNumericPort_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("localabstract:chrome_devtools_remote")); + } + + [Test] + public void AdbPortSpec_TryParse_ZeroPort_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("tcp:0")); + } + + [Test] + public void AdbPortSpec_TryParse_PortAbove65535_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("tcp:70000")); + } + + [Test] + public void AdbPortSpec_TryParse_UnknownProtocol_ReturnsNull () + { + Assert.IsNull (AdbPortSpec.TryParse ("udp:5000")); + } + + [Test] + public void AdbPortSpec_ToSocketSpec_Tcp () + { + var spec = new AdbPortSpec (AdbProtocol.Tcp, 5000); + Assert.AreEqual ("tcp:5000", spec.ToSocketSpec ()); + } + + [Test] + public void AdbPortSpec_ToSocketSpec_LocalAbstract () + { + var spec = new AdbPortSpec (AdbProtocol.LocalAbstract, 9222); + Assert.AreEqual ("localabstract:9222", spec.ToSocketSpec ()); + } + + [Test] + public void AdbPortSpec_ToString_MatchesSocketSpec () + { + var spec = new AdbPortSpec (AdbProtocol.Tcp, 8080); + Assert.AreEqual ("tcp:8080", spec.ToString ()); + } + + [Test] + public void AdbPortSpec_TryParse_Roundtrip () + { + var original = new AdbPortSpec (AdbProtocol.Tcp, 3000); + var parsed = AdbPortSpec.TryParse (original.ToSocketSpec ()); + Assert.AreEqual (original, parsed); + } + + // --- AdbPortRule tests --- + + [Test] + public void AdbPortRule_ValueEquality () + { + var rule1 = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 5000)); + var rule2 = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 5000)); + var rule3 = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 3000)); Assert.AreEqual (rule1, rule2); Assert.AreNotEqual (rule1, rule3); @@ -858,19 +973,21 @@ public void AdbReversePortRule_ValueEquality () } [Test] - public void AdbReversePortRule_Deconstruct () + public void AdbPortRule_Deconstruct () { - var rule = new AdbReversePortRule ("tcp:5000", "tcp:3000"); + var rule = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 3000)); var (remote, local) = rule; - Assert.AreEqual ("tcp:5000", remote); - Assert.AreEqual ("tcp:3000", local); + Assert.AreEqual (AdbProtocol.Tcp, remote.Protocol); + Assert.AreEqual (5000, remote.Port); + Assert.AreEqual (AdbProtocol.Tcp, local.Protocol); + Assert.AreEqual (3000, local.Port); } [Test] - public void AdbReversePortRule_ToString () + public void AdbPortRule_ToString () { - var rule = new AdbReversePortRule ("tcp:5000", "tcp:3000"); + var rule = new AdbPortRule (new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 3000)); var str = rule.ToString (); Assert.That (str, Does.Contain ("tcp:5000")); @@ -937,6 +1054,24 @@ public void ReversePortAsync_String_EmptyLocal_ThrowsArgumentException () async () => await runner.ReversePortAsync ("emulator-5554", "tcp:5000", "")); } + // --- ReversePortAsync AdbPortSpec overload validation tests --- + + [Test] + public void ReversePortAsync_AdbPortSpec_NullRemote_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", (AdbPortSpec) null!, new AdbPortSpec (AdbProtocol.Tcp, 5000))); + } + + [Test] + public void ReversePortAsync_AdbPortSpec_NullLocal_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.ReversePortAsync ("emulator-5554", new AdbPortSpec (AdbProtocol.Tcp, 5000), (AdbPortSpec) null!)); + } + // --- RemoveReversePortAsync parameter validation tests --- [Test] @@ -973,6 +1108,16 @@ public void RemoveReversePortAsync_String_EmptyRemote_ThrowsArgumentException () async () => await runner.RemoveReversePortAsync ("emulator-5554", "")); } + // --- RemoveReversePortAsync AdbPortSpec overload validation tests --- + + [Test] + public void RemoveReversePortAsync_AdbPortSpec_NullRemote_ThrowsArgumentNull () + { + var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); + Assert.ThrowsAsync ( + async () => await runner.RemoveReversePortAsync ("emulator-5554", (AdbPortSpec) null!)); + } + // --- RemoveAllReversePortsAsync parameter validation tests --- [Test] From c359344657ac08ca14f586cbdfe9df1145946bbb Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 17:19:44 +0000 Subject: [PATCH 04/11] Simplify port API: keep only AdbPortSpec overloads Remove string and int convenience overloads per @jonathanpeppers review. The strongly-typed AdbPortSpec is now the only API surface for ReversePortAsync and RemoveReversePortAsync. Removed ValidatePort helper and RS0026/RS0027 suppressions (no longer needed). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 4 - .../netstandard2.0/PublicAPI.Unshipped.txt | 4 - .../Runners/AdbRunner.cs | 83 +++-------------- .../AdbRunnerTests.cs | 90 ++----------------- 4 files changed, 18 insertions(+), 163 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 885ca596..4df512c8 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -200,8 +200,4 @@ static Xamarin.Android.Tools.AdbPortSpec.TryParse(string! socketSpec) -> Xamarin virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, int remotePort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, string! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, int remotePort, int localPort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, string! remote, string! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 885ca596..4df512c8 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -200,8 +200,4 @@ static Xamarin.Android.Tools.AdbPortSpec.TryParse(string! socketSpec) -> Xamarin virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, int remotePort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, string! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, int remotePort, int localPort, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, string! remote, string! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index db3be2a6..918b9c8f 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -244,99 +244,48 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati return null; } -#pragma warning disable RS0026, RS0027 // Multiple overloads with optional parameters are intentional for API convenience /// - /// Sets up reverse port forwarding from the device to the host via - /// 'adb -s <serial> reverse <remote> <local>'. - /// Supports any socket spec accepted by adb (tcp:PORT, localabstract:NAME, etc.). - /// This is the core overload; the typed and int convenience overloads delegate here. + /// Sets up reverse port forwarding via 'adb -s <serial> reverse <remote> <local>'. /// /// Device serial number. - /// Remote (device-side) socket spec, e.g. "tcp:5000" or "localabstract:foo". - /// Local (host-side) socket spec, e.g. "tcp:5000". + /// Remote (device-side) port spec. + /// Local (host-side) port spec. /// Cancellation token. - public virtual async Task ReversePortAsync (string serial, string remote, string local, CancellationToken cancellationToken = default) + public virtual async Task ReversePortAsync (string serial, AdbPortSpec remote, AdbPortSpec local, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace (serial)) throw new ArgumentException ("Serial must not be empty.", nameof (serial)); - if (string.IsNullOrWhiteSpace (remote)) - throw new ArgumentException ("Remote socket spec must not be empty.", nameof (remote)); - if (string.IsNullOrWhiteSpace (local)) - throw new ArgumentException ("Local socket spec must not be empty.", nameof (local)); - - var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", remote, local); - using var stderr = new StringWriter (); - var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); - ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse {remote} {local}", stderr); - } - - /// - /// Typed overload: sets up reverse port forwarding using values. - /// - public virtual Task ReversePortAsync (string serial, AdbPortSpec remote, AdbPortSpec local, CancellationToken cancellationToken = default) - { if (remote is null) throw new ArgumentNullException (nameof (remote)); if (local is null) throw new ArgumentNullException (nameof (local)); - return ReversePortAsync (serial, remote.ToSocketSpec (), local.ToSocketSpec (), cancellationToken); - } - /// - /// TCP convenience overload: sets up reverse port forwarding via - /// 'adb -s <serial> reverse tcp:<remotePort> tcp:<localPort>'. - /// - public virtual Task ReversePortAsync (string serial, int remotePort, int localPort, CancellationToken cancellationToken = default) - { - ValidatePort (remotePort, nameof (remotePort)); - ValidatePort (localPort, nameof (localPort)); - return ReversePortAsync (serial, new AdbPortSpec (AdbProtocol.Tcp, remotePort), new AdbPortSpec (AdbProtocol.Tcp, localPort), cancellationToken); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", remote.ToSocketSpec (), local.ToSocketSpec ()); + using var stderr = new StringWriter (); + var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse {remote} {local}", stderr); } -#pragma warning restore RS0026, RS0027 -#pragma warning disable RS0026, RS0027 // Multiple overloads with optional parameters are intentional for API convenience /// /// Removes a specific reverse port forwarding rule via /// 'adb -s <serial> reverse --remove <remote>'. - /// Supports any socket spec accepted by adb. /// /// Device serial number. - /// Remote (device-side) socket spec to remove, e.g. "tcp:5000". + /// Remote (device-side) port spec to remove. /// Cancellation token. - public virtual async Task RemoveReversePortAsync (string serial, string remote, CancellationToken cancellationToken = default) + public virtual async Task RemoveReversePortAsync (string serial, AdbPortSpec remote, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace (serial)) throw new ArgumentException ("Serial must not be empty.", nameof (serial)); - if (string.IsNullOrWhiteSpace (remote)) - throw new ArgumentException ("Remote socket spec must not be empty.", nameof (remote)); + if (remote is null) + throw new ArgumentNullException (nameof (remote)); - var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove", remote); + var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove", remote.ToSocketSpec ()); using var stderr = new StringWriter (); var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --remove {remote}", stderr); } - /// - /// Typed overload: removes a specific reverse port forwarding rule using . - /// - public virtual Task RemoveReversePortAsync (string serial, AdbPortSpec remote, CancellationToken cancellationToken = default) - { - if (remote is null) - throw new ArgumentNullException (nameof (remote)); - return RemoveReversePortAsync (serial, remote.ToSocketSpec (), cancellationToken); - } - - /// - /// TCP convenience overload: removes a specific reverse port forwarding rule via - /// 'adb -s <serial> reverse --remove tcp:<remotePort>'. - /// - public virtual Task RemoveReversePortAsync (string serial, int remotePort, CancellationToken cancellationToken = default) - { - ValidatePort (remotePort, nameof (remotePort)); - return RemoveReversePortAsync (serial, new AdbPortSpec (AdbProtocol.Tcp, remotePort), cancellationToken); - } -#pragma warning restore RS0026, RS0027 - /// /// Removes all reverse port forwarding rules via /// 'adb -s <serial> reverse --remove-all'. @@ -400,12 +349,6 @@ internal static IReadOnlyList ParseReverseListOutput (IEnumerable 65535) - throw new ArgumentOutOfRangeException (paramName, port, "Port must be between 1 and 65535."); - } - /// /// Parses the output lines from 'adb devices -l'. /// Accepts an to avoid allocating a joined string. diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs index fbc2b8c7..09679f63 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -1001,63 +1001,11 @@ public void ReversePortAsync_EmptySerial_ThrowsArgumentException () { var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("", 5000, 5000)); + async () => await runner.ReversePortAsync ("", new AdbPortSpec (AdbProtocol.Tcp, 5000), new AdbPortSpec (AdbProtocol.Tcp, 5000))); } [Test] - public void ReversePortAsync_ZeroRemotePort_ThrowsArgumentOutOfRange () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("emulator-5554", 0, 5000)); - } - - [Test] - public void ReversePortAsync_NegativeLocalPort_ThrowsArgumentOutOfRange () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("emulator-5554", 5000, -1)); - } - - [Test] - public void ReversePortAsync_PortAbove65535_ThrowsArgumentOutOfRange () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("emulator-5554", 70000, 5000)); - } - - // --- ReversePortAsync string overload validation tests --- - - [Test] - public void ReversePortAsync_String_EmptySerial_ThrowsArgumentException () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("", "tcp:5000", "tcp:5000")); - } - - [Test] - public void ReversePortAsync_String_EmptyRemote_ThrowsArgumentException () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("emulator-5554", "", "tcp:5000")); - } - - [Test] - public void ReversePortAsync_String_EmptyLocal_ThrowsArgumentException () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("emulator-5554", "tcp:5000", "")); - } - - // --- ReversePortAsync AdbPortSpec overload validation tests --- - - [Test] - public void ReversePortAsync_AdbPortSpec_NullRemote_ThrowsArgumentNull () + public void ReversePortAsync_NullRemote_ThrowsArgumentNull () { var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); Assert.ThrowsAsync ( @@ -1065,7 +1013,7 @@ public void ReversePortAsync_AdbPortSpec_NullRemote_ThrowsArgumentNull () } [Test] - public void ReversePortAsync_AdbPortSpec_NullLocal_ThrowsArgumentNull () + public void ReversePortAsync_NullLocal_ThrowsArgumentNull () { var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); Assert.ThrowsAsync ( @@ -1079,39 +1027,11 @@ public void RemoveReversePortAsync_EmptySerial_ThrowsArgumentException () { var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); Assert.ThrowsAsync ( - async () => await runner.RemoveReversePortAsync ("", 5000)); - } - - [Test] - public void RemoveReversePortAsync_ZeroPort_ThrowsArgumentOutOfRange () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.RemoveReversePortAsync ("emulator-5554", 0)); + async () => await runner.RemoveReversePortAsync ("", new AdbPortSpec (AdbProtocol.Tcp, 5000))); } - // --- RemoveReversePortAsync string overload validation tests --- - - [Test] - public void RemoveReversePortAsync_String_EmptySerial_ThrowsArgumentException () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.RemoveReversePortAsync ("", "tcp:5000")); - } - - [Test] - public void RemoveReversePortAsync_String_EmptyRemote_ThrowsArgumentException () - { - var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); - Assert.ThrowsAsync ( - async () => await runner.RemoveReversePortAsync ("emulator-5554", "")); - } - - // --- RemoveReversePortAsync AdbPortSpec overload validation tests --- - [Test] - public void RemoveReversePortAsync_AdbPortSpec_NullRemote_ThrowsArgumentNull () + public void RemoveReversePortAsync_NullRemote_ThrowsArgumentNull () { var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); Assert.ThrowsAsync ( From 8a30a7e80384f40f1aa383595294eda828223ec2 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 17:26:35 +0000 Subject: [PATCH 05/11] Include stdout in ListReversePortsAsync error diagnostics Pass stdout to ThrowIfFailed so failures include any diagnostics written to stdout, not just stderr. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index 918b9c8f..681800a5 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -314,7 +314,7 @@ public virtual async Task> ListReversePortsAsync (str using var stderr = new StringWriter (); var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--list"); var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); - ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --list", stderr); + ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} reverse --list", stderr, stdout); return ParseReverseListOutput (stdout.ToString ().Split ('\n')); } From c36090ae447b46bca7b41589462f7df4c2a3bc47 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 17:43:22 +0000 Subject: [PATCH 06/11] Address Copilot review: split files, throw on invalid protocol, nullable TryParse - Split AdbPortSpec into its own file (one public type per file convention) - AdbPortRule.cs now contains only AdbPortRule record - ToSocketSpec() throws ArgumentOutOfRangeException for unknown AdbProtocol values - TryParse parameter changed from string to string? to match null-handling behavior - Remove unused 'using System' from AdbPortRule.cs - Update PublicAPI files for TryParse signature change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 2 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 2 +- .../Runners/AdbPortRule.cs | 51 ----------------- .../Runners/AdbPortSpec.cs | 57 +++++++++++++++++++ .../AdbRunnerTests.cs | 30 +++++++--- 5 files changed, 80 insertions(+), 62 deletions(-) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 4df512c8..a8ad1737 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -196,7 +196,7 @@ Xamarin.Android.Tools.AdbProtocol.LocalFilesystem = 3 -> Xamarin.Android.Tools.A Xamarin.Android.Tools.AdbProtocol.LocalReserved = 2 -> Xamarin.Android.Tools.AdbProtocol Xamarin.Android.Tools.AdbProtocol.Tcp = 0 -> Xamarin.Android.Tools.AdbProtocol override Xamarin.Android.Tools.AdbPortSpec.ToString() -> string! -static Xamarin.Android.Tools.AdbPortSpec.TryParse(string! socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? +static Xamarin.Android.Tools.AdbPortSpec.TryParse(string? socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 4df512c8..a8ad1737 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -196,7 +196,7 @@ Xamarin.Android.Tools.AdbProtocol.LocalFilesystem = 3 -> Xamarin.Android.Tools.A Xamarin.Android.Tools.AdbProtocol.LocalReserved = 2 -> Xamarin.Android.Tools.AdbProtocol Xamarin.Android.Tools.AdbProtocol.Tcp = 0 -> Xamarin.Android.Tools.AdbProtocol override Xamarin.Android.Tools.AdbPortSpec.ToString() -> string! -static Xamarin.Android.Tools.AdbPortSpec.TryParse(string! socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? +static Xamarin.Android.Tools.AdbPortSpec.TryParse(string? socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs index a684e97a..190e6661 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.cs @@ -1,59 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; - namespace Xamarin.Android.Tools; -/// -/// Represents a port and protocol pair for adb forwarding/reverse operations. -/// -public record AdbPortSpec (AdbProtocol Protocol, int Port) -{ - /// - /// Returns the adb socket spec string, e.g. "tcp:5000". - /// - public string ToSocketSpec () => Protocol switch { - AdbProtocol.Tcp => $"tcp:{Port}", - AdbProtocol.LocalAbstract => $"localabstract:{Port}", - AdbProtocol.LocalReserved => $"localreserved:{Port}", - AdbProtocol.LocalFilesystem => $"localfilesystem:{Port}", - _ => $"tcp:{Port}", - }; - - /// - /// Parses an adb socket spec string like "tcp:5000" into an . - /// Returns null if the format is unrecognized. - /// - public static AdbPortSpec? TryParse (string socketSpec) - { - if (string.IsNullOrWhiteSpace (socketSpec)) - return null; - - var colonIndex = socketSpec.IndexOf (':'); - if (colonIndex <= 0 || colonIndex >= socketSpec.Length - 1) - return null; - - var protocolStr = socketSpec.Substring (0, colonIndex); - var portStr = socketSpec.Substring (colonIndex + 1); - - if (!int.TryParse (portStr, out var port) || port <= 0 || port > 65535) - return null; - - var protocol = protocolStr.ToLowerInvariant () switch { - "tcp" => (AdbProtocol?) AdbProtocol.Tcp, - "localabstract" => AdbProtocol.LocalAbstract, - "localreserved" => AdbProtocol.LocalReserved, - "localfilesystem" => AdbProtocol.LocalFilesystem, - _ => null, - }; - - return protocol.HasValue ? new AdbPortSpec (protocol.Value, port) : null; - } - - public override string ToString () => ToSocketSpec (); -} - /// /// Represents an adb port forwarding rule as reported by 'adb reverse --list' or 'adb forward --list'. /// diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs new file mode 100644 index 00000000..e9118558 --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Xamarin.Android.Tools; + +/// +/// Represents a port and protocol pair for adb forwarding/reverse operations. +/// +public record AdbPortSpec (AdbProtocol Protocol, int Port) +{ + /// + /// Returns the adb socket spec string, e.g. "tcp:5000". + /// + public string ToSocketSpec () => Protocol switch { + AdbProtocol.Tcp => $"tcp:{Port}", + AdbProtocol.LocalAbstract => $"localabstract:{Port}", + AdbProtocol.LocalReserved => $"localreserved:{Port}", + AdbProtocol.LocalFilesystem => $"localfilesystem:{Port}", + _ => throw new ArgumentOutOfRangeException (nameof (Protocol), Protocol, $"Unsupported ADB protocol: {Protocol}"), + }; + + /// + /// Parses an adb socket spec string like "tcp:5000" into an . + /// Returns null if the format is unrecognized. + /// + public static AdbPortSpec? TryParse (string? socketSpec) + { + if (string.IsNullOrWhiteSpace (socketSpec)) + return null; + + // socketSpec is guaranteed non-null after IsNullOrWhiteSpace check + var value = socketSpec!; + var colonIndex = value.IndexOf (':'); + if (colonIndex <= 0 || colonIndex >= value.Length - 1) + return null; + + var protocolStr = value.Substring (0, colonIndex); + var portStr = value.Substring (colonIndex + 1); + + if (!int.TryParse (portStr, out var port) || port <= 0 || port > 65535) + return null; + + var protocol = protocolStr.ToLowerInvariant () switch { + "tcp" => (AdbProtocol?) AdbProtocol.Tcp, + "localabstract" => AdbProtocol.LocalAbstract, + "localreserved" => AdbProtocol.LocalReserved, + "localfilesystem" => AdbProtocol.LocalFilesystem, + _ => null, + }; + + return protocol.HasValue ? new AdbPortSpec (protocol.Value, port) : null; + } + + public override string ToString () => ToSocketSpec (); +} diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs index 09679f63..a44b39fe 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -854,8 +854,11 @@ public void ParseReverseListOutput_WindowsLineEndings () public void AdbPortSpec_TryParse_ValidTcp () { var spec = AdbPortSpec.TryParse ("tcp:5000"); - Assert.IsNotNull (spec); - Assert.AreEqual (AdbProtocol.Tcp, spec!.Protocol); + if (spec is null) { + Assert.Fail ("Expected non-null AdbPortSpec"); + return; + } + Assert.AreEqual (AdbProtocol.Tcp, spec.Protocol); Assert.AreEqual (5000, spec.Port); } @@ -863,8 +866,11 @@ public void AdbPortSpec_TryParse_ValidTcp () public void AdbPortSpec_TryParse_ValidLocalAbstract () { var spec = AdbPortSpec.TryParse ("localabstract:9222"); - Assert.IsNotNull (spec); - Assert.AreEqual (AdbProtocol.LocalAbstract, spec!.Protocol); + if (spec is null) { + Assert.Fail ("Expected non-null AdbPortSpec"); + return; + } + Assert.AreEqual (AdbProtocol.LocalAbstract, spec.Protocol); Assert.AreEqual (9222, spec.Port); } @@ -872,8 +878,11 @@ public void AdbPortSpec_TryParse_ValidLocalAbstract () public void AdbPortSpec_TryParse_ValidLocalReserved () { var spec = AdbPortSpec.TryParse ("localreserved:1234"); - Assert.IsNotNull (spec); - Assert.AreEqual (AdbProtocol.LocalReserved, spec!.Protocol); + if (spec is null) { + Assert.Fail ("Expected non-null AdbPortSpec"); + return; + } + Assert.AreEqual (AdbProtocol.LocalReserved, spec.Protocol); Assert.AreEqual (1234, spec.Port); } @@ -881,15 +890,18 @@ public void AdbPortSpec_TryParse_ValidLocalReserved () public void AdbPortSpec_TryParse_ValidLocalFilesystem () { var spec = AdbPortSpec.TryParse ("localfilesystem:8080"); - Assert.IsNotNull (spec); - Assert.AreEqual (AdbProtocol.LocalFilesystem, spec!.Protocol); + if (spec is null) { + Assert.Fail ("Expected non-null AdbPortSpec"); + return; + } + Assert.AreEqual (AdbProtocol.LocalFilesystem, spec.Protocol); Assert.AreEqual (8080, spec.Port); } [Test] public void AdbPortSpec_TryParse_Null_ReturnsNull () { - Assert.IsNull (AdbPortSpec.TryParse (null!)); + Assert.IsNull (AdbPortSpec.TryParse (default)); } [Test] From 3bf763cfcc812c7c2fcc3bfd7776340a04ba76c0 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 18:23:54 +0000 Subject: [PATCH 07/11] Address Copilot review round 3: remove null-forgiving, validate port range - Replace null! with null in tests (no nullable context in test project) - Replace socketSpec! with property pattern (is not { Length: > 0 }) for null flow - Add port range validation (1-65535) in ReversePortAsync and RemoveReversePortAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs | 4 +--- src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs | 6 ++++++ .../AdbRunnerTests.cs | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs index e9118558..26e5b188 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs @@ -27,11 +27,9 @@ public record AdbPortSpec (AdbProtocol Protocol, int Port) /// public static AdbPortSpec? TryParse (string? socketSpec) { - if (string.IsNullOrWhiteSpace (socketSpec)) + if (socketSpec is not { Length: > 0 } value || string.IsNullOrWhiteSpace (value)) return null; - // socketSpec is guaranteed non-null after IsNullOrWhiteSpace check - var value = socketSpec!; var colonIndex = value.IndexOf (':'); if (colonIndex <= 0 || colonIndex >= value.Length - 1) return null; diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index 681800a5..e2de6950 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -259,6 +259,10 @@ public virtual async Task ReversePortAsync (string serial, AdbPortSpec remote, A throw new ArgumentNullException (nameof (remote)); if (local is null) throw new ArgumentNullException (nameof (local)); + if (remote.Port <= 0 || remote.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (remote), remote.Port, "Port must be between 1 and 65535."); + if (local.Port <= 0 || local.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (local), local.Port, "Port must be between 1 and 65535."); var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", remote.ToSocketSpec (), local.ToSocketSpec ()); using var stderr = new StringWriter (); @@ -279,6 +283,8 @@ public virtual async Task RemoveReversePortAsync (string serial, AdbPortSpec rem throw new ArgumentException ("Serial must not be empty.", nameof (serial)); if (remote is null) throw new ArgumentNullException (nameof (remote)); + if (remote.Port <= 0 || remote.Port > 65535) + throw new ArgumentOutOfRangeException (nameof (remote), remote.Port, "Port must be between 1 and 65535."); var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "reverse", "--remove", remote.ToSocketSpec ()); using var stderr = new StringWriter (); diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs index a44b39fe..5565858c 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -1021,7 +1021,7 @@ public void ReversePortAsync_NullRemote_ThrowsArgumentNull () { var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("emulator-5554", (AdbPortSpec) null!, new AdbPortSpec (AdbProtocol.Tcp, 5000))); + async () => await runner.ReversePortAsync ("emulator-5554", (AdbPortSpec) null, new AdbPortSpec (AdbProtocol.Tcp, 5000))); } [Test] @@ -1029,7 +1029,7 @@ public void ReversePortAsync_NullLocal_ThrowsArgumentNull () { var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); Assert.ThrowsAsync ( - async () => await runner.ReversePortAsync ("emulator-5554", new AdbPortSpec (AdbProtocol.Tcp, 5000), (AdbPortSpec) null!)); + async () => await runner.ReversePortAsync ("emulator-5554", new AdbPortSpec (AdbProtocol.Tcp, 5000), (AdbPortSpec) null)); } // --- RemoveReversePortAsync parameter validation tests --- @@ -1047,7 +1047,7 @@ public void RemoveReversePortAsync_NullRemote_ThrowsArgumentNull () { var runner = new AdbRunner ("/fake/sdk/platform-tools/adb"); Assert.ThrowsAsync ( - async () => await runner.RemoveReversePortAsync ("emulator-5554", (AdbPortSpec) null!)); + async () => await runner.RemoveReversePortAsync ("emulator-5554", (AdbPortSpec) null)); } // --- RemoveAllReversePortsAsync parameter validation tests --- From 15af8178a07fabd58850110edf8c470223384f65 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 18:57:06 +0000 Subject: [PATCH 08/11] Address Copilot review round 4: whitespace-tolerant parsing, tab test - ParseReverseListOutput now splits on all whitespace (tabs, spaces) instead of single space - Added ParseReverseListOutput_TabSeparated test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/AdbRunner.cs | 2 +- .../AdbRunnerTests.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index e2de6950..0eadc70e 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -343,7 +343,7 @@ internal static IReadOnlyList ParseReverseListOutput (IEnumerable= 2) { var remote = AdbPortSpec.TryParse (parts [0]); var local = AdbPortSpec.TryParse (parts [1]); diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs index 5565858c..1caeb2a0 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -848,6 +848,21 @@ public void ParseReverseListOutput_WindowsLineEndings () Assert.AreEqual (8081, rules [1].Remote.Port); } + [Test] + public void ParseReverseListOutput_TabSeparated () + { + var output = new [] { + "(reverse)\ttcp:5000\ttcp:5000", + "(reverse)\ttcp:8081\ttcp:8081", + }; + + var rules = AdbRunner.ParseReverseListOutput (output); + + Assert.AreEqual (2, rules.Count); + Assert.AreEqual (5000, rules [0].Remote.Port); + Assert.AreEqual (8081, rules [1].Remote.Port); + } + // --- AdbPortSpec tests --- [Test] From 733b9825ef5787cb714c4f2e7e104a67157ac836 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 22:40:47 +0000 Subject: [PATCH 09/11] Address @jonathanpeppers review: TCP-only enum, add ToSocketSpec tests - Remove LocalAbstract, LocalReserved, LocalFilesystem from AdbProtocol enum (add later when needed) - Simplify ToSocketSpec and TryParse to only handle TCP - Add ToSocketSpec unit tests: HighPort, LowPort, InvalidProtocol_Throws - Add TryParse test: NonTcpProtocol_ReturnsNull - Remove 3 non-TCP TryParse tests - Update PublicAPI files (remove 3 enum entries per TFM) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 4 +- .../netstandard2.0/PublicAPI.Unshipped.txt | 4 +- .../Runners/AdbPortSpec.cs | 6 --- .../Runners/AdbProtocol.cs | 6 +-- .../AdbRunnerTests.cs | 54 +++++++------------ 5 files changed, 24 insertions(+), 50 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt index a8ad1737..61fc0e0b 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -191,9 +191,7 @@ Xamarin.Android.Tools.AdbPortSpec.Protocol.get -> Xamarin.Android.Tools.AdbProto Xamarin.Android.Tools.AdbPortSpec.Protocol.init -> void Xamarin.Android.Tools.AdbPortSpec.ToSocketSpec() -> string! Xamarin.Android.Tools.AdbProtocol -Xamarin.Android.Tools.AdbProtocol.LocalAbstract = 1 -> Xamarin.Android.Tools.AdbProtocol -Xamarin.Android.Tools.AdbProtocol.LocalFilesystem = 3 -> Xamarin.Android.Tools.AdbProtocol -Xamarin.Android.Tools.AdbProtocol.LocalReserved = 2 -> Xamarin.Android.Tools.AdbProtocol + Xamarin.Android.Tools.AdbProtocol.Tcp = 0 -> Xamarin.Android.Tools.AdbProtocol override Xamarin.Android.Tools.AdbPortSpec.ToString() -> string! static Xamarin.Android.Tools.AdbPortSpec.TryParse(string? socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index a8ad1737..61fc0e0b 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -191,9 +191,7 @@ Xamarin.Android.Tools.AdbPortSpec.Protocol.get -> Xamarin.Android.Tools.AdbProto Xamarin.Android.Tools.AdbPortSpec.Protocol.init -> void Xamarin.Android.Tools.AdbPortSpec.ToSocketSpec() -> string! Xamarin.Android.Tools.AdbProtocol -Xamarin.Android.Tools.AdbProtocol.LocalAbstract = 1 -> Xamarin.Android.Tools.AdbProtocol -Xamarin.Android.Tools.AdbProtocol.LocalFilesystem = 3 -> Xamarin.Android.Tools.AdbProtocol -Xamarin.Android.Tools.AdbProtocol.LocalReserved = 2 -> Xamarin.Android.Tools.AdbProtocol + Xamarin.Android.Tools.AdbProtocol.Tcp = 0 -> Xamarin.Android.Tools.AdbProtocol override Xamarin.Android.Tools.AdbPortSpec.ToString() -> string! static Xamarin.Android.Tools.AdbPortSpec.TryParse(string? socketSpec) -> Xamarin.Android.Tools.AdbPortSpec? diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs index 26e5b188..0c852aef 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs @@ -15,9 +15,6 @@ public record AdbPortSpec (AdbProtocol Protocol, int Port) /// public string ToSocketSpec () => Protocol switch { AdbProtocol.Tcp => $"tcp:{Port}", - AdbProtocol.LocalAbstract => $"localabstract:{Port}", - AdbProtocol.LocalReserved => $"localreserved:{Port}", - AdbProtocol.LocalFilesystem => $"localfilesystem:{Port}", _ => throw new ArgumentOutOfRangeException (nameof (Protocol), Protocol, $"Unsupported ADB protocol: {Protocol}"), }; @@ -42,9 +39,6 @@ public record AdbPortSpec (AdbProtocol Protocol, int Port) var protocol = protocolStr.ToLowerInvariant () switch { "tcp" => (AdbProtocol?) AdbProtocol.Tcp, - "localabstract" => AdbProtocol.LocalAbstract, - "localreserved" => AdbProtocol.LocalReserved, - "localfilesystem" => AdbProtocol.LocalFilesystem, _ => null, }; diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs index c1044b8e..8b2c2dc4 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs @@ -8,8 +8,8 @@ namespace Xamarin.Android.Tools; /// public enum AdbProtocol { + /// + /// TCP socket spec, e.g. "tcp:5000". + /// Tcp, - LocalAbstract, - LocalReserved, - LocalFilesystem, } diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs index 1caeb2a0..00b5ccc6 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs @@ -878,39 +878,9 @@ public void AdbPortSpec_TryParse_ValidTcp () } [Test] - public void AdbPortSpec_TryParse_ValidLocalAbstract () + public void AdbPortSpec_TryParse_NonTcpProtocol_ReturnsNull () { - var spec = AdbPortSpec.TryParse ("localabstract:9222"); - if (spec is null) { - Assert.Fail ("Expected non-null AdbPortSpec"); - return; - } - Assert.AreEqual (AdbProtocol.LocalAbstract, spec.Protocol); - Assert.AreEqual (9222, spec.Port); - } - - [Test] - public void AdbPortSpec_TryParse_ValidLocalReserved () - { - var spec = AdbPortSpec.TryParse ("localreserved:1234"); - if (spec is null) { - Assert.Fail ("Expected non-null AdbPortSpec"); - return; - } - Assert.AreEqual (AdbProtocol.LocalReserved, spec.Protocol); - Assert.AreEqual (1234, spec.Port); - } - - [Test] - public void AdbPortSpec_TryParse_ValidLocalFilesystem () - { - var spec = AdbPortSpec.TryParse ("localfilesystem:8080"); - if (spec is null) { - Assert.Fail ("Expected non-null AdbPortSpec"); - return; - } - Assert.AreEqual (AdbProtocol.LocalFilesystem, spec.Protocol); - Assert.AreEqual (8080, spec.Port); + Assert.IsNull (AdbPortSpec.TryParse ("localabstract:9222")); } [Test] @@ -963,10 +933,24 @@ public void AdbPortSpec_ToSocketSpec_Tcp () } [Test] - public void AdbPortSpec_ToSocketSpec_LocalAbstract () + public void AdbPortSpec_ToSocketSpec_HighPort () + { + var spec = new AdbPortSpec (AdbProtocol.Tcp, 65535); + Assert.AreEqual ("tcp:65535", spec.ToSocketSpec ()); + } + + [Test] + public void AdbPortSpec_ToSocketSpec_LowPort () + { + var spec = new AdbPortSpec (AdbProtocol.Tcp, 1); + Assert.AreEqual ("tcp:1", spec.ToSocketSpec ()); + } + + [Test] + public void AdbPortSpec_ToSocketSpec_InvalidProtocol_Throws () { - var spec = new AdbPortSpec (AdbProtocol.LocalAbstract, 9222); - Assert.AreEqual ("localabstract:9222", spec.ToSocketSpec ()); + var spec = new AdbPortSpec ((AdbProtocol) 99, 5000); + Assert.Throws (() => spec.ToSocketSpec ()); } [Test] From 51f8c72cf8acce68c6ce05980fa8090b392d3ef3 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 20 Mar 2026 15:39:31 -0500 Subject: [PATCH 10/11] Apply suggestion from @jonathanpeppers --- src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs index 0c852aef..33df8ca9 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs @@ -14,7 +14,7 @@ public record AdbPortSpec (AdbProtocol Protocol, int Port) /// Returns the adb socket spec string, e.g. "tcp:5000". /// public string ToSocketSpec () => Protocol switch { - AdbProtocol.Tcp => $"tcp:{Port}", + AdbProtocol.Tcp => FormattableString.Invariant ($"tcp:{Port})", _ => throw new ArgumentOutOfRangeException (nameof (Protocol), Protocol, $"Unsupported ADB protocol: {Protocol}"), }; From 1194140b0fe5261a923f62d3b8e8fdd32dfc7092 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Mon, 23 Mar 2026 12:52:00 +0000 Subject: [PATCH 11/11] Fix syntax error in FormattableString.Invariant call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suggestion commit had mismatched parentheses: FormattableString.Invariant ($"tcp:{Port}") ← extra ) inside string Fixed to: FormattableString.Invariant ($"tcp:{Port}") ← correct Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs index 33df8ca9..765c6ad2 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs @@ -14,7 +14,7 @@ public record AdbPortSpec (AdbProtocol Protocol, int Port) /// Returns the adb socket spec string, e.g. "tcp:5000". /// public string ToSocketSpec () => Protocol switch { - AdbProtocol.Tcp => FormattableString.Invariant ($"tcp:{Port})", + AdbProtocol.Tcp => FormattableString.Invariant ($"tcp:{Port}"), _ => throw new ArgumentOutOfRangeException (nameof (Protocol), Protocol, $"Unsupported ADB protocol: {Protocol}"), };