Skip to content

Add ADB reverse port forwarding support#305

Open
rmarinho wants to merge 2 commits intomainfrom
feature/adb-reverse-port
Open

Add ADB reverse port forwarding support#305
rmarinho wants to merge 2 commits intomainfrom
feature/adb-reverse-port

Conversation

@rmarinho
Copy link
Member

@rmarinho rmarinho commented Mar 13, 2026

Summary

Add reverse port forwarding APIs to AdbRunner, enabling the MAUI DevTools CLI to manage hot-reload tunnels without going through ServiceHub.

Closes #303

Context

Reverse port forwarding (adb reverse) is used across three separate codebases today, each with its own implementation:

1. Visual Studio (ClientTools.Platform) — ADB Wire Protocol

  • AdbServer.cs: raw wire protocol via TransportRunCommandWithStatus
  • Complex: manages raw sockets, transport selection, status parsing

2. VS Code Extension (vscode-maui) — ServiceHub RPC

  • TypeScript calls C# AdbServer via ServiceHub JSON-RPC
  • Requires ServiceHub infrastructure (C# host process, JSON-RPC)

3. Manual CLI — No C# implementation

  • dotnet/android docs reference adb reverse tcp:9000 tcp:9001 for tracing

The Problem

Each IDE reimplements the same ADB operations. The wire protocol approach is powerful but complex.

What This PR Does

Adds a shared, CLI-based implementation in android-tools:

// TCP convenience wrappers
await adbRunner.ReversePortAsync("emulator-5554", remotePort: 5000, localPort: 5000);

// String socket-spec for non-TCP protocols
await adbRunner.ReversePortAsync("emulator-5554", "localabstract:hot-reload", "tcp:5000");

// Manage tunnels
await adbRunner.RemoveReversePortAsync("emulator-5554", remotePort: 5000);
await adbRunner.RemoveAllReversePortsAsync("emulator-5554");

// Diagnostics
IReadOnlyList<AdbReversePortRule> rules = await adbRunner.ListReversePortsAsync("emulator-5554");

New API Surface

AdbReversePortRule — Positional Record

public record AdbReversePortRule(string Remote, string Local);

Value type with structural equality, deconstruction (net10.0), and built-in ToString().

AdbRunner methods

Method ADB Command Purpose
ReversePortAsync(serial, string remote, string local, ct) adb -s <serial> reverse <remote> <local> Core — any socket spec
ReversePortAsync(serial, int remotePort, int localPort, ct) adb -s <serial> reverse tcp:<r> tcp:<l> TCP convenience wrapper
RemoveReversePortAsync(serial, string remote, ct) adb -s <serial> reverse --remove <remote> Core — any socket spec
RemoveReversePortAsync(serial, int remotePort, ct) adb -s <serial> reverse --remove tcp:<r> TCP convenience wrapper
RemoveAllReversePortsAsync(serial, ct) adb -s <serial> reverse --remove-all Remove all rules
ListReversePortsAsync(serial, ct) adb -s <serial> reverse --list List active rules

Design: String overloads are core implementations accepting full ADB socket specs (tcp:5000, localabstract:name, localfilesystem:/path). Int overloads delegate to string overloads with tcp:{port} formatting. Matches existing patterns in ClientTools.Platform and vscode-maui.

Key Design Decisions

  1. CLI-based, not wire protocol — Simpler to maintain and test, functionally equivalent
  2. String + int overloads — Full flexibility for non-TCP protocols, ergonomic TCP wrappers
  3. Positional record — Value equality, concise construction, consistent with AvdInfo/SdkPackage
  4. RemoveAllReversePortsAsync — Cleanup after session crash/stale tunnels
  5. ListReversePortsAsync — Diagnostic reporting for CLI

Tests

25 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbReversePortRule record: value equality, deconstruction, ToString
  • Parameter validation (int): empty serial, zero/negative/overflow ports
  • Parameter validation (string): empty serial, empty remote, empty local

Migration Path

Phase 1 (this PR): Shared API in android-tools
Phase 2: MAUI DevTools CLI adds `maui android adb reverse` command
Phase 3: vscode-maui calls CLI instead of ServiceHub
Phase 4: Visual Studio can switch from wire protocol to CLI

@jonathanpeppers — Would automating reverse port setup for tracing/profiling in MSBuild tasks be useful? The API is available via the existing submodule.

Related

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>
Copilot AI review requested due to automatic review settings March 13, 2026 09:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds reverse port forwarding support to the Android SDK runner layer so downstream tooling (MAUI DevTools CLI / VS Code MAUI extension) can manage Hot Reload tunnels directly via library APIs.

Changes:

  • Added AdbRunner APIs for adb reverse operations (add/remove/remove-all/list) plus parsing for reverse --list output.
  • Introduced new public model type AdbReversePortRule to represent a listed reverse rule.
  • Updated PublicAPI unshipped files and added unit tests for parsing and parameter validation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs Adds tests for reverse list parsing and basic parameter validation for new APIs
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Implements new reverse port forwarding methods and output parsing helper
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Adds public DTO for reverse port rules
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt Declares new public API surface for netstandard2.0
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt Declares new public API surface for net10.0

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +780 to +786
[Test]
public void ReversePortAsync_PortAbove65535_ThrowsArgumentOutOfRange ()
{
var runner = new AdbRunner ("/fake/sdk/platform-tools/adb");
Assert.ThrowsAsync<System.ArgumentOutOfRangeException> (
async () => await runner.ReversePortAsync ("emulator-5554", 70000, 5000));
}
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);
Comment on lines +149 to +153
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I question the API design:

public class AdbReversePortRule
{
    public string Remote { get; init; }  // e.g. "tcp:5000"
    public string Local { get; init; }   // e.g. "tcp:5000"
}

C# is strongly typed, we shouldn't be using strings like tcp:5000.

We should have an enum as a separate property to specify if it is tcp (and fill in the other options in the enum).

Then we should have an int property for the port.

Can't you also do adb forward?

So should this be:

public record AdbPortAndProtocol (/* this is the enum*/ AdbProtocol Protocol, int Port);

public record AdbReverseRule (AdbPortAndProtocol Remote, AdbPortAndProtocol Local);

public record AdbForwardingRule (AdbPortAndProtocol Remote, AdbPortAndProtocol Local);

But then I don't know if I like having two records for reverse & forward. Maybe it should be a single record + another enum that says if it is Forward/Reverse.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants