This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add ADB reverse port forwarding support - #305

Merged
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port
Mar 23, 2026
Merged

Add ADB reverse port forwarding support#305
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port

Conversation

@rmarinho

@rmarinhormarinho commented Mar 13, 2026

Copy link
Copy Markdown
Member

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) — raw ADB wire protocol via TransportRunCommandWithStatus
  2. VS Code Extension (vscode-maui) — ServiceHub JSON-RPC to C# AdbServer
  3. Manual CLI — no shared C# implementation

Each IDE reimplements the same ADB operations. This PR adds a shared, CLI-based implementation in android-tools.

New API Surface

Types

publicenumAdbProtocol{Tcp,LocalAbstract,LocalReserved,LocalFilesystem}publicrecordAdbPortSpec(AdbProtocolProtocol,intPort){publicstringToSocketSpec();// e.g. "tcp:5000"publicstaticAdbPortSpec?TryParse(stringspec);// parse "tcp:5000" → AdbPortSpec}publicrecordAdbPortRule(AdbPortSpecRemote,AdbPortSpecLocal);

AdbRunner Methods

MethodADB CommandPurpose
ReversePortAsync(serial, AdbPortSpec remote, AdbPortSpec local, ct)adb reverse <remote> <local>Set up reverse forwarding
RemoveReversePortAsync(serial, AdbPortSpec remote, ct)adb reverse --remove <remote>Remove a specific rule
RemoveAllReversePortsAsync(serial, ct)adb reverse --remove-allRemove all rules
ListReversePortsAsync(serial, ct)adb reverse --listList active rules

Usage

vartcp5000=newAdbPortSpec(AdbProtocol.Tcp,5000);// Set up hot-reload tunnelawaitadbRunner.ReversePortAsync("emulator-5554",tcp5000,tcp5000);// List active rulesIReadOnlyList<AdbPortRule>rules=awaitadbRunner.ListReversePortsAsync("emulator-5554");// Clean upawaitadbRunner.RemoveReversePortAsync("emulator-5554",tcp5000);awaitadbRunner.RemoveAllReversePortsAsync("emulator-5554");

Key Design Decisions

  1. Strongly-typed AdbPortSpec — enum + int instead of raw strings like "tcp:5000", per @jonathanpeppers review
  2. CLI-based, not wire protocol — simpler to maintain and test, functionally equivalent
  3. Single overload per method — only AdbPortSpec, no string or int convenience overloads
  4. AdbPortRule record — value equality, structural matching on list output
  5. stdout included in error diagnosticsListReversePortsAsync passes both stdout and stderr to ThrowIfFailed

Tests

22 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbPortSpec.TryParse: valid TCP/LocalAbstract/LocalReserved/LocalFilesystem, null, empty, no colon, non-numeric port, zero port, unknown protocol
  • Parameter validation: empty serial, null remote/local (ArgumentNullException)
  • RemoveAllReversePortsAsync: empty serial validation
  • ListReversePortsAsync: empty serial validation

Review Feedback Addressed

ReviewerFeedbackResolution
@jonathanpeppersUse strongly-typed enum+int, not strings like "tcp:5000"AdbPortSpec(AdbProtocol, int) — single overload
@jonathanpeppersToo many overloads (string, int, AdbPortSpec)✅ Removed string and int overloads, kept only AdbPortSpec
@jonathanpeppersShould support adb forward too — rename to AdbPortRule✅ Renamed from AdbReversePortRule to AdbPortRule
CopilotInclude stdout in error diagnostics for ListReversePortsAsync✅ Pass stdout to ThrowIfFailed

Migration Path

Phase 1 (this PR): Shared API in android-tools ← HERE
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

Related

CopilotAI review requested due to automatic review settings March 13, 2026 09:20

This comment was marked as outdated.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch 2 times, most recently from b4bba1e to 3075d2cCompareMarch 19, 2026 14:22
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 40512c4 to 798b585CompareMarch 19, 2026 17:19
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 17:28

This comment was marked as outdated.

rmarinho added a commit that referenced this pull request Mar 19, 2026
…loads, stdout in errors
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 18:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, enabling downstream tooling (e.g., MAUI DevTools CLI) to manage hot-reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule).
  • Adds AdbRunner APIs for reverse port forwarding management (reverse, --remove, --remove-all, --list) plus list-output parsing.
  • Extends unit tests to cover parsing, AdbPortSpec parsing/formatting, and parameter validation; updates PublicAPI unshipped files.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec parsing/formatting, and new API parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds reverse port forwarding methods and adb reverse --list output parsing helper.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a forwarding rule (remote/local).
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for net10.0.

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

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, intended to be consumed by MAUI DevTools CLI (and later VS Code / VS) to manage Hot Reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed reverse port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule) and adds reverse operations to AdbRunner.
  • Implements parsing for adb reverse --list output.
  • Adds unit tests covering parsing, round-tripping, value equality, and parameter validation; updates PublicAPI unshipped files for both TFMs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse-list parsing, AdbPortSpec parsing/formatting, AdbPortRule equality, and new API argument validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds ReversePortAsync, removal APIs, list API, and adb reverse --list output parser.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a reverse/forward rule pair.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 Xamarin.Android.Tools.AndroidSdk’s AdbRunner, enabling downstream tools (e.g., MAUI DevTools CLI) to manage adb reverse tunnels via the adb CLI.

Changes:

  • Added new public types (AdbProtocol, AdbPortSpec, AdbPortRule) for strongly-typed reverse/forward socket specs and rules.
  • Added AdbRunner APIs for adb reverse operations: add rule, remove rule, remove all, and list rules (with list output parsing).
  • Added unit tests covering list parsing, AdbPortSpec parsing/formatting, and parameter validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec/AdbPortRule, and parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csImplements adb reverse command wrappers and a parser for adb reverse --list output.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csIntroduces AdbProtocol enum (currently TCP only).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csIntroduces AdbPortSpec record with ToSocketSpec() + TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csIntroduces AdbPortRule record representing a reverse/forward rule.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs

var rules = AdbRunner.ParseReverseListOutput (output);

// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped

CopilotAIMar 19, 2026

Copy link

Choose a reason for hiding this comment

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

This comment is a bit misleading: localabstract:... isn’t a “non-numeric port” (it’s a named socket). Consider rewording to clarify that the spec is skipped because AdbPortSpec.TryParse only supports numeric tcp:<port> at the moment.

Suggested change
// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped
// localabstract:chrome_devtools_remote is a named localabstract socket, not a tcp:<port>, so TryParse skips it

Copilot uses AI. Check for mistakes.
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs
rmarinhoand others added 2 commits March 20, 2026 01:31
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>
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>
rmarinhoand others added 7 commits March 20, 2026 01:31
…tRule
- 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>
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>
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>
…ble 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>
…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>
- 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>
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 8e65808 to 733b982CompareMarch 20, 2026 01:31
jonathanpeppersand others added 2 commits March 20, 2026 15:39
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>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Small follow-up request: add forward port management as the symmetric pair to the reverse-port methods this PR introduced. The AdbPortSpec / AdbPortRule / AdbProtocol types are already in place; forward is just four new methods reusing them.

publicpartialclassAdbRunner{/// adb -s <serial> forward <local> <remote>publicvirtualTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove <local>publicvirtualTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove-allpublicvirtualTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);/// adb -s <serial> forward --list (filter to matching serial; global output is `<serial> <local> <remote>` per line)publicvirtualTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);}

Why we need this in addition to reverse

adb forward and adb reverse are not interchangeable — they connect opposite directions:

  • reverse <remote-on-device> <local-on-host>: device-side socket forwards to host-side. Used by hot reload (the device app connects to a "device" port that's actually tunnelled to the IDE host).
  • forward <local-on-host> <remote-on-device>: host-side socket forwards to device-side. Used when the IDE / harness needs to connect to a service running on the device — debugger attach via JDWP (forward tcp:N jdwp:<pid>), DevFlow agent connect when the agent listens on a device port and the host needs a stable host-side port to reach it, performance-tracing endpoints exposed by the runtime, etc.

Consumers

  • VS Code MAUI extension ServiceHub→CLI migration — MauiAndroidPlatform.tsforwardPort() (debugger configurations, perf tooling).
  • MAUI DevTools CLI (dotnet/maui-labs) — maui android port forward … group, sibling of the existing reverse surface (maui-labs#197).
  • Visual Studio — same ClientTools.Platform paths that drive reverse today have parallel forward call-sites.

Happy to send the PR if a maintainer is okay with this scope landing as a direct follow-up to #305.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Sent the forward-port follow-up as PR #351 (draft).

rmarinho added a commit to rmarinho/android-tools that referenced this pull request May 5, 2026
Adds the symmetric forward-port pair to the reverse-port methods that landed
in dotnet#305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet#305 (comment)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 2, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jul 13, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet/android-tools#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants

@rmarinho@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add ADB reverse port forwarding support - #305

Merged
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port
Mar 23, 2026
Merged

Add ADB reverse port forwarding support#305
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port

Conversation

@rmarinho

@rmarinhormarinho commented Mar 13, 2026

Copy link
Copy Markdown
Member

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) — raw ADB wire protocol via TransportRunCommandWithStatus
  2. VS Code Extension (vscode-maui) — ServiceHub JSON-RPC to C# AdbServer
  3. Manual CLI — no shared C# implementation

Each IDE reimplements the same ADB operations. This PR adds a shared, CLI-based implementation in android-tools.

New API Surface

Types

publicenumAdbProtocol{Tcp,LocalAbstract,LocalReserved,LocalFilesystem}publicrecordAdbPortSpec(AdbProtocolProtocol,intPort){publicstringToSocketSpec();// e.g. "tcp:5000"publicstaticAdbPortSpec?TryParse(stringspec);// parse "tcp:5000" → AdbPortSpec}publicrecordAdbPortRule(AdbPortSpecRemote,AdbPortSpecLocal);

AdbRunner Methods

MethodADB CommandPurpose
ReversePortAsync(serial, AdbPortSpec remote, AdbPortSpec local, ct)adb reverse <remote> <local>Set up reverse forwarding
RemoveReversePortAsync(serial, AdbPortSpec remote, ct)adb reverse --remove <remote>Remove a specific rule
RemoveAllReversePortsAsync(serial, ct)adb reverse --remove-allRemove all rules
ListReversePortsAsync(serial, ct)adb reverse --listList active rules

Usage

vartcp5000=newAdbPortSpec(AdbProtocol.Tcp,5000);// Set up hot-reload tunnelawaitadbRunner.ReversePortAsync("emulator-5554",tcp5000,tcp5000);// List active rulesIReadOnlyList<AdbPortRule>rules=awaitadbRunner.ListReversePortsAsync("emulator-5554");// Clean upawaitadbRunner.RemoveReversePortAsync("emulator-5554",tcp5000);awaitadbRunner.RemoveAllReversePortsAsync("emulator-5554");

Key Design Decisions

  1. Strongly-typed AdbPortSpec — enum + int instead of raw strings like "tcp:5000", per @jonathanpeppers review
  2. CLI-based, not wire protocol — simpler to maintain and test, functionally equivalent
  3. Single overload per method — only AdbPortSpec, no string or int convenience overloads
  4. AdbPortRule record — value equality, structural matching on list output
  5. stdout included in error diagnosticsListReversePortsAsync passes both stdout and stderr to ThrowIfFailed

Tests

22 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbPortSpec.TryParse: valid TCP/LocalAbstract/LocalReserved/LocalFilesystem, null, empty, no colon, non-numeric port, zero port, unknown protocol
  • Parameter validation: empty serial, null remote/local (ArgumentNullException)
  • RemoveAllReversePortsAsync: empty serial validation
  • ListReversePortsAsync: empty serial validation

Review Feedback Addressed

ReviewerFeedbackResolution
@jonathanpeppersUse strongly-typed enum+int, not strings like "tcp:5000"AdbPortSpec(AdbProtocol, int) — single overload
@jonathanpeppersToo many overloads (string, int, AdbPortSpec)✅ Removed string and int overloads, kept only AdbPortSpec
@jonathanpeppersShould support adb forward too — rename to AdbPortRule✅ Renamed from AdbReversePortRule to AdbPortRule
CopilotInclude stdout in error diagnostics for ListReversePortsAsync✅ Pass stdout to ThrowIfFailed

Migration Path

Phase 1 (this PR): Shared API in android-tools ← HERE
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

Related

CopilotAI review requested due to automatic review settings March 13, 2026 09:20

This comment was marked as outdated.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch 2 times, most recently from b4bba1e to 3075d2cCompareMarch 19, 2026 14:22
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 40512c4 to 798b585CompareMarch 19, 2026 17:19
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 17:28

This comment was marked as outdated.

rmarinho added a commit that referenced this pull request Mar 19, 2026
…loads, stdout in errors
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 18:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, enabling downstream tooling (e.g., MAUI DevTools CLI) to manage hot-reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule).
  • Adds AdbRunner APIs for reverse port forwarding management (reverse, --remove, --remove-all, --list) plus list-output parsing.
  • Extends unit tests to cover parsing, AdbPortSpec parsing/formatting, and parameter validation; updates PublicAPI unshipped files.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec parsing/formatting, and new API parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds reverse port forwarding methods and adb reverse --list output parsing helper.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a forwarding rule (remote/local).
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for net10.0.

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

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, intended to be consumed by MAUI DevTools CLI (and later VS Code / VS) to manage Hot Reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed reverse port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule) and adds reverse operations to AdbRunner.
  • Implements parsing for adb reverse --list output.
  • Adds unit tests covering parsing, round-tripping, value equality, and parameter validation; updates PublicAPI unshipped files for both TFMs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse-list parsing, AdbPortSpec parsing/formatting, AdbPortRule equality, and new API argument validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds ReversePortAsync, removal APIs, list API, and adb reverse --list output parser.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a reverse/forward rule pair.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 Xamarin.Android.Tools.AndroidSdk’s AdbRunner, enabling downstream tools (e.g., MAUI DevTools CLI) to manage adb reverse tunnels via the adb CLI.

Changes:

  • Added new public types (AdbProtocol, AdbPortSpec, AdbPortRule) for strongly-typed reverse/forward socket specs and rules.
  • Added AdbRunner APIs for adb reverse operations: add rule, remove rule, remove all, and list rules (with list output parsing).
  • Added unit tests covering list parsing, AdbPortSpec parsing/formatting, and parameter validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec/AdbPortRule, and parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csImplements adb reverse command wrappers and a parser for adb reverse --list output.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csIntroduces AdbProtocol enum (currently TCP only).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csIntroduces AdbPortSpec record with ToSocketSpec() + TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csIntroduces AdbPortRule record representing a reverse/forward rule.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs

var rules = AdbRunner.ParseReverseListOutput (output);

// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped

CopilotAIMar 19, 2026

Copy link

Choose a reason for hiding this comment

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

This comment is a bit misleading: localabstract:... isn’t a “non-numeric port” (it’s a named socket). Consider rewording to clarify that the spec is skipped because AdbPortSpec.TryParse only supports numeric tcp:<port> at the moment.

Suggested change
// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped
// localabstract:chrome_devtools_remote is a named localabstract socket, not a tcp:<port>, so TryParse skips it

Copilot uses AI. Check for mistakes.
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs
rmarinhoand others added 2 commits March 20, 2026 01:31
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>
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>
rmarinhoand others added 7 commits March 20, 2026 01:31
…tRule
- 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>
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>
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>
…ble 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>
…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>
- 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>
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 8e65808 to 733b982CompareMarch 20, 2026 01:31
jonathanpeppersand others added 2 commits March 20, 2026 15:39
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>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Small follow-up request: add forward port management as the symmetric pair to the reverse-port methods this PR introduced. The AdbPortSpec / AdbPortRule / AdbProtocol types are already in place; forward is just four new methods reusing them.

publicpartialclassAdbRunner{/// adb -s <serial> forward <local> <remote>publicvirtualTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove <local>publicvirtualTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove-allpublicvirtualTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);/// adb -s <serial> forward --list (filter to matching serial; global output is `<serial> <local> <remote>` per line)publicvirtualTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);}

Why we need this in addition to reverse

adb forward and adb reverse are not interchangeable — they connect opposite directions:

  • reverse <remote-on-device> <local-on-host>: device-side socket forwards to host-side. Used by hot reload (the device app connects to a "device" port that's actually tunnelled to the IDE host).
  • forward <local-on-host> <remote-on-device>: host-side socket forwards to device-side. Used when the IDE / harness needs to connect to a service running on the device — debugger attach via JDWP (forward tcp:N jdwp:<pid>), DevFlow agent connect when the agent listens on a device port and the host needs a stable host-side port to reach it, performance-tracing endpoints exposed by the runtime, etc.

Consumers

  • VS Code MAUI extension ServiceHub→CLI migration — MauiAndroidPlatform.tsforwardPort() (debugger configurations, perf tooling).
  • MAUI DevTools CLI (dotnet/maui-labs) — maui android port forward … group, sibling of the existing reverse surface (maui-labs#197).
  • Visual Studio — same ClientTools.Platform paths that drive reverse today have parallel forward call-sites.

Happy to send the PR if a maintainer is okay with this scope landing as a direct follow-up to #305.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Sent the forward-port follow-up as PR #351 (draft).

rmarinho added a commit to rmarinho/android-tools that referenced this pull request May 5, 2026
Adds the symmetric forward-port pair to the reverse-port methods that landed
in dotnet#305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet#305 (comment)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 2, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jul 13, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet/android-tools#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants

@rmarinho@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add ADB reverse port forwarding support - #305

Merged
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port
Mar 23, 2026
Merged

Add ADB reverse port forwarding support#305
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port

Conversation

@rmarinho

@rmarinhormarinho commented Mar 13, 2026

Copy link
Copy Markdown
Member

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) — raw ADB wire protocol via TransportRunCommandWithStatus
  2. VS Code Extension (vscode-maui) — ServiceHub JSON-RPC to C# AdbServer
  3. Manual CLI — no shared C# implementation

Each IDE reimplements the same ADB operations. This PR adds a shared, CLI-based implementation in android-tools.

New API Surface

Types

publicenumAdbProtocol{Tcp,LocalAbstract,LocalReserved,LocalFilesystem}publicrecordAdbPortSpec(AdbProtocolProtocol,intPort){publicstringToSocketSpec();// e.g. "tcp:5000"publicstaticAdbPortSpec?TryParse(stringspec);// parse "tcp:5000" → AdbPortSpec}publicrecordAdbPortRule(AdbPortSpecRemote,AdbPortSpecLocal);

AdbRunner Methods

MethodADB CommandPurpose
ReversePortAsync(serial, AdbPortSpec remote, AdbPortSpec local, ct)adb reverse <remote> <local>Set up reverse forwarding
RemoveReversePortAsync(serial, AdbPortSpec remote, ct)adb reverse --remove <remote>Remove a specific rule
RemoveAllReversePortsAsync(serial, ct)adb reverse --remove-allRemove all rules
ListReversePortsAsync(serial, ct)adb reverse --listList active rules

Usage

vartcp5000=newAdbPortSpec(AdbProtocol.Tcp,5000);// Set up hot-reload tunnelawaitadbRunner.ReversePortAsync("emulator-5554",tcp5000,tcp5000);// List active rulesIReadOnlyList<AdbPortRule>rules=awaitadbRunner.ListReversePortsAsync("emulator-5554");// Clean upawaitadbRunner.RemoveReversePortAsync("emulator-5554",tcp5000);awaitadbRunner.RemoveAllReversePortsAsync("emulator-5554");

Key Design Decisions

  1. Strongly-typed AdbPortSpec — enum + int instead of raw strings like "tcp:5000", per @jonathanpeppers review
  2. CLI-based, not wire protocol — simpler to maintain and test, functionally equivalent
  3. Single overload per method — only AdbPortSpec, no string or int convenience overloads
  4. AdbPortRule record — value equality, structural matching on list output
  5. stdout included in error diagnosticsListReversePortsAsync passes both stdout and stderr to ThrowIfFailed

Tests

22 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbPortSpec.TryParse: valid TCP/LocalAbstract/LocalReserved/LocalFilesystem, null, empty, no colon, non-numeric port, zero port, unknown protocol
  • Parameter validation: empty serial, null remote/local (ArgumentNullException)
  • RemoveAllReversePortsAsync: empty serial validation
  • ListReversePortsAsync: empty serial validation

Review Feedback Addressed

ReviewerFeedbackResolution
@jonathanpeppersUse strongly-typed enum+int, not strings like "tcp:5000"AdbPortSpec(AdbProtocol, int) — single overload
@jonathanpeppersToo many overloads (string, int, AdbPortSpec)✅ Removed string and int overloads, kept only AdbPortSpec
@jonathanpeppersShould support adb forward too — rename to AdbPortRule✅ Renamed from AdbReversePortRule to AdbPortRule
CopilotInclude stdout in error diagnostics for ListReversePortsAsync✅ Pass stdout to ThrowIfFailed

Migration Path

Phase 1 (this PR): Shared API in android-tools ← HERE
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

Related

CopilotAI review requested due to automatic review settings March 13, 2026 09:20

This comment was marked as outdated.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch 2 times, most recently from b4bba1e to 3075d2cCompareMarch 19, 2026 14:22
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 40512c4 to 798b585CompareMarch 19, 2026 17:19
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 17:28

This comment was marked as outdated.

rmarinho added a commit that referenced this pull request Mar 19, 2026
…loads, stdout in errors
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 18:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, enabling downstream tooling (e.g., MAUI DevTools CLI) to manage hot-reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule).
  • Adds AdbRunner APIs for reverse port forwarding management (reverse, --remove, --remove-all, --list) plus list-output parsing.
  • Extends unit tests to cover parsing, AdbPortSpec parsing/formatting, and parameter validation; updates PublicAPI unshipped files.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec parsing/formatting, and new API parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds reverse port forwarding methods and adb reverse --list output parsing helper.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a forwarding rule (remote/local).
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for net10.0.

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

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, intended to be consumed by MAUI DevTools CLI (and later VS Code / VS) to manage Hot Reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed reverse port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule) and adds reverse operations to AdbRunner.
  • Implements parsing for adb reverse --list output.
  • Adds unit tests covering parsing, round-tripping, value equality, and parameter validation; updates PublicAPI unshipped files for both TFMs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse-list parsing, AdbPortSpec parsing/formatting, AdbPortRule equality, and new API argument validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds ReversePortAsync, removal APIs, list API, and adb reverse --list output parser.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a reverse/forward rule pair.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 Xamarin.Android.Tools.AndroidSdk’s AdbRunner, enabling downstream tools (e.g., MAUI DevTools CLI) to manage adb reverse tunnels via the adb CLI.

Changes:

  • Added new public types (AdbProtocol, AdbPortSpec, AdbPortRule) for strongly-typed reverse/forward socket specs and rules.
  • Added AdbRunner APIs for adb reverse operations: add rule, remove rule, remove all, and list rules (with list output parsing).
  • Added unit tests covering list parsing, AdbPortSpec parsing/formatting, and parameter validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec/AdbPortRule, and parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csImplements adb reverse command wrappers and a parser for adb reverse --list output.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csIntroduces AdbProtocol enum (currently TCP only).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csIntroduces AdbPortSpec record with ToSocketSpec() + TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csIntroduces AdbPortRule record representing a reverse/forward rule.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs

var rules = AdbRunner.ParseReverseListOutput (output);

// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped

CopilotAIMar 19, 2026

Copy link

Choose a reason for hiding this comment

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

This comment is a bit misleading: localabstract:... isn’t a “non-numeric port” (it’s a named socket). Consider rewording to clarify that the spec is skipped because AdbPortSpec.TryParse only supports numeric tcp:<port> at the moment.

Suggested change
// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped
// localabstract:chrome_devtools_remote is a named localabstract socket, not a tcp:<port>, so TryParse skips it

Copilot uses AI. Check for mistakes.
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs
rmarinhoand others added 2 commits March 20, 2026 01:31
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>
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>
rmarinhoand others added 7 commits March 20, 2026 01:31
…tRule
- 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>
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>
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>
…ble 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>
…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>
- 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>
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 8e65808 to 733b982CompareMarch 20, 2026 01:31
jonathanpeppersand others added 2 commits March 20, 2026 15:39
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>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Small follow-up request: add forward port management as the symmetric pair to the reverse-port methods this PR introduced. The AdbPortSpec / AdbPortRule / AdbProtocol types are already in place; forward is just four new methods reusing them.

publicpartialclassAdbRunner{/// adb -s <serial> forward <local> <remote>publicvirtualTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove <local>publicvirtualTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove-allpublicvirtualTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);/// adb -s <serial> forward --list (filter to matching serial; global output is `<serial> <local> <remote>` per line)publicvirtualTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);}

Why we need this in addition to reverse

adb forward and adb reverse are not interchangeable — they connect opposite directions:

  • reverse <remote-on-device> <local-on-host>: device-side socket forwards to host-side. Used by hot reload (the device app connects to a "device" port that's actually tunnelled to the IDE host).
  • forward <local-on-host> <remote-on-device>: host-side socket forwards to device-side. Used when the IDE / harness needs to connect to a service running on the device — debugger attach via JDWP (forward tcp:N jdwp:<pid>), DevFlow agent connect when the agent listens on a device port and the host needs a stable host-side port to reach it, performance-tracing endpoints exposed by the runtime, etc.

Consumers

  • VS Code MAUI extension ServiceHub→CLI migration — MauiAndroidPlatform.tsforwardPort() (debugger configurations, perf tooling).
  • MAUI DevTools CLI (dotnet/maui-labs) — maui android port forward … group, sibling of the existing reverse surface (maui-labs#197).
  • Visual Studio — same ClientTools.Platform paths that drive reverse today have parallel forward call-sites.

Happy to send the PR if a maintainer is okay with this scope landing as a direct follow-up to #305.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Sent the forward-port follow-up as PR #351 (draft).

rmarinho added a commit to rmarinho/android-tools that referenced this pull request May 5, 2026
Adds the symmetric forward-port pair to the reverse-port methods that landed
in dotnet#305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet#305 (comment)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 2, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jul 13, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet/android-tools#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants

@rmarinho@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add ADB reverse port forwarding support - #305

Merged
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port
Mar 23, 2026
Merged

Add ADB reverse port forwarding support#305
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port

Conversation

@rmarinho

@rmarinhormarinho commented Mar 13, 2026

Copy link
Copy Markdown
Member

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) — raw ADB wire protocol via TransportRunCommandWithStatus
  2. VS Code Extension (vscode-maui) — ServiceHub JSON-RPC to C# AdbServer
  3. Manual CLI — no shared C# implementation

Each IDE reimplements the same ADB operations. This PR adds a shared, CLI-based implementation in android-tools.

New API Surface

Types

publicenumAdbProtocol{Tcp,LocalAbstract,LocalReserved,LocalFilesystem}publicrecordAdbPortSpec(AdbProtocolProtocol,intPort){publicstringToSocketSpec();// e.g. "tcp:5000"publicstaticAdbPortSpec?TryParse(stringspec);// parse "tcp:5000" → AdbPortSpec}publicrecordAdbPortRule(AdbPortSpecRemote,AdbPortSpecLocal);

AdbRunner Methods

MethodADB CommandPurpose
ReversePortAsync(serial, AdbPortSpec remote, AdbPortSpec local, ct)adb reverse <remote> <local>Set up reverse forwarding
RemoveReversePortAsync(serial, AdbPortSpec remote, ct)adb reverse --remove <remote>Remove a specific rule
RemoveAllReversePortsAsync(serial, ct)adb reverse --remove-allRemove all rules
ListReversePortsAsync(serial, ct)adb reverse --listList active rules

Usage

vartcp5000=newAdbPortSpec(AdbProtocol.Tcp,5000);// Set up hot-reload tunnelawaitadbRunner.ReversePortAsync("emulator-5554",tcp5000,tcp5000);// List active rulesIReadOnlyList<AdbPortRule>rules=awaitadbRunner.ListReversePortsAsync("emulator-5554");// Clean upawaitadbRunner.RemoveReversePortAsync("emulator-5554",tcp5000);awaitadbRunner.RemoveAllReversePortsAsync("emulator-5554");

Key Design Decisions

  1. Strongly-typed AdbPortSpec — enum + int instead of raw strings like "tcp:5000", per @jonathanpeppers review
  2. CLI-based, not wire protocol — simpler to maintain and test, functionally equivalent
  3. Single overload per method — only AdbPortSpec, no string or int convenience overloads
  4. AdbPortRule record — value equality, structural matching on list output
  5. stdout included in error diagnosticsListReversePortsAsync passes both stdout and stderr to ThrowIfFailed

Tests

22 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbPortSpec.TryParse: valid TCP/LocalAbstract/LocalReserved/LocalFilesystem, null, empty, no colon, non-numeric port, zero port, unknown protocol
  • Parameter validation: empty serial, null remote/local (ArgumentNullException)
  • RemoveAllReversePortsAsync: empty serial validation
  • ListReversePortsAsync: empty serial validation

Review Feedback Addressed

ReviewerFeedbackResolution
@jonathanpeppersUse strongly-typed enum+int, not strings like "tcp:5000"AdbPortSpec(AdbProtocol, int) — single overload
@jonathanpeppersToo many overloads (string, int, AdbPortSpec)✅ Removed string and int overloads, kept only AdbPortSpec
@jonathanpeppersShould support adb forward too — rename to AdbPortRule✅ Renamed from AdbReversePortRule to AdbPortRule
CopilotInclude stdout in error diagnostics for ListReversePortsAsync✅ Pass stdout to ThrowIfFailed

Migration Path

Phase 1 (this PR): Shared API in android-tools ← HERE
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

Related

CopilotAI review requested due to automatic review settings March 13, 2026 09:20

This comment was marked as outdated.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch 2 times, most recently from b4bba1e to 3075d2cCompareMarch 19, 2026 14:22
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 40512c4 to 798b585CompareMarch 19, 2026 17:19
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 17:28

This comment was marked as outdated.

rmarinho added a commit that referenced this pull request Mar 19, 2026
…loads, stdout in errors
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 18:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, enabling downstream tooling (e.g., MAUI DevTools CLI) to manage hot-reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule).
  • Adds AdbRunner APIs for reverse port forwarding management (reverse, --remove, --remove-all, --list) plus list-output parsing.
  • Extends unit tests to cover parsing, AdbPortSpec parsing/formatting, and parameter validation; updates PublicAPI unshipped files.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec parsing/formatting, and new API parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds reverse port forwarding methods and adb reverse --list output parsing helper.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a forwarding rule (remote/local).
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for net10.0.

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

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, intended to be consumed by MAUI DevTools CLI (and later VS Code / VS) to manage Hot Reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed reverse port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule) and adds reverse operations to AdbRunner.
  • Implements parsing for adb reverse --list output.
  • Adds unit tests covering parsing, round-tripping, value equality, and parameter validation; updates PublicAPI unshipped files for both TFMs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse-list parsing, AdbPortSpec parsing/formatting, AdbPortRule equality, and new API argument validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds ReversePortAsync, removal APIs, list API, and adb reverse --list output parser.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a reverse/forward rule pair.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 Xamarin.Android.Tools.AndroidSdk’s AdbRunner, enabling downstream tools (e.g., MAUI DevTools CLI) to manage adb reverse tunnels via the adb CLI.

Changes:

  • Added new public types (AdbProtocol, AdbPortSpec, AdbPortRule) for strongly-typed reverse/forward socket specs and rules.
  • Added AdbRunner APIs for adb reverse operations: add rule, remove rule, remove all, and list rules (with list output parsing).
  • Added unit tests covering list parsing, AdbPortSpec parsing/formatting, and parameter validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec/AdbPortRule, and parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csImplements adb reverse command wrappers and a parser for adb reverse --list output.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csIntroduces AdbProtocol enum (currently TCP only).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csIntroduces AdbPortSpec record with ToSocketSpec() + TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csIntroduces AdbPortRule record representing a reverse/forward rule.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs

var rules = AdbRunner.ParseReverseListOutput (output);

// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped

CopilotAIMar 19, 2026

Copy link

Choose a reason for hiding this comment

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

This comment is a bit misleading: localabstract:... isn’t a “non-numeric port” (it’s a named socket). Consider rewording to clarify that the spec is skipped because AdbPortSpec.TryParse only supports numeric tcp:<port> at the moment.

Suggested change
// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped
// localabstract:chrome_devtools_remote is a named localabstract socket, not a tcp:<port>, so TryParse skips it

Copilot uses AI. Check for mistakes.
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs
rmarinhoand others added 2 commits March 20, 2026 01:31
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>
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>
rmarinhoand others added 7 commits March 20, 2026 01:31
…tRule
- 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>
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>
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>
…ble 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>
…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>
- 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>
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 8e65808 to 733b982CompareMarch 20, 2026 01:31
jonathanpeppersand others added 2 commits March 20, 2026 15:39
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>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Small follow-up request: add forward port management as the symmetric pair to the reverse-port methods this PR introduced. The AdbPortSpec / AdbPortRule / AdbProtocol types are already in place; forward is just four new methods reusing them.

publicpartialclassAdbRunner{/// adb -s <serial> forward <local> <remote>publicvirtualTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove <local>publicvirtualTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove-allpublicvirtualTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);/// adb -s <serial> forward --list (filter to matching serial; global output is `<serial> <local> <remote>` per line)publicvirtualTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);}

Why we need this in addition to reverse

adb forward and adb reverse are not interchangeable — they connect opposite directions:

  • reverse <remote-on-device> <local-on-host>: device-side socket forwards to host-side. Used by hot reload (the device app connects to a "device" port that's actually tunnelled to the IDE host).
  • forward <local-on-host> <remote-on-device>: host-side socket forwards to device-side. Used when the IDE / harness needs to connect to a service running on the device — debugger attach via JDWP (forward tcp:N jdwp:<pid>), DevFlow agent connect when the agent listens on a device port and the host needs a stable host-side port to reach it, performance-tracing endpoints exposed by the runtime, etc.

Consumers

  • VS Code MAUI extension ServiceHub→CLI migration — MauiAndroidPlatform.tsforwardPort() (debugger configurations, perf tooling).
  • MAUI DevTools CLI (dotnet/maui-labs) — maui android port forward … group, sibling of the existing reverse surface (maui-labs#197).
  • Visual Studio — same ClientTools.Platform paths that drive reverse today have parallel forward call-sites.

Happy to send the PR if a maintainer is okay with this scope landing as a direct follow-up to #305.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Sent the forward-port follow-up as PR #351 (draft).

rmarinho added a commit to rmarinho/android-tools that referenced this pull request May 5, 2026
Adds the symmetric forward-port pair to the reverse-port methods that landed
in dotnet#305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet#305 (comment)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 2, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jul 13, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet/android-tools#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants

@rmarinho@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add ADB reverse port forwarding support - #305

Merged
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port
Mar 23, 2026
Merged

Add ADB reverse port forwarding support#305
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port

Conversation

@rmarinho

@rmarinhormarinho commented Mar 13, 2026

Copy link
Copy Markdown
Member

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) — raw ADB wire protocol via TransportRunCommandWithStatus
  2. VS Code Extension (vscode-maui) — ServiceHub JSON-RPC to C# AdbServer
  3. Manual CLI — no shared C# implementation

Each IDE reimplements the same ADB operations. This PR adds a shared, CLI-based implementation in android-tools.

New API Surface

Types

publicenumAdbProtocol{Tcp,LocalAbstract,LocalReserved,LocalFilesystem}publicrecordAdbPortSpec(AdbProtocolProtocol,intPort){publicstringToSocketSpec();// e.g. "tcp:5000"publicstaticAdbPortSpec?TryParse(stringspec);// parse "tcp:5000" → AdbPortSpec}publicrecordAdbPortRule(AdbPortSpecRemote,AdbPortSpecLocal);

AdbRunner Methods

MethodADB CommandPurpose
ReversePortAsync(serial, AdbPortSpec remote, AdbPortSpec local, ct)adb reverse <remote> <local>Set up reverse forwarding
RemoveReversePortAsync(serial, AdbPortSpec remote, ct)adb reverse --remove <remote>Remove a specific rule
RemoveAllReversePortsAsync(serial, ct)adb reverse --remove-allRemove all rules
ListReversePortsAsync(serial, ct)adb reverse --listList active rules

Usage

vartcp5000=newAdbPortSpec(AdbProtocol.Tcp,5000);// Set up hot-reload tunnelawaitadbRunner.ReversePortAsync("emulator-5554",tcp5000,tcp5000);// List active rulesIReadOnlyList<AdbPortRule>rules=awaitadbRunner.ListReversePortsAsync("emulator-5554");// Clean upawaitadbRunner.RemoveReversePortAsync("emulator-5554",tcp5000);awaitadbRunner.RemoveAllReversePortsAsync("emulator-5554");

Key Design Decisions

  1. Strongly-typed AdbPortSpec — enum + int instead of raw strings like "tcp:5000", per @jonathanpeppers review
  2. CLI-based, not wire protocol — simpler to maintain and test, functionally equivalent
  3. Single overload per method — only AdbPortSpec, no string or int convenience overloads
  4. AdbPortRule record — value equality, structural matching on list output
  5. stdout included in error diagnosticsListReversePortsAsync passes both stdout and stderr to ThrowIfFailed

Tests

22 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbPortSpec.TryParse: valid TCP/LocalAbstract/LocalReserved/LocalFilesystem, null, empty, no colon, non-numeric port, zero port, unknown protocol
  • Parameter validation: empty serial, null remote/local (ArgumentNullException)
  • RemoveAllReversePortsAsync: empty serial validation
  • ListReversePortsAsync: empty serial validation

Review Feedback Addressed

ReviewerFeedbackResolution
@jonathanpeppersUse strongly-typed enum+int, not strings like "tcp:5000"AdbPortSpec(AdbProtocol, int) — single overload
@jonathanpeppersToo many overloads (string, int, AdbPortSpec)✅ Removed string and int overloads, kept only AdbPortSpec
@jonathanpeppersShould support adb forward too — rename to AdbPortRule✅ Renamed from AdbReversePortRule to AdbPortRule
CopilotInclude stdout in error diagnostics for ListReversePortsAsync✅ Pass stdout to ThrowIfFailed

Migration Path

Phase 1 (this PR): Shared API in android-tools ← HERE
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

Related

CopilotAI review requested due to automatic review settings March 13, 2026 09:20

This comment was marked as outdated.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch 2 times, most recently from b4bba1e to 3075d2cCompareMarch 19, 2026 14:22
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 40512c4 to 798b585CompareMarch 19, 2026 17:19
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 17:28

This comment was marked as outdated.

rmarinho added a commit that referenced this pull request Mar 19, 2026
…loads, stdout in errors
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 18:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, enabling downstream tooling (e.g., MAUI DevTools CLI) to manage hot-reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule).
  • Adds AdbRunner APIs for reverse port forwarding management (reverse, --remove, --remove-all, --list) plus list-output parsing.
  • Extends unit tests to cover parsing, AdbPortSpec parsing/formatting, and parameter validation; updates PublicAPI unshipped files.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec parsing/formatting, and new API parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds reverse port forwarding methods and adb reverse --list output parsing helper.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a forwarding rule (remote/local).
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for net10.0.

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

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, intended to be consumed by MAUI DevTools CLI (and later VS Code / VS) to manage Hot Reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed reverse port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule) and adds reverse operations to AdbRunner.
  • Implements parsing for adb reverse --list output.
  • Adds unit tests covering parsing, round-tripping, value equality, and parameter validation; updates PublicAPI unshipped files for both TFMs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse-list parsing, AdbPortSpec parsing/formatting, AdbPortRule equality, and new API argument validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds ReversePortAsync, removal APIs, list API, and adb reverse --list output parser.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a reverse/forward rule pair.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 Xamarin.Android.Tools.AndroidSdk’s AdbRunner, enabling downstream tools (e.g., MAUI DevTools CLI) to manage adb reverse tunnels via the adb CLI.

Changes:

  • Added new public types (AdbProtocol, AdbPortSpec, AdbPortRule) for strongly-typed reverse/forward socket specs and rules.
  • Added AdbRunner APIs for adb reverse operations: add rule, remove rule, remove all, and list rules (with list output parsing).
  • Added unit tests covering list parsing, AdbPortSpec parsing/formatting, and parameter validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec/AdbPortRule, and parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csImplements adb reverse command wrappers and a parser for adb reverse --list output.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csIntroduces AdbProtocol enum (currently TCP only).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csIntroduces AdbPortSpec record with ToSocketSpec() + TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csIntroduces AdbPortRule record representing a reverse/forward rule.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs

var rules = AdbRunner.ParseReverseListOutput (output);

// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped

CopilotAIMar 19, 2026

Copy link

Choose a reason for hiding this comment

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

This comment is a bit misleading: localabstract:... isn’t a “non-numeric port” (it’s a named socket). Consider rewording to clarify that the spec is skipped because AdbPortSpec.TryParse only supports numeric tcp:<port> at the moment.

Suggested change
// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped
// localabstract:chrome_devtools_remote is a named localabstract socket, not a tcp:<port>, so TryParse skips it

Copilot uses AI. Check for mistakes.
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs
rmarinhoand others added 2 commits March 20, 2026 01:31
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>
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>
rmarinhoand others added 7 commits March 20, 2026 01:31
…tRule
- 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>
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>
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>
…ble 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>
…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>
- 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>
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 8e65808 to 733b982CompareMarch 20, 2026 01:31
jonathanpeppersand others added 2 commits March 20, 2026 15:39
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>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Small follow-up request: add forward port management as the symmetric pair to the reverse-port methods this PR introduced. The AdbPortSpec / AdbPortRule / AdbProtocol types are already in place; forward is just four new methods reusing them.

publicpartialclassAdbRunner{/// adb -s <serial> forward <local> <remote>publicvirtualTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove <local>publicvirtualTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove-allpublicvirtualTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);/// adb -s <serial> forward --list (filter to matching serial; global output is `<serial> <local> <remote>` per line)publicvirtualTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);}

Why we need this in addition to reverse

adb forward and adb reverse are not interchangeable — they connect opposite directions:

  • reverse <remote-on-device> <local-on-host>: device-side socket forwards to host-side. Used by hot reload (the device app connects to a "device" port that's actually tunnelled to the IDE host).
  • forward <local-on-host> <remote-on-device>: host-side socket forwards to device-side. Used when the IDE / harness needs to connect to a service running on the device — debugger attach via JDWP (forward tcp:N jdwp:<pid>), DevFlow agent connect when the agent listens on a device port and the host needs a stable host-side port to reach it, performance-tracing endpoints exposed by the runtime, etc.

Consumers

  • VS Code MAUI extension ServiceHub→CLI migration — MauiAndroidPlatform.tsforwardPort() (debugger configurations, perf tooling).
  • MAUI DevTools CLI (dotnet/maui-labs) — maui android port forward … group, sibling of the existing reverse surface (maui-labs#197).
  • Visual Studio — same ClientTools.Platform paths that drive reverse today have parallel forward call-sites.

Happy to send the PR if a maintainer is okay with this scope landing as a direct follow-up to #305.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Sent the forward-port follow-up as PR #351 (draft).

rmarinho added a commit to rmarinho/android-tools that referenced this pull request May 5, 2026
Adds the symmetric forward-port pair to the reverse-port methods that landed
in dotnet#305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet#305 (comment)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 2, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jul 13, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet/android-tools#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants

@rmarinho@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add ADB reverse port forwarding support - #305

Merged
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port
Mar 23, 2026
Merged

Add ADB reverse port forwarding support#305
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port

Conversation

@rmarinho

@rmarinhormarinho commented Mar 13, 2026

Copy link
Copy Markdown
Member

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) — raw ADB wire protocol via TransportRunCommandWithStatus
  2. VS Code Extension (vscode-maui) — ServiceHub JSON-RPC to C# AdbServer
  3. Manual CLI — no shared C# implementation

Each IDE reimplements the same ADB operations. This PR adds a shared, CLI-based implementation in android-tools.

New API Surface

Types

publicenumAdbProtocol{Tcp,LocalAbstract,LocalReserved,LocalFilesystem}publicrecordAdbPortSpec(AdbProtocolProtocol,intPort){publicstringToSocketSpec();// e.g. "tcp:5000"publicstaticAdbPortSpec?TryParse(stringspec);// parse "tcp:5000" → AdbPortSpec}publicrecordAdbPortRule(AdbPortSpecRemote,AdbPortSpecLocal);

AdbRunner Methods

MethodADB CommandPurpose
ReversePortAsync(serial, AdbPortSpec remote, AdbPortSpec local, ct)adb reverse <remote> <local>Set up reverse forwarding
RemoveReversePortAsync(serial, AdbPortSpec remote, ct)adb reverse --remove <remote>Remove a specific rule
RemoveAllReversePortsAsync(serial, ct)adb reverse --remove-allRemove all rules
ListReversePortsAsync(serial, ct)adb reverse --listList active rules

Usage

vartcp5000=newAdbPortSpec(AdbProtocol.Tcp,5000);// Set up hot-reload tunnelawaitadbRunner.ReversePortAsync("emulator-5554",tcp5000,tcp5000);// List active rulesIReadOnlyList<AdbPortRule>rules=awaitadbRunner.ListReversePortsAsync("emulator-5554");// Clean upawaitadbRunner.RemoveReversePortAsync("emulator-5554",tcp5000);awaitadbRunner.RemoveAllReversePortsAsync("emulator-5554");

Key Design Decisions

  1. Strongly-typed AdbPortSpec — enum + int instead of raw strings like "tcp:5000", per @jonathanpeppers review
  2. CLI-based, not wire protocol — simpler to maintain and test, functionally equivalent
  3. Single overload per method — only AdbPortSpec, no string or int convenience overloads
  4. AdbPortRule record — value equality, structural matching on list output
  5. stdout included in error diagnosticsListReversePortsAsync passes both stdout and stderr to ThrowIfFailed

Tests

22 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbPortSpec.TryParse: valid TCP/LocalAbstract/LocalReserved/LocalFilesystem, null, empty, no colon, non-numeric port, zero port, unknown protocol
  • Parameter validation: empty serial, null remote/local (ArgumentNullException)
  • RemoveAllReversePortsAsync: empty serial validation
  • ListReversePortsAsync: empty serial validation

Review Feedback Addressed

ReviewerFeedbackResolution
@jonathanpeppersUse strongly-typed enum+int, not strings like "tcp:5000"AdbPortSpec(AdbProtocol, int) — single overload
@jonathanpeppersToo many overloads (string, int, AdbPortSpec)✅ Removed string and int overloads, kept only AdbPortSpec
@jonathanpeppersShould support adb forward too — rename to AdbPortRule✅ Renamed from AdbReversePortRule to AdbPortRule
CopilotInclude stdout in error diagnostics for ListReversePortsAsync✅ Pass stdout to ThrowIfFailed

Migration Path

Phase 1 (this PR): Shared API in android-tools ← HERE
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

Related

CopilotAI review requested due to automatic review settings March 13, 2026 09:20

This comment was marked as outdated.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch 2 times, most recently from b4bba1e to 3075d2cCompareMarch 19, 2026 14:22
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 40512c4 to 798b585CompareMarch 19, 2026 17:19
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 17:28

This comment was marked as outdated.

rmarinho added a commit that referenced this pull request Mar 19, 2026
…loads, stdout in errors
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 18:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, enabling downstream tooling (e.g., MAUI DevTools CLI) to manage hot-reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule).
  • Adds AdbRunner APIs for reverse port forwarding management (reverse, --remove, --remove-all, --list) plus list-output parsing.
  • Extends unit tests to cover parsing, AdbPortSpec parsing/formatting, and parameter validation; updates PublicAPI unshipped files.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec parsing/formatting, and new API parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds reverse port forwarding methods and adb reverse --list output parsing helper.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a forwarding rule (remote/local).
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for net10.0.

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

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, intended to be consumed by MAUI DevTools CLI (and later VS Code / VS) to manage Hot Reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed reverse port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule) and adds reverse operations to AdbRunner.
  • Implements parsing for adb reverse --list output.
  • Adds unit tests covering parsing, round-tripping, value equality, and parameter validation; updates PublicAPI unshipped files for both TFMs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse-list parsing, AdbPortSpec parsing/formatting, AdbPortRule equality, and new API argument validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds ReversePortAsync, removal APIs, list API, and adb reverse --list output parser.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a reverse/forward rule pair.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 Xamarin.Android.Tools.AndroidSdk’s AdbRunner, enabling downstream tools (e.g., MAUI DevTools CLI) to manage adb reverse tunnels via the adb CLI.

Changes:

  • Added new public types (AdbProtocol, AdbPortSpec, AdbPortRule) for strongly-typed reverse/forward socket specs and rules.
  • Added AdbRunner APIs for adb reverse operations: add rule, remove rule, remove all, and list rules (with list output parsing).
  • Added unit tests covering list parsing, AdbPortSpec parsing/formatting, and parameter validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec/AdbPortRule, and parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csImplements adb reverse command wrappers and a parser for adb reverse --list output.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csIntroduces AdbProtocol enum (currently TCP only).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csIntroduces AdbPortSpec record with ToSocketSpec() + TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csIntroduces AdbPortRule record representing a reverse/forward rule.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs

var rules = AdbRunner.ParseReverseListOutput (output);

// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped

CopilotAIMar 19, 2026

Copy link

Choose a reason for hiding this comment

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

This comment is a bit misleading: localabstract:... isn’t a “non-numeric port” (it’s a named socket). Consider rewording to clarify that the spec is skipped because AdbPortSpec.TryParse only supports numeric tcp:<port> at the moment.

Suggested change
// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped
// localabstract:chrome_devtools_remote is a named localabstract socket, not a tcp:<port>, so TryParse skips it

Copilot uses AI. Check for mistakes.
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs
rmarinhoand others added 2 commits March 20, 2026 01:31
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>
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>
rmarinhoand others added 7 commits March 20, 2026 01:31
…tRule
- 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>
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>
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>
…ble 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>
…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>
- 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>
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 8e65808 to 733b982CompareMarch 20, 2026 01:31
jonathanpeppersand others added 2 commits March 20, 2026 15:39
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>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Small follow-up request: add forward port management as the symmetric pair to the reverse-port methods this PR introduced. The AdbPortSpec / AdbPortRule / AdbProtocol types are already in place; forward is just four new methods reusing them.

publicpartialclassAdbRunner{/// adb -s <serial> forward <local> <remote>publicvirtualTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove <local>publicvirtualTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove-allpublicvirtualTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);/// adb -s <serial> forward --list (filter to matching serial; global output is `<serial> <local> <remote>` per line)publicvirtualTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);}

Why we need this in addition to reverse

adb forward and adb reverse are not interchangeable — they connect opposite directions:

  • reverse <remote-on-device> <local-on-host>: device-side socket forwards to host-side. Used by hot reload (the device app connects to a "device" port that's actually tunnelled to the IDE host).
  • forward <local-on-host> <remote-on-device>: host-side socket forwards to device-side. Used when the IDE / harness needs to connect to a service running on the device — debugger attach via JDWP (forward tcp:N jdwp:<pid>), DevFlow agent connect when the agent listens on a device port and the host needs a stable host-side port to reach it, performance-tracing endpoints exposed by the runtime, etc.

Consumers

  • VS Code MAUI extension ServiceHub→CLI migration — MauiAndroidPlatform.tsforwardPort() (debugger configurations, perf tooling).
  • MAUI DevTools CLI (dotnet/maui-labs) — maui android port forward … group, sibling of the existing reverse surface (maui-labs#197).
  • Visual Studio — same ClientTools.Platform paths that drive reverse today have parallel forward call-sites.

Happy to send the PR if a maintainer is okay with this scope landing as a direct follow-up to #305.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Sent the forward-port follow-up as PR #351 (draft).

rmarinho added a commit to rmarinho/android-tools that referenced this pull request May 5, 2026
Adds the symmetric forward-port pair to the reverse-port methods that landed
in dotnet#305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet#305 (comment)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 2, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jul 13, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet/android-tools#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants

@rmarinho@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add ADB reverse port forwarding support - #305

Merged
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port
Mar 23, 2026
Merged

Add ADB reverse port forwarding support#305
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port

Conversation

@rmarinho

@rmarinhormarinho commented Mar 13, 2026

Copy link
Copy Markdown
Member

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) — raw ADB wire protocol via TransportRunCommandWithStatus
  2. VS Code Extension (vscode-maui) — ServiceHub JSON-RPC to C# AdbServer
  3. Manual CLI — no shared C# implementation

Each IDE reimplements the same ADB operations. This PR adds a shared, CLI-based implementation in android-tools.

New API Surface

Types

publicenumAdbProtocol{Tcp,LocalAbstract,LocalReserved,LocalFilesystem}publicrecordAdbPortSpec(AdbProtocolProtocol,intPort){publicstringToSocketSpec();// e.g. "tcp:5000"publicstaticAdbPortSpec?TryParse(stringspec);// parse "tcp:5000" → AdbPortSpec}publicrecordAdbPortRule(AdbPortSpecRemote,AdbPortSpecLocal);

AdbRunner Methods

MethodADB CommandPurpose
ReversePortAsync(serial, AdbPortSpec remote, AdbPortSpec local, ct)adb reverse <remote> <local>Set up reverse forwarding
RemoveReversePortAsync(serial, AdbPortSpec remote, ct)adb reverse --remove <remote>Remove a specific rule
RemoveAllReversePortsAsync(serial, ct)adb reverse --remove-allRemove all rules
ListReversePortsAsync(serial, ct)adb reverse --listList active rules

Usage

vartcp5000=newAdbPortSpec(AdbProtocol.Tcp,5000);// Set up hot-reload tunnelawaitadbRunner.ReversePortAsync("emulator-5554",tcp5000,tcp5000);// List active rulesIReadOnlyList<AdbPortRule>rules=awaitadbRunner.ListReversePortsAsync("emulator-5554");// Clean upawaitadbRunner.RemoveReversePortAsync("emulator-5554",tcp5000);awaitadbRunner.RemoveAllReversePortsAsync("emulator-5554");

Key Design Decisions

  1. Strongly-typed AdbPortSpec — enum + int instead of raw strings like "tcp:5000", per @jonathanpeppers review
  2. CLI-based, not wire protocol — simpler to maintain and test, functionally equivalent
  3. Single overload per method — only AdbPortSpec, no string or int convenience overloads
  4. AdbPortRule record — value equality, structural matching on list output
  5. stdout included in error diagnosticsListReversePortsAsync passes both stdout and stderr to ThrowIfFailed

Tests

22 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbPortSpec.TryParse: valid TCP/LocalAbstract/LocalReserved/LocalFilesystem, null, empty, no colon, non-numeric port, zero port, unknown protocol
  • Parameter validation: empty serial, null remote/local (ArgumentNullException)
  • RemoveAllReversePortsAsync: empty serial validation
  • ListReversePortsAsync: empty serial validation

Review Feedback Addressed

ReviewerFeedbackResolution
@jonathanpeppersUse strongly-typed enum+int, not strings like "tcp:5000"AdbPortSpec(AdbProtocol, int) — single overload
@jonathanpeppersToo many overloads (string, int, AdbPortSpec)✅ Removed string and int overloads, kept only AdbPortSpec
@jonathanpeppersShould support adb forward too — rename to AdbPortRule✅ Renamed from AdbReversePortRule to AdbPortRule
CopilotInclude stdout in error diagnostics for ListReversePortsAsync✅ Pass stdout to ThrowIfFailed

Migration Path

Phase 1 (this PR): Shared API in android-tools ← HERE
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

Related

CopilotAI review requested due to automatic review settings March 13, 2026 09:20

This comment was marked as outdated.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch 2 times, most recently from b4bba1e to 3075d2cCompareMarch 19, 2026 14:22
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 40512c4 to 798b585CompareMarch 19, 2026 17:19
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 17:28

This comment was marked as outdated.

rmarinho added a commit that referenced this pull request Mar 19, 2026
…loads, stdout in errors
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 18:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, enabling downstream tooling (e.g., MAUI DevTools CLI) to manage hot-reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule).
  • Adds AdbRunner APIs for reverse port forwarding management (reverse, --remove, --remove-all, --list) plus list-output parsing.
  • Extends unit tests to cover parsing, AdbPortSpec parsing/formatting, and parameter validation; updates PublicAPI unshipped files.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec parsing/formatting, and new API parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds reverse port forwarding methods and adb reverse --list output parsing helper.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a forwarding rule (remote/local).
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for net10.0.

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

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, intended to be consumed by MAUI DevTools CLI (and later VS Code / VS) to manage Hot Reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed reverse port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule) and adds reverse operations to AdbRunner.
  • Implements parsing for adb reverse --list output.
  • Adds unit tests covering parsing, round-tripping, value equality, and parameter validation; updates PublicAPI unshipped files for both TFMs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse-list parsing, AdbPortSpec parsing/formatting, AdbPortRule equality, and new API argument validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds ReversePortAsync, removal APIs, list API, and adb reverse --list output parser.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a reverse/forward rule pair.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 Xamarin.Android.Tools.AndroidSdk’s AdbRunner, enabling downstream tools (e.g., MAUI DevTools CLI) to manage adb reverse tunnels via the adb CLI.

Changes:

  • Added new public types (AdbProtocol, AdbPortSpec, AdbPortRule) for strongly-typed reverse/forward socket specs and rules.
  • Added AdbRunner APIs for adb reverse operations: add rule, remove rule, remove all, and list rules (with list output parsing).
  • Added unit tests covering list parsing, AdbPortSpec parsing/formatting, and parameter validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec/AdbPortRule, and parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csImplements adb reverse command wrappers and a parser for adb reverse --list output.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csIntroduces AdbProtocol enum (currently TCP only).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csIntroduces AdbPortSpec record with ToSocketSpec() + TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csIntroduces AdbPortRule record representing a reverse/forward rule.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs

var rules = AdbRunner.ParseReverseListOutput (output);

// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped

CopilotAIMar 19, 2026

Copy link

Choose a reason for hiding this comment

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

This comment is a bit misleading: localabstract:... isn’t a “non-numeric port” (it’s a named socket). Consider rewording to clarify that the spec is skipped because AdbPortSpec.TryParse only supports numeric tcp:<port> at the moment.

Suggested change
// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped
// localabstract:chrome_devtools_remote is a named localabstract socket, not a tcp:<port>, so TryParse skips it

Copilot uses AI. Check for mistakes.
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs
rmarinhoand others added 2 commits March 20, 2026 01:31
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>
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>
rmarinhoand others added 7 commits March 20, 2026 01:31
…tRule
- 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>
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>
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>
…ble 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>
…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>
- 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>
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 8e65808 to 733b982CompareMarch 20, 2026 01:31
jonathanpeppersand others added 2 commits March 20, 2026 15:39
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>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Small follow-up request: add forward port management as the symmetric pair to the reverse-port methods this PR introduced. The AdbPortSpec / AdbPortRule / AdbProtocol types are already in place; forward is just four new methods reusing them.

publicpartialclassAdbRunner{/// adb -s <serial> forward <local> <remote>publicvirtualTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove <local>publicvirtualTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove-allpublicvirtualTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);/// adb -s <serial> forward --list (filter to matching serial; global output is `<serial> <local> <remote>` per line)publicvirtualTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);}

Why we need this in addition to reverse

adb forward and adb reverse are not interchangeable — they connect opposite directions:

  • reverse <remote-on-device> <local-on-host>: device-side socket forwards to host-side. Used by hot reload (the device app connects to a "device" port that's actually tunnelled to the IDE host).
  • forward <local-on-host> <remote-on-device>: host-side socket forwards to device-side. Used when the IDE / harness needs to connect to a service running on the device — debugger attach via JDWP (forward tcp:N jdwp:<pid>), DevFlow agent connect when the agent listens on a device port and the host needs a stable host-side port to reach it, performance-tracing endpoints exposed by the runtime, etc.

Consumers

  • VS Code MAUI extension ServiceHub→CLI migration — MauiAndroidPlatform.tsforwardPort() (debugger configurations, perf tooling).
  • MAUI DevTools CLI (dotnet/maui-labs) — maui android port forward … group, sibling of the existing reverse surface (maui-labs#197).
  • Visual Studio — same ClientTools.Platform paths that drive reverse today have parallel forward call-sites.

Happy to send the PR if a maintainer is okay with this scope landing as a direct follow-up to #305.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Sent the forward-port follow-up as PR #351 (draft).

rmarinho added a commit to rmarinho/android-tools that referenced this pull request May 5, 2026
Adds the symmetric forward-port pair to the reverse-port methods that landed
in dotnet#305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet#305 (comment)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 2, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jul 13, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet/android-tools#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants

@rmarinho@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add ADB reverse port forwarding support - #305

Merged
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port
Mar 23, 2026
Merged

Add ADB reverse port forwarding support#305
jonathanpeppers merged 11 commits into
mainfrom
feature/adb-reverse-port

Conversation

@rmarinho

@rmarinhormarinho commented Mar 13, 2026

Copy link
Copy Markdown
Member

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) — raw ADB wire protocol via TransportRunCommandWithStatus
  2. VS Code Extension (vscode-maui) — ServiceHub JSON-RPC to C# AdbServer
  3. Manual CLI — no shared C# implementation

Each IDE reimplements the same ADB operations. This PR adds a shared, CLI-based implementation in android-tools.

New API Surface

Types

publicenumAdbProtocol{Tcp,LocalAbstract,LocalReserved,LocalFilesystem}publicrecordAdbPortSpec(AdbProtocolProtocol,intPort){publicstringToSocketSpec();// e.g. "tcp:5000"publicstaticAdbPortSpec?TryParse(stringspec);// parse "tcp:5000" → AdbPortSpec}publicrecordAdbPortRule(AdbPortSpecRemote,AdbPortSpecLocal);

AdbRunner Methods

MethodADB CommandPurpose
ReversePortAsync(serial, AdbPortSpec remote, AdbPortSpec local, ct)adb reverse <remote> <local>Set up reverse forwarding
RemoveReversePortAsync(serial, AdbPortSpec remote, ct)adb reverse --remove <remote>Remove a specific rule
RemoveAllReversePortsAsync(serial, ct)adb reverse --remove-allRemove all rules
ListReversePortsAsync(serial, ct)adb reverse --listList active rules

Usage

vartcp5000=newAdbPortSpec(AdbProtocol.Tcp,5000);// Set up hot-reload tunnelawaitadbRunner.ReversePortAsync("emulator-5554",tcp5000,tcp5000);// List active rulesIReadOnlyList<AdbPortRule>rules=awaitadbRunner.ListReversePortsAsync("emulator-5554");// Clean upawaitadbRunner.RemoveReversePortAsync("emulator-5554",tcp5000);awaitadbRunner.RemoveAllReversePortsAsync("emulator-5554");

Key Design Decisions

  1. Strongly-typed AdbPortSpec — enum + int instead of raw strings like "tcp:5000", per @jonathanpeppers review
  2. CLI-based, not wire protocol — simpler to maintain and test, functionally equivalent
  3. Single overload per method — only AdbPortSpec, no string or int convenience overloads
  4. AdbPortRule record — value equality, structural matching on list output
  5. stdout included in error diagnosticsListReversePortsAsync passes both stdout and stderr to ThrowIfFailed

Tests

22 tests, all passing:

  • ParseReverseListOutput: single/multiple rules, empty output, non-reverse lines, malformed lines, non-TCP specs, Windows line endings
  • AdbPortSpec.TryParse: valid TCP/LocalAbstract/LocalReserved/LocalFilesystem, null, empty, no colon, non-numeric port, zero port, unknown protocol
  • Parameter validation: empty serial, null remote/local (ArgumentNullException)
  • RemoveAllReversePortsAsync: empty serial validation
  • ListReversePortsAsync: empty serial validation

Review Feedback Addressed

ReviewerFeedbackResolution
@jonathanpeppersUse strongly-typed enum+int, not strings like "tcp:5000"AdbPortSpec(AdbProtocol, int) — single overload
@jonathanpeppersToo many overloads (string, int, AdbPortSpec)✅ Removed string and int overloads, kept only AdbPortSpec
@jonathanpeppersShould support adb forward too — rename to AdbPortRule✅ Renamed from AdbReversePortRule to AdbPortRule
CopilotInclude stdout in error diagnostics for ListReversePortsAsync✅ Pass stdout to ThrowIfFailed

Migration Path

Phase 1 (this PR): Shared API in android-tools ← HERE
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

Related

CopilotAI review requested due to automatic review settings March 13, 2026 09:20

This comment was marked as outdated.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbReversePortRule.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch 2 times, most recently from b4bba1e to 3075d2cCompareMarch 19, 2026 14:22
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
rmarinho added a commit that referenced this pull request Mar 19, 2026
…tRule
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 40512c4 to 798b585CompareMarch 19, 2026 17:19
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 17:28

This comment was marked as outdated.

rmarinho added a commit that referenced this pull request Mar 19, 2026
…loads, stdout in errors
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinho requested a review from CopilotMarch 19, 2026 18:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, enabling downstream tooling (e.g., MAUI DevTools CLI) to manage hot-reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule).
  • Adds AdbRunner APIs for reverse port forwarding management (reverse, --remove, --remove-all, --list) plus list-output parsing.
  • Extends unit tests to cover parsing, AdbPortSpec parsing/formatting, and parameter validation; updates PublicAPI unshipped files.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec parsing/formatting, and new API parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds reverse port forwarding methods and adb reverse --list output parsing helper.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a forwarding rule (remote/local).
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares newly added public API surface for net10.0.

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

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds shared adb reverse (reverse port forwarding) support to Xamarin.Android.Tools.AndroidSdk via AdbRunner, intended to be consumed by MAUI DevTools CLI (and later VS Code / VS) to manage Hot Reload tunnels without ServiceHub.

Changes:

  • Introduces strongly-typed reverse port forwarding models (AdbProtocol, AdbPortSpec, AdbPortRule) and adds reverse operations to AdbRunner.
  • Implements parsing for adb reverse --list output.
  • Adds unit tests covering parsing, round-tripping, value equality, and parameter validation; updates PublicAPI unshipped files for both TFMs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse-list parsing, AdbPortSpec parsing/formatting, AdbPortRule equality, and new API argument validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csAdds ReversePortAsync, removal APIs, list API, and adb reverse --list output parser.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csAdds AdbProtocol enum for socket spec protocol kinds.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csAdds AdbPortSpec record with ToSocketSpec() and TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csAdds AdbPortRule record to represent a reverse/forward rule pair.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Add three rules learned from PR #305 review feedback:
- Prefer strongly-typed APIs (enum+record) over string parameters
- Avoid convenience overloads (string, int, typed) — pick one
- Include stdout in ProcessUtils.ThrowIfFailed error diagnostics
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 Xamarin.Android.Tools.AndroidSdk’s AdbRunner, enabling downstream tools (e.g., MAUI DevTools CLI) to manage adb reverse tunnels via the adb CLI.

Changes:

  • Added new public types (AdbProtocol, AdbPortSpec, AdbPortRule) for strongly-typed reverse/forward socket specs and rules.
  • Added AdbRunner APIs for adb reverse operations: add rule, remove rule, remove all, and list rules (with list output parsing).
  • Added unit tests covering list parsing, AdbPortSpec parsing/formatting, and parameter validation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.csAdds unit tests for reverse list parsing, AdbPortSpec/AdbPortRule, and parameter validation.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.csImplements adb reverse command wrappers and a parser for adb reverse --list output.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.csIntroduces AdbProtocol enum (currently TCP only).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.csIntroduces AdbPortSpec record with ToSocketSpec() + TryParse().
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortRule.csIntroduces AdbPortRule record representing a reverse/forward rule.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txtDeclares new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txtDeclares new public API surface for net10.0.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs

var rules = AdbRunner.ParseReverseListOutput (output);

// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped

CopilotAIMar 19, 2026

Copy link

Choose a reason for hiding this comment

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

This comment is a bit misleading: localabstract:... isn’t a “non-numeric port” (it’s a named socket). Consider rewording to clarify that the spec is skipped because AdbPortSpec.TryParse only supports numeric tcp:<port> at the moment.

Suggested change
// localabstract:chrome_devtools_remote has a non-numeric port, so it is skipped
// localabstract:chrome_devtools_remote is a named localabstract socket, not a tcp:<port>, so TryParse skips it

Copilot uses AI. Check for mistakes.
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbPortSpec.cs
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbProtocol.cs
rmarinhoand others added 2 commits March 20, 2026 01:31
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>
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>
rmarinhoand others added 7 commits March 20, 2026 01:31
…tRule
- 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>
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>
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>
…ble 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>
…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>
- 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>
- 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>
@rmarinho
rmarinhoforce-pushed the feature/adb-reverse-port branch from 8e65808 to 733b982CompareMarch 20, 2026 01:31
jonathanpeppersand others added 2 commits March 20, 2026 15:39
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>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Small follow-up request: add forward port management as the symmetric pair to the reverse-port methods this PR introduced. The AdbPortSpec / AdbPortRule / AdbProtocol types are already in place; forward is just four new methods reusing them.

publicpartialclassAdbRunner{/// adb -s <serial> forward <local> <remote>publicvirtualTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove <local>publicvirtualTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default);/// adb -s <serial> forward --remove-allpublicvirtualTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);/// adb -s <serial> forward --list (filter to matching serial; global output is `<serial> <local> <remote>` per line)publicvirtualTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default);}

Why we need this in addition to reverse

adb forward and adb reverse are not interchangeable — they connect opposite directions:

  • reverse <remote-on-device> <local-on-host>: device-side socket forwards to host-side. Used by hot reload (the device app connects to a "device" port that's actually tunnelled to the IDE host).
  • forward <local-on-host> <remote-on-device>: host-side socket forwards to device-side. Used when the IDE / harness needs to connect to a service running on the device — debugger attach via JDWP (forward tcp:N jdwp:<pid>), DevFlow agent connect when the agent listens on a device port and the host needs a stable host-side port to reach it, performance-tracing endpoints exposed by the runtime, etc.

Consumers

  • VS Code MAUI extension ServiceHub→CLI migration — MauiAndroidPlatform.tsforwardPort() (debugger configurations, perf tooling).
  • MAUI DevTools CLI (dotnet/maui-labs) — maui android port forward … group, sibling of the existing reverse surface (maui-labs#197).
  • Visual Studio — same ClientTools.Platform paths that drive reverse today have parallel forward call-sites.

Happy to send the PR if a maintainer is okay with this scope landing as a direct follow-up to #305.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Sent the forward-port follow-up as PR #351 (draft).

rmarinho added a commit to rmarinho/android-tools that referenced this pull request May 5, 2026
Adds the symmetric forward-port pair to the reverse-port methods that landed
in dotnet#305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet#305 (comment)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 2, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jul 13, 2026
(follow-up to #305)
Adds the symmetric forward-port pair to the reverse-port methods that landed
in #305. Same `AdbPortSpec` / `AdbPortRule` / `AdbProtocol` types — just four
new methods.
| Method | adb command |
|-------------------------------------------------|--------------------------------------------|
| ForwardPortAsync(serial, local, remote) | adb -s <serial> forward <local> <remote> |
| RemoveForwardPortAsync(serial, local) | adb -s <serial> forward --remove <local> |
| RemoveAllForwardPortsAsync(serial) | adb -s <serial> forward --remove-all |
| ListForwardPortsAsync(serial) | adb forward --list (filtered by serial) |
`adb forward` and `adb reverse` are not interchangeable — they connect
opposite directions. `forward` is host->device (the IDE/harness reaches a
service running on the device) and is the path used for JDWP debugger
attach (`forward tcp:N jdwp:<pid>`), perf-tracing endpoints exposed by the
runtime, and host-side DevFlow agent connect when the agent listens on a
device port.
Output-format note for `--list`: `adb forward --list` emits one line per
rule across all devices in the form `<serial> <local> <remote>` (different
from `(reverse) <remote> <local>`). `ListForwardPortsAsync` uses the
unscoped `adb forward --list` and filters to the requested serial in
`ParseForwardListOutput`. Serial match is case-sensitive (matches adb).
- 12 new parser tests in `ParseForwardListOutput_*` mirroring the reverse
parser tests (single rule, multiple rules, serial filtering, empty
output, malformed lines, non-tcp specs, Windows line endings, tab
separation, case sensitivity).
- 7 new parameter-validation tests covering empty serial / null spec for
the four new public methods.
- VS Code MAUI extension ServiceHub->CLI migration (`forwardPort` in
`MauiAndroidPlatform.ts` — debugger configurations, perf tooling).
- MAUI DevTools CLI (dotnet/maui-labs#197) — `maui android port forward`
group, sibling of the existing `reverse` surface.
- Visual Studio `ClientTools.Platform` — same paths that drive reverse
today.
Discussed in
dotnet/android-tools#305 (comment)
### Address review feedback: capture stdout in ThrowIfFailed, parser asymmetry comment, drop null!, remove test region dividers
- ForwardPortAsync/RemoveForwardPortAsync/RemoveAllForwardPortsAsync now capture stdout and pass it to ProcessUtils.ThrowIfFailed (matches repo convention; adb sometimes writes errors to stdout).
- Added <remarks> block on ParseForwardListOutput calling out the field-order asymmetry vs ParseReverseListOutput (forward: serial local remote; reverse: (reverse) remote local).
- Replaced '(AdbPortSpec) null!' with '(AdbPortSpec) null' in 3 forward-port test sites to match reverse-test convention and repo no-null-forgiving rule.
- Removed all '// --- ... ---' region-like divider comments in AdbRunnerTests.cs (per jonathanpeppers feedback in PR #351).
### Fix RemoveAllForwardPortsAsync to honour per-serial scope
The underlying 'adb forward --remove-all' (and the wire-protocol equivalent 'host-serial:<serial>:killforward-all') is daemon-global -- the '-s <serial>' flag does not scope it. The previous implementation would silently remove forwards for every connected device despite the method's per-device API contract.
Reimplement by listing forwards for the given serial via ListForwardPortsAsync and removing them individually via RemoveForwardPortAsync. Update the XML docs to describe the actual behaviour.
Add two new tests using a recording subclass of AdbRunner that overrides ListForwardPortsAsync and RemoveForwardPortAsync to verify (1) only ports for the requested serial are removed, and (2) an empty listing is a no-op.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB reverse port forwarding support

3 participants

@rmarinho@jonathanpeppers