Skip to content

[AdbRunner] Add ADB forward port management (follow-up to #305)#351

Merged
jonathanpeppers merged 3 commits into
dotnet:mainfrom
rmarinho:feature/adb-forward-port
Jun 2, 2026
Merged

[AdbRunner] Add ADB forward port management (follow-up to #305)#351
jonathanpeppers merged 3 commits into
dotnet:mainfrom
rmarinho:feature/adb-forward-port

Conversation

@rmarinho

Copy link
Copy Markdown
Member

Summary

Adds the symmetric forward-port pair to the reverse-port methods that landed in #305. Same AdbPortSpec / AdbPortRule / AdbProtocol types — just four new methods. Discussed in #305 (comment).

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)

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:
    • JDWP debugger attachforward tcp:N jdwp:<pid>
    • Performance-tracing endpoints exposed by the runtime
    • DevFlow agent connect — when the in-app agent listens on a device port and the host needs a stable host-side port to reach it

--list output format note

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 to match adb's behaviour.

Tests

  • 12 new parser tests (ParseForwardListOutput_*) mirroring the reverse parser tests:
    • single rule, multiple rules, serial filtering, empty output, no lines, empty serial, no matching device, malformed line, 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.

All Forward + Reverse tests pass locally (36/36). Full suite has one pre-existing unrelated JdkDirectory_JavaHome failure on main that's not touched by this PR.

Consumers

  • 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 have parallel forward call-sites.

Public API additions

virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, AdbPortSpec! local, AdbPortSpec! remote, CancellationToken cancellationToken = default) -> Task!
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, CancellationToken cancellationToken = default) -> Task<IReadOnlyList<AdbPortRule!>!>!
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, CancellationToken cancellationToken = default) -> Task!
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, AdbPortSpec! local, CancellationToken cancellationToken = default) -> Task!

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>
@rmarinho rmarinho force-pushed the feature/adb-forward-port branch from 905c04a to 40c20d0 Compare May 5, 2026 16:14
@rmarinho rmarinho marked this pull request as ready for review May 5, 2026 16:15
Copilot AI review requested due to automatic review settings May 5, 2026 16:15

Copilot AI 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 adb forward support to Xamarin.Android.Tools.AdbRunner, mirroring the existing reverse-port forwarding API surface added in #305, including parsing and parameter-validation tests plus PublicAPI updates.

Changes:

  • Added ForwardPortAsync, RemoveForwardPortAsync, RemoveAllForwardPortsAsync, and ListForwardPortsAsync to AdbRunner.
  • Implemented ParseForwardListOutput to parse/filter adb forward --list output by device serial.
  • Added new NUnit tests for forward list parsing and forward method parameter validation; updated PublicAPI unshipped entries for both TFMs.

Reviewed changes

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

File Description
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs Adds parser coverage for adb forward --list output and parameter validation tests for the new forward APIs.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Introduces the new forward-port management methods and parsing logic, mirroring the reverse-port implementation patterns.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt Records new public API members for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt Records new public API members for net10.0.

Comment thread src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment thread tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs
@jonathanpeppers

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Android Tools PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ✅ LGTM — Clean, well-structured symmetric addition to the reverse-port API.

Summary:

  • 4 new public methods (ForwardPortAsync, RemoveForwardPortAsync, RemoveAllForwardPortsAsync, ListForwardPortsAsync) follow the established reverse-port patterns exactly
  • Parser handles the different adb forward --list output format correctly (<serial> <local> <remote> vs reverse's (reverse) <remote> <local>)
  • Uses ProcessUtils throughout, proper parameter validation, IReadOnlyList<T> return types ✅
  • PublicAPI files updated for both TFMs ✅
  • 19 new tests (12 parser + 7 validation) with good edge-case coverage ✅
  • CI is green ✅
Severity Count
⚠️ warning 1 (null-forgiving ! in 3 test lines — inconsistent with existing reverse tests)
💡 suggestion 1 (positive observation on parser correctness)

Positive callouts:

  • Test coverage mirrors the reverse parser tests comprehensively (serial filtering, tab/CRLF, case sensitivity, malformed lines, non-TCP specs)
  • Good PR description explaining the --list output format difference and why client-side serial filtering is needed for forward

Generated by Android Tools PR Reviewer for issue #351 · ● 3.3M

Comment thread src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Comment thread tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs Outdated
Comment thread tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs Outdated
Comment thread src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment thread src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
…metry 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 dotnet#351).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho requested a review from jonathanpeppers June 1, 2026 14:06
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>
@rmarinho

rmarinho commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

Follow-up to my Thread A reply above: while writing that analysis I flagged host-serial:<serial>:killforward-all as a daemon-global operation, but kept treating it as "orthogonal" to this PR. On a second pass that was the wrong call — RemoveAllForwardPortsAsync is new public API in this PR, and shipping a per-device method whose implementation silently nukes forwards across every connected device is a footgun.

Fixed in 9bbbf88:

  • RemoveAllForwardPortsAsync now lists forwards for the given serial via ListForwardPortsAsync and removes them individually via RemoveForwardPortAsync, so per-device scope is honoured for real.
  • Updated the XML <remarks> to describe the actual behaviour and call out why we can't just shell out to adb forward --remove-all.
  • Added two cross-platform tests (RemoveAllForwardPortsAsync_RemovesOnlyPortsForGivenSerial and RemoveAllForwardPortsAsync_EmptyList_IsNoOp) using a recording subclass that overrides the two virtual methods we call into, asserting we only touch ports for the requested serial.

The deferred items from Thread A (stdout-parity for the three reverse mutation methods, plus the broader migration to AdbClient once #327 merges, plus cross-platform failure-path tests) are now tracked in #382.

Build clean, 112 AdbRunner tests passing.

@jonathanpeppers

Copy link
Copy Markdown
Member

/review

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

Android Tools PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ✅ LGTM — Clean, well-structured addition of forward port management as the symmetric pair to the existing reverse port methods.

Highlights:

  • ✅ All four methods follow the established Reverse* patterns exactly (validation, ProcessUtils, ConfigureAwait, ThrowIfFailed with stdout)
  • RemoveAllForwardPortsAsync correctly avoids the global adb forward --remove-all pitfall with thorough <remarks> documentation
  • ParseForwardListOutput handles the field-order asymmetry vs reverse correctly, with a clear cross-reference comment
  • ✅ Public API files updated for both net10.0 and netstandard2.0 TFMs
  • ✅ Comprehensive test coverage: 12 parser tests (mirroring reverse), 7 parameter validation tests, 2 RecordingAdbRunner composition tests
  • ✅ Banner separator comments removed (repo convention cleanup)

Issues: 0 ❌ | 0 ⚠️ | 2 💡

CI note: Only license/cla check run is visible (passing). Build/test CI may be tracked via status checks or a separate pipeline.

Generated by Android Tools PR Reviewer for issue #351 · ● 5M

using var stderr = new StringWriter ();
var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "forward", "--list");
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
ProcessUtils.ThrowIfFailed (exitCode, $"adb forward --list", stderr, stdout);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Code style — The $ prefix is unnecessary here since there are no interpolation expressions. A plain string literal "adb forward --list" avoids the (slight) reader surprise.

Rule: Don't wrap a value in an interpolated string

Comment on lines +422 to +431
public virtual async Task RemoveAllForwardPortsAsync (string serial, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace (serial))
throw new ArgumentException ("Serial must not be empty.", nameof (serial));

var rules = await ListForwardPortsAsync (serial, cancellationToken).ConfigureAwait (false);
foreach (var rule in rules) {
cancellationToken.ThrowIfCancellationRequested ();
await RemoveForwardPortAsync (serial, rule.Local, cancellationToken).ConfigureAwait (false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Pattern — Excellent design choice. The list-then-remove-individually approach avoids the adb forward --remove-all global-scope pitfall, and the <remarks> documentation explaining why is exemplary — future maintainers will immediately understand the constraint. The virtual overrides also make the composition cleanly testable (as the RecordingAdbRunner tests demonstrate).

Rule: Document non-obvious design decisions

@jonathanpeppers jonathanpeppers merged commit 5165523 into dotnet:main Jun 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants