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

Commit 5165523

Browse files
rmarinhoCopilot
andauthored
[AdbRunner] Add ADB forward port management (#351)
(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>
1 parent 06a0867 commit 5165523

4 files changed

Lines changed: 427 additions & 22 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,145 @@ internal static IReadOnlyList<AdbPortRule> ParseReverseListOutput (IEnumerable<s
355355
returnrules;
356356
}
357357

358+
/// <summary>
359+
/// Sets up forward port forwarding via 'adb -s &lt;serial&gt; forward &lt;local&gt; &lt;remote&gt;'.
360+
/// The host-side &lt;local&gt; socket is forwarded to the device-side &lt;remote&gt; socket,
361+
/// the symmetric pair to <see cref="ReversePortAsync"/>.
362+
/// </summary>
363+
/// <param name="serial">Device serial number.</param>
364+
/// <param name="local">Local (host-side) port spec.</param>
365+
/// <param name="remote">Remote (device-side) port spec.</param>
366+
/// <param name="cancellationToken">Cancellation token.</param>
367+
publicvirtualasyncTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default)
368+
{
369+
if(string.IsNullOrWhiteSpace(serial))
370+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
371+
if(localisnull)
372+
thrownewArgumentNullException(nameof(local));
373+
if(remoteisnull)
374+
thrownewArgumentNullException(nameof(remote));
375+
if(local.Port<=0||local.Port>65535)
376+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
377+
if(remote.Port<=0||remote.Port>65535)
378+
thrownewArgumentOutOfRangeException(nameof(remote),remote.Port,"Port must be between 1 and 65535.");
379+
380+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward",local.ToSocketSpec(),remote.ToSocketSpec());
381+
usingvarstdout=newStringWriter();
382+
usingvarstderr=newStringWriter();
383+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
384+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward {local}{remote}",stderr,stdout);
385+
}
386+
387+
/// <summary>
388+
/// Removes a specific forward port forwarding rule via
389+
/// 'adb -s &lt;serial&gt; forward --remove &lt;local&gt;'.
390+
/// </summary>
391+
/// <param name="serial">Device serial number.</param>
392+
/// <param name="local">Local (host-side) port spec to remove.</param>
393+
/// <param name="cancellationToken">Cancellation token.</param>
394+
publicvirtualasyncTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default)
395+
{
396+
if(string.IsNullOrWhiteSpace(serial))
397+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
398+
if(localisnull)
399+
thrownewArgumentNullException(nameof(local));
400+
if(local.Port<=0||local.Port>65535)
401+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
402+
403+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward","--remove",local.ToSocketSpec());
404+
usingvarstdout=newStringWriter();
405+
usingvarstderr=newStringWriter();
406+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
407+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward --remove {local}",stderr,stdout);
408+
}
409+
410+
/// <summary>
411+
/// Removes all forward port forwarding rules for the specified device.
412+
/// </summary>
413+
/// <remarks>
414+
/// The underlying <c>adb forward --remove-all</c> command (and its wire-protocol
415+
/// equivalent <c>host-serial:&lt;serial&gt;:killforward-all</c>) operates globally on the
416+
/// adb daemon — the <c>-s &lt;serial&gt;</c> flag does not scope it, and calling it
417+
/// would remove forwards for every connected device. To honour the per-device
418+
/// contract of this method, we list the forwards for <paramref name="serial"/>
419+
/// via <see cref="ListForwardPortsAsync"/> and remove them individually via
420+
/// <see cref="RemoveForwardPortAsync"/>.
421+
/// </remarks>
422+
publicvirtualasyncTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
423+
{
424+
if(string.IsNullOrWhiteSpace(serial))
425+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
426+
427+
varrules=awaitListForwardPortsAsync(serial,cancellationToken).ConfigureAwait(false);
428+
foreach(varruleinrules){
429+
cancellationToken.ThrowIfCancellationRequested();
430+
awaitRemoveForwardPortAsync(serial,rule.Local,cancellationToken).ConfigureAwait(false);
431+
}
432+
}
433+
434+
/// <summary>
435+
/// Lists active forward port forwarding rules for the specified device via
436+
/// 'adb forward --list'.
437+
/// The underlying command always lists rules across all devices, so the
438+
/// result is filtered to entries matching <paramref name="serial"/>.
439+
/// </summary>
440+
publicvirtualasyncTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
441+
{
442+
if(string.IsNullOrWhiteSpace(serial))
443+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
444+
445+
usingvarstdout=newStringWriter();
446+
usingvarstderr=newStringWriter();
447+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"forward","--list");
448+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
449+
ProcessUtils.ThrowIfFailed(exitCode,$"adb forward --list",stderr,stdout);
450+
451+
returnParseForwardListOutput(stdout.ToString().Split('\n'),serial);
452+
}
453+
454+
/// <summary>
455+
/// Parses the output of 'adb forward --list'.
456+
/// Each line is "&lt;serial&gt; &lt;local&gt; &lt;remote&gt;", e.g. "emulator-5554 tcp:5000 tcp:6000".
457+
/// Only rules matching <paramref name="serial"/> are returned. Lines with
458+
/// unparseable socket specs are skipped.
459+
/// </summary>
460+
/// <remarks>
461+
/// Note the field-order asymmetry vs <see cref="ParseReverseListOutput"/>:
462+
/// forward --list: &lt;serial&gt; &lt;local&gt; &lt;remote&gt;
463+
/// reverse --list: (reverse) &lt;remote&gt; &lt;local&gt;
464+
/// Both parsers construct an <see cref="AdbPortRule"/> whose constructor takes
465+
/// (Remote, Local), so the order in which we pass the parsed parts differs between
466+
/// the two parsers — keep that in mind when modifying either of them.
467+
/// </remarks>
468+
internalstaticIReadOnlyList<AdbPortRule>ParseForwardListOutput(IEnumerable<string>lines,stringserial)
469+
{
470+
varrules=newList<AdbPortRule>();
471+
if(string.IsNullOrEmpty(serial))
472+
returnrules;
473+
474+
foreach(varlineinlines){
475+
vartrimmed=line.Trim();
476+
if(string.IsNullOrEmpty(trimmed))
477+
continue;
478+
479+
// Expected format: "<serial> <local> <remote>" — see <remarks> above for
480+
// the field-order asymmetry with reverse --list.
481+
varparts=trimmed.Split((char[]?)null,StringSplitOptions.RemoveEmptyEntries);
482+
if(parts.Length<3)
483+
continue;
484+
485+
if(!string.Equals(parts[0],serial,StringComparison.Ordinal))
486+
continue;
487+
488+
varlocal=AdbPortSpec.TryParse(parts[1]);
489+
varremote=AdbPortSpec.TryParse(parts[2]);
490+
if(localis{}l&&remoteis{}r)
491+
rules.Add(newAdbPortRule(r,l));
492+
}
493+
494+
returnrules;
495+
}
496+
358497
/// <summary>
359498
/// Parses the output lines from 'adb devices -l'.
360499
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

0 commit comments

Comments
 (0)
, '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.

Commit 5165523

Browse files
rmarinhoCopilot
andauthored
[AdbRunner] Add ADB forward port management (#351)
(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>
1 parent 06a0867 commit 5165523

4 files changed

Lines changed: 427 additions & 22 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,145 @@ internal static IReadOnlyList<AdbPortRule> ParseReverseListOutput (IEnumerable<s
355355
returnrules;
356356
}
357357

358+
/// <summary>
359+
/// Sets up forward port forwarding via 'adb -s &lt;serial&gt; forward &lt;local&gt; &lt;remote&gt;'.
360+
/// The host-side &lt;local&gt; socket is forwarded to the device-side &lt;remote&gt; socket,
361+
/// the symmetric pair to <see cref="ReversePortAsync"/>.
362+
/// </summary>
363+
/// <param name="serial">Device serial number.</param>
364+
/// <param name="local">Local (host-side) port spec.</param>
365+
/// <param name="remote">Remote (device-side) port spec.</param>
366+
/// <param name="cancellationToken">Cancellation token.</param>
367+
publicvirtualasyncTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default)
368+
{
369+
if(string.IsNullOrWhiteSpace(serial))
370+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
371+
if(localisnull)
372+
thrownewArgumentNullException(nameof(local));
373+
if(remoteisnull)
374+
thrownewArgumentNullException(nameof(remote));
375+
if(local.Port<=0||local.Port>65535)
376+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
377+
if(remote.Port<=0||remote.Port>65535)
378+
thrownewArgumentOutOfRangeException(nameof(remote),remote.Port,"Port must be between 1 and 65535.");
379+
380+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward",local.ToSocketSpec(),remote.ToSocketSpec());
381+
usingvarstdout=newStringWriter();
382+
usingvarstderr=newStringWriter();
383+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
384+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward {local}{remote}",stderr,stdout);
385+
}
386+
387+
/// <summary>
388+
/// Removes a specific forward port forwarding rule via
389+
/// 'adb -s &lt;serial&gt; forward --remove &lt;local&gt;'.
390+
/// </summary>
391+
/// <param name="serial">Device serial number.</param>
392+
/// <param name="local">Local (host-side) port spec to remove.</param>
393+
/// <param name="cancellationToken">Cancellation token.</param>
394+
publicvirtualasyncTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default)
395+
{
396+
if(string.IsNullOrWhiteSpace(serial))
397+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
398+
if(localisnull)
399+
thrownewArgumentNullException(nameof(local));
400+
if(local.Port<=0||local.Port>65535)
401+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
402+
403+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward","--remove",local.ToSocketSpec());
404+
usingvarstdout=newStringWriter();
405+
usingvarstderr=newStringWriter();
406+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
407+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward --remove {local}",stderr,stdout);
408+
}
409+
410+
/// <summary>
411+
/// Removes all forward port forwarding rules for the specified device.
412+
/// </summary>
413+
/// <remarks>
414+
/// The underlying <c>adb forward --remove-all</c> command (and its wire-protocol
415+
/// equivalent <c>host-serial:&lt;serial&gt;:killforward-all</c>) operates globally on the
416+
/// adb daemon — the <c>-s &lt;serial&gt;</c> flag does not scope it, and calling it
417+
/// would remove forwards for every connected device. To honour the per-device
418+
/// contract of this method, we list the forwards for <paramref name="serial"/>
419+
/// via <see cref="ListForwardPortsAsync"/> and remove them individually via
420+
/// <see cref="RemoveForwardPortAsync"/>.
421+
/// </remarks>
422+
publicvirtualasyncTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
423+
{
424+
if(string.IsNullOrWhiteSpace(serial))
425+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
426+
427+
varrules=awaitListForwardPortsAsync(serial,cancellationToken).ConfigureAwait(false);
428+
foreach(varruleinrules){
429+
cancellationToken.ThrowIfCancellationRequested();
430+
awaitRemoveForwardPortAsync(serial,rule.Local,cancellationToken).ConfigureAwait(false);
431+
}
432+
}
433+
434+
/// <summary>
435+
/// Lists active forward port forwarding rules for the specified device via
436+
/// 'adb forward --list'.
437+
/// The underlying command always lists rules across all devices, so the
438+
/// result is filtered to entries matching <paramref name="serial"/>.
439+
/// </summary>
440+
publicvirtualasyncTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
441+
{
442+
if(string.IsNullOrWhiteSpace(serial))
443+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
444+
445+
usingvarstdout=newStringWriter();
446+
usingvarstderr=newStringWriter();
447+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"forward","--list");
448+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
449+
ProcessUtils.ThrowIfFailed(exitCode,$"adb forward --list",stderr,stdout);
450+
451+
returnParseForwardListOutput(stdout.ToString().Split('\n'),serial);
452+
}
453+
454+
/// <summary>
455+
/// Parses the output of 'adb forward --list'.
456+
/// Each line is "&lt;serial&gt; &lt;local&gt; &lt;remote&gt;", e.g. "emulator-5554 tcp:5000 tcp:6000".
457+
/// Only rules matching <paramref name="serial"/> are returned. Lines with
458+
/// unparseable socket specs are skipped.
459+
/// </summary>
460+
/// <remarks>
461+
/// Note the field-order asymmetry vs <see cref="ParseReverseListOutput"/>:
462+
/// forward --list: &lt;serial&gt; &lt;local&gt; &lt;remote&gt;
463+
/// reverse --list: (reverse) &lt;remote&gt; &lt;local&gt;
464+
/// Both parsers construct an <see cref="AdbPortRule"/> whose constructor takes
465+
/// (Remote, Local), so the order in which we pass the parsed parts differs between
466+
/// the two parsers — keep that in mind when modifying either of them.
467+
/// </remarks>
468+
internalstaticIReadOnlyList<AdbPortRule>ParseForwardListOutput(IEnumerable<string>lines,stringserial)
469+
{
470+
varrules=newList<AdbPortRule>();
471+
if(string.IsNullOrEmpty(serial))
472+
returnrules;
473+
474+
foreach(varlineinlines){
475+
vartrimmed=line.Trim();
476+
if(string.IsNullOrEmpty(trimmed))
477+
continue;
478+
479+
// Expected format: "<serial> <local> <remote>" — see <remarks> above for
480+
// the field-order asymmetry with reverse --list.
481+
varparts=trimmed.Split((char[]?)null,StringSplitOptions.RemoveEmptyEntries);
482+
if(parts.Length<3)
483+
continue;
484+
485+
if(!string.Equals(parts[0],serial,StringComparison.Ordinal))
486+
continue;
487+
488+
varlocal=AdbPortSpec.TryParse(parts[1]);
489+
varremote=AdbPortSpec.TryParse(parts[2]);
490+
if(localis{}l&&remoteis{}r)
491+
rules.Add(newAdbPortRule(r,l));
492+
}
493+
494+
returnrules;
495+
}
496+
358497
/// <summary>
359498
/// Parses the output lines from 'adb devices -l'.
360499
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

0 commit comments

Comments
 (0)
, '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.

Commit 5165523

Browse files
rmarinhoCopilot
andauthored
[AdbRunner] Add ADB forward port management (#351)
(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>
1 parent 06a0867 commit 5165523

4 files changed

Lines changed: 427 additions & 22 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,145 @@ internal static IReadOnlyList<AdbPortRule> ParseReverseListOutput (IEnumerable<s
355355
returnrules;
356356
}
357357

358+
/// <summary>
359+
/// Sets up forward port forwarding via 'adb -s &lt;serial&gt; forward &lt;local&gt; &lt;remote&gt;'.
360+
/// The host-side &lt;local&gt; socket is forwarded to the device-side &lt;remote&gt; socket,
361+
/// the symmetric pair to <see cref="ReversePortAsync"/>.
362+
/// </summary>
363+
/// <param name="serial">Device serial number.</param>
364+
/// <param name="local">Local (host-side) port spec.</param>
365+
/// <param name="remote">Remote (device-side) port spec.</param>
366+
/// <param name="cancellationToken">Cancellation token.</param>
367+
publicvirtualasyncTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default)
368+
{
369+
if(string.IsNullOrWhiteSpace(serial))
370+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
371+
if(localisnull)
372+
thrownewArgumentNullException(nameof(local));
373+
if(remoteisnull)
374+
thrownewArgumentNullException(nameof(remote));
375+
if(local.Port<=0||local.Port>65535)
376+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
377+
if(remote.Port<=0||remote.Port>65535)
378+
thrownewArgumentOutOfRangeException(nameof(remote),remote.Port,"Port must be between 1 and 65535.");
379+
380+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward",local.ToSocketSpec(),remote.ToSocketSpec());
381+
usingvarstdout=newStringWriter();
382+
usingvarstderr=newStringWriter();
383+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
384+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward {local}{remote}",stderr,stdout);
385+
}
386+
387+
/// <summary>
388+
/// Removes a specific forward port forwarding rule via
389+
/// 'adb -s &lt;serial&gt; forward --remove &lt;local&gt;'.
390+
/// </summary>
391+
/// <param name="serial">Device serial number.</param>
392+
/// <param name="local">Local (host-side) port spec to remove.</param>
393+
/// <param name="cancellationToken">Cancellation token.</param>
394+
publicvirtualasyncTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default)
395+
{
396+
if(string.IsNullOrWhiteSpace(serial))
397+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
398+
if(localisnull)
399+
thrownewArgumentNullException(nameof(local));
400+
if(local.Port<=0||local.Port>65535)
401+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
402+
403+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward","--remove",local.ToSocketSpec());
404+
usingvarstdout=newStringWriter();
405+
usingvarstderr=newStringWriter();
406+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
407+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward --remove {local}",stderr,stdout);
408+
}
409+
410+
/// <summary>
411+
/// Removes all forward port forwarding rules for the specified device.
412+
/// </summary>
413+
/// <remarks>
414+
/// The underlying <c>adb forward --remove-all</c> command (and its wire-protocol
415+
/// equivalent <c>host-serial:&lt;serial&gt;:killforward-all</c>) operates globally on the
416+
/// adb daemon — the <c>-s &lt;serial&gt;</c> flag does not scope it, and calling it
417+
/// would remove forwards for every connected device. To honour the per-device
418+
/// contract of this method, we list the forwards for <paramref name="serial"/>
419+
/// via <see cref="ListForwardPortsAsync"/> and remove them individually via
420+
/// <see cref="RemoveForwardPortAsync"/>.
421+
/// </remarks>
422+
publicvirtualasyncTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
423+
{
424+
if(string.IsNullOrWhiteSpace(serial))
425+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
426+
427+
varrules=awaitListForwardPortsAsync(serial,cancellationToken).ConfigureAwait(false);
428+
foreach(varruleinrules){
429+
cancellationToken.ThrowIfCancellationRequested();
430+
awaitRemoveForwardPortAsync(serial,rule.Local,cancellationToken).ConfigureAwait(false);
431+
}
432+
}
433+
434+
/// <summary>
435+
/// Lists active forward port forwarding rules for the specified device via
436+
/// 'adb forward --list'.
437+
/// The underlying command always lists rules across all devices, so the
438+
/// result is filtered to entries matching <paramref name="serial"/>.
439+
/// </summary>
440+
publicvirtualasyncTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
441+
{
442+
if(string.IsNullOrWhiteSpace(serial))
443+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
444+
445+
usingvarstdout=newStringWriter();
446+
usingvarstderr=newStringWriter();
447+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"forward","--list");
448+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
449+
ProcessUtils.ThrowIfFailed(exitCode,$"adb forward --list",stderr,stdout);
450+
451+
returnParseForwardListOutput(stdout.ToString().Split('\n'),serial);
452+
}
453+
454+
/// <summary>
455+
/// Parses the output of 'adb forward --list'.
456+
/// Each line is "&lt;serial&gt; &lt;local&gt; &lt;remote&gt;", e.g. "emulator-5554 tcp:5000 tcp:6000".
457+
/// Only rules matching <paramref name="serial"/> are returned. Lines with
458+
/// unparseable socket specs are skipped.
459+
/// </summary>
460+
/// <remarks>
461+
/// Note the field-order asymmetry vs <see cref="ParseReverseListOutput"/>:
462+
/// forward --list: &lt;serial&gt; &lt;local&gt; &lt;remote&gt;
463+
/// reverse --list: (reverse) &lt;remote&gt; &lt;local&gt;
464+
/// Both parsers construct an <see cref="AdbPortRule"/> whose constructor takes
465+
/// (Remote, Local), so the order in which we pass the parsed parts differs between
466+
/// the two parsers — keep that in mind when modifying either of them.
467+
/// </remarks>
468+
internalstaticIReadOnlyList<AdbPortRule>ParseForwardListOutput(IEnumerable<string>lines,stringserial)
469+
{
470+
varrules=newList<AdbPortRule>();
471+
if(string.IsNullOrEmpty(serial))
472+
returnrules;
473+
474+
foreach(varlineinlines){
475+
vartrimmed=line.Trim();
476+
if(string.IsNullOrEmpty(trimmed))
477+
continue;
478+
479+
// Expected format: "<serial> <local> <remote>" — see <remarks> above for
480+
// the field-order asymmetry with reverse --list.
481+
varparts=trimmed.Split((char[]?)null,StringSplitOptions.RemoveEmptyEntries);
482+
if(parts.Length<3)
483+
continue;
484+
485+
if(!string.Equals(parts[0],serial,StringComparison.Ordinal))
486+
continue;
487+
488+
varlocal=AdbPortSpec.TryParse(parts[1]);
489+
varremote=AdbPortSpec.TryParse(parts[2]);
490+
if(localis{}l&&remoteis{}r)
491+
rules.Add(newAdbPortRule(r,l));
492+
}
493+
494+
returnrules;
495+
}
496+
358497
/// <summary>
359498
/// Parses the output lines from 'adb devices -l'.
360499
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

0 commit comments

Comments
 (0)
, '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.

Commit 5165523

Browse files
rmarinhoCopilot
andauthored
[AdbRunner] Add ADB forward port management (#351)
(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>
1 parent 06a0867 commit 5165523

4 files changed

Lines changed: 427 additions & 22 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,145 @@ internal static IReadOnlyList<AdbPortRule> ParseReverseListOutput (IEnumerable<s
355355
returnrules;
356356
}
357357

358+
/// <summary>
359+
/// Sets up forward port forwarding via 'adb -s &lt;serial&gt; forward &lt;local&gt; &lt;remote&gt;'.
360+
/// The host-side &lt;local&gt; socket is forwarded to the device-side &lt;remote&gt; socket,
361+
/// the symmetric pair to <see cref="ReversePortAsync"/>.
362+
/// </summary>
363+
/// <param name="serial">Device serial number.</param>
364+
/// <param name="local">Local (host-side) port spec.</param>
365+
/// <param name="remote">Remote (device-side) port spec.</param>
366+
/// <param name="cancellationToken">Cancellation token.</param>
367+
publicvirtualasyncTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default)
368+
{
369+
if(string.IsNullOrWhiteSpace(serial))
370+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
371+
if(localisnull)
372+
thrownewArgumentNullException(nameof(local));
373+
if(remoteisnull)
374+
thrownewArgumentNullException(nameof(remote));
375+
if(local.Port<=0||local.Port>65535)
376+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
377+
if(remote.Port<=0||remote.Port>65535)
378+
thrownewArgumentOutOfRangeException(nameof(remote),remote.Port,"Port must be between 1 and 65535.");
379+
380+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward",local.ToSocketSpec(),remote.ToSocketSpec());
381+
usingvarstdout=newStringWriter();
382+
usingvarstderr=newStringWriter();
383+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
384+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward {local}{remote}",stderr,stdout);
385+
}
386+
387+
/// <summary>
388+
/// Removes a specific forward port forwarding rule via
389+
/// 'adb -s &lt;serial&gt; forward --remove &lt;local&gt;'.
390+
/// </summary>
391+
/// <param name="serial">Device serial number.</param>
392+
/// <param name="local">Local (host-side) port spec to remove.</param>
393+
/// <param name="cancellationToken">Cancellation token.</param>
394+
publicvirtualasyncTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default)
395+
{
396+
if(string.IsNullOrWhiteSpace(serial))
397+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
398+
if(localisnull)
399+
thrownewArgumentNullException(nameof(local));
400+
if(local.Port<=0||local.Port>65535)
401+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
402+
403+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward","--remove",local.ToSocketSpec());
404+
usingvarstdout=newStringWriter();
405+
usingvarstderr=newStringWriter();
406+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
407+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward --remove {local}",stderr,stdout);
408+
}
409+
410+
/// <summary>
411+
/// Removes all forward port forwarding rules for the specified device.
412+
/// </summary>
413+
/// <remarks>
414+
/// The underlying <c>adb forward --remove-all</c> command (and its wire-protocol
415+
/// equivalent <c>host-serial:&lt;serial&gt;:killforward-all</c>) operates globally on the
416+
/// adb daemon — the <c>-s &lt;serial&gt;</c> flag does not scope it, and calling it
417+
/// would remove forwards for every connected device. To honour the per-device
418+
/// contract of this method, we list the forwards for <paramref name="serial"/>
419+
/// via <see cref="ListForwardPortsAsync"/> and remove them individually via
420+
/// <see cref="RemoveForwardPortAsync"/>.
421+
/// </remarks>
422+
publicvirtualasyncTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
423+
{
424+
if(string.IsNullOrWhiteSpace(serial))
425+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
426+
427+
varrules=awaitListForwardPortsAsync(serial,cancellationToken).ConfigureAwait(false);
428+
foreach(varruleinrules){
429+
cancellationToken.ThrowIfCancellationRequested();
430+
awaitRemoveForwardPortAsync(serial,rule.Local,cancellationToken).ConfigureAwait(false);
431+
}
432+
}
433+
434+
/// <summary>
435+
/// Lists active forward port forwarding rules for the specified device via
436+
/// 'adb forward --list'.
437+
/// The underlying command always lists rules across all devices, so the
438+
/// result is filtered to entries matching <paramref name="serial"/>.
439+
/// </summary>
440+
publicvirtualasyncTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
441+
{
442+
if(string.IsNullOrWhiteSpace(serial))
443+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
444+
445+
usingvarstdout=newStringWriter();
446+
usingvarstderr=newStringWriter();
447+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"forward","--list");
448+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
449+
ProcessUtils.ThrowIfFailed(exitCode,$"adb forward --list",stderr,stdout);
450+
451+
returnParseForwardListOutput(stdout.ToString().Split('\n'),serial);
452+
}
453+
454+
/// <summary>
455+
/// Parses the output of 'adb forward --list'.
456+
/// Each line is "&lt;serial&gt; &lt;local&gt; &lt;remote&gt;", e.g. "emulator-5554 tcp:5000 tcp:6000".
457+
/// Only rules matching <paramref name="serial"/> are returned. Lines with
458+
/// unparseable socket specs are skipped.
459+
/// </summary>
460+
/// <remarks>
461+
/// Note the field-order asymmetry vs <see cref="ParseReverseListOutput"/>:
462+
/// forward --list: &lt;serial&gt; &lt;local&gt; &lt;remote&gt;
463+
/// reverse --list: (reverse) &lt;remote&gt; &lt;local&gt;
464+
/// Both parsers construct an <see cref="AdbPortRule"/> whose constructor takes
465+
/// (Remote, Local), so the order in which we pass the parsed parts differs between
466+
/// the two parsers — keep that in mind when modifying either of them.
467+
/// </remarks>
468+
internalstaticIReadOnlyList<AdbPortRule>ParseForwardListOutput(IEnumerable<string>lines,stringserial)
469+
{
470+
varrules=newList<AdbPortRule>();
471+
if(string.IsNullOrEmpty(serial))
472+
returnrules;
473+
474+
foreach(varlineinlines){
475+
vartrimmed=line.Trim();
476+
if(string.IsNullOrEmpty(trimmed))
477+
continue;
478+
479+
// Expected format: "<serial> <local> <remote>" — see <remarks> above for
480+
// the field-order asymmetry with reverse --list.
481+
varparts=trimmed.Split((char[]?)null,StringSplitOptions.RemoveEmptyEntries);
482+
if(parts.Length<3)
483+
continue;
484+
485+
if(!string.Equals(parts[0],serial,StringComparison.Ordinal))
486+
continue;
487+
488+
varlocal=AdbPortSpec.TryParse(parts[1]);
489+
varremote=AdbPortSpec.TryParse(parts[2]);
490+
if(localis{}l&&remoteis{}r)
491+
rules.Add(newAdbPortRule(r,l));
492+
}
493+
494+
returnrules;
495+
}
496+
358497
/// <summary>
359498
/// Parses the output lines from 'adb devices -l'.
360499
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

0 commit comments

Comments
 (0)
, '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.

Commit 5165523

Browse files
rmarinhoCopilot
andauthored
[AdbRunner] Add ADB forward port management (#351)
(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>
1 parent 06a0867 commit 5165523

4 files changed

Lines changed: 427 additions & 22 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,145 @@ internal static IReadOnlyList<AdbPortRule> ParseReverseListOutput (IEnumerable<s
355355
returnrules;
356356
}
357357

358+
/// <summary>
359+
/// Sets up forward port forwarding via 'adb -s &lt;serial&gt; forward &lt;local&gt; &lt;remote&gt;'.
360+
/// The host-side &lt;local&gt; socket is forwarded to the device-side &lt;remote&gt; socket,
361+
/// the symmetric pair to <see cref="ReversePortAsync"/>.
362+
/// </summary>
363+
/// <param name="serial">Device serial number.</param>
364+
/// <param name="local">Local (host-side) port spec.</param>
365+
/// <param name="remote">Remote (device-side) port spec.</param>
366+
/// <param name="cancellationToken">Cancellation token.</param>
367+
publicvirtualasyncTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default)
368+
{
369+
if(string.IsNullOrWhiteSpace(serial))
370+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
371+
if(localisnull)
372+
thrownewArgumentNullException(nameof(local));
373+
if(remoteisnull)
374+
thrownewArgumentNullException(nameof(remote));
375+
if(local.Port<=0||local.Port>65535)
376+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
377+
if(remote.Port<=0||remote.Port>65535)
378+
thrownewArgumentOutOfRangeException(nameof(remote),remote.Port,"Port must be between 1 and 65535.");
379+
380+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward",local.ToSocketSpec(),remote.ToSocketSpec());
381+
usingvarstdout=newStringWriter();
382+
usingvarstderr=newStringWriter();
383+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
384+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward {local}{remote}",stderr,stdout);
385+
}
386+
387+
/// <summary>
388+
/// Removes a specific forward port forwarding rule via
389+
/// 'adb -s &lt;serial&gt; forward --remove &lt;local&gt;'.
390+
/// </summary>
391+
/// <param name="serial">Device serial number.</param>
392+
/// <param name="local">Local (host-side) port spec to remove.</param>
393+
/// <param name="cancellationToken">Cancellation token.</param>
394+
publicvirtualasyncTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default)
395+
{
396+
if(string.IsNullOrWhiteSpace(serial))
397+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
398+
if(localisnull)
399+
thrownewArgumentNullException(nameof(local));
400+
if(local.Port<=0||local.Port>65535)
401+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
402+
403+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward","--remove",local.ToSocketSpec());
404+
usingvarstdout=newStringWriter();
405+
usingvarstderr=newStringWriter();
406+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
407+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward --remove {local}",stderr,stdout);
408+
}
409+
410+
/// <summary>
411+
/// Removes all forward port forwarding rules for the specified device.
412+
/// </summary>
413+
/// <remarks>
414+
/// The underlying <c>adb forward --remove-all</c> command (and its wire-protocol
415+
/// equivalent <c>host-serial:&lt;serial&gt;:killforward-all</c>) operates globally on the
416+
/// adb daemon — the <c>-s &lt;serial&gt;</c> flag does not scope it, and calling it
417+
/// would remove forwards for every connected device. To honour the per-device
418+
/// contract of this method, we list the forwards for <paramref name="serial"/>
419+
/// via <see cref="ListForwardPortsAsync"/> and remove them individually via
420+
/// <see cref="RemoveForwardPortAsync"/>.
421+
/// </remarks>
422+
publicvirtualasyncTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
423+
{
424+
if(string.IsNullOrWhiteSpace(serial))
425+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
426+
427+
varrules=awaitListForwardPortsAsync(serial,cancellationToken).ConfigureAwait(false);
428+
foreach(varruleinrules){
429+
cancellationToken.ThrowIfCancellationRequested();
430+
awaitRemoveForwardPortAsync(serial,rule.Local,cancellationToken).ConfigureAwait(false);
431+
}
432+
}
433+
434+
/// <summary>
435+
/// Lists active forward port forwarding rules for the specified device via
436+
/// 'adb forward --list'.
437+
/// The underlying command always lists rules across all devices, so the
438+
/// result is filtered to entries matching <paramref name="serial"/>.
439+
/// </summary>
440+
publicvirtualasyncTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
441+
{
442+
if(string.IsNullOrWhiteSpace(serial))
443+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
444+
445+
usingvarstdout=newStringWriter();
446+
usingvarstderr=newStringWriter();
447+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"forward","--list");
448+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
449+
ProcessUtils.ThrowIfFailed(exitCode,$"adb forward --list",stderr,stdout);
450+
451+
returnParseForwardListOutput(stdout.ToString().Split('\n'),serial);
452+
}
453+
454+
/// <summary>
455+
/// Parses the output of 'adb forward --list'.
456+
/// Each line is "&lt;serial&gt; &lt;local&gt; &lt;remote&gt;", e.g. "emulator-5554 tcp:5000 tcp:6000".
457+
/// Only rules matching <paramref name="serial"/> are returned. Lines with
458+
/// unparseable socket specs are skipped.
459+
/// </summary>
460+
/// <remarks>
461+
/// Note the field-order asymmetry vs <see cref="ParseReverseListOutput"/>:
462+
/// forward --list: &lt;serial&gt; &lt;local&gt; &lt;remote&gt;
463+
/// reverse --list: (reverse) &lt;remote&gt; &lt;local&gt;
464+
/// Both parsers construct an <see cref="AdbPortRule"/> whose constructor takes
465+
/// (Remote, Local), so the order in which we pass the parsed parts differs between
466+
/// the two parsers — keep that in mind when modifying either of them.
467+
/// </remarks>
468+
internalstaticIReadOnlyList<AdbPortRule>ParseForwardListOutput(IEnumerable<string>lines,stringserial)
469+
{
470+
varrules=newList<AdbPortRule>();
471+
if(string.IsNullOrEmpty(serial))
472+
returnrules;
473+
474+
foreach(varlineinlines){
475+
vartrimmed=line.Trim();
476+
if(string.IsNullOrEmpty(trimmed))
477+
continue;
478+
479+
// Expected format: "<serial> <local> <remote>" — see <remarks> above for
480+
// the field-order asymmetry with reverse --list.
481+
varparts=trimmed.Split((char[]?)null,StringSplitOptions.RemoveEmptyEntries);
482+
if(parts.Length<3)
483+
continue;
484+
485+
if(!string.Equals(parts[0],serial,StringComparison.Ordinal))
486+
continue;
487+
488+
varlocal=AdbPortSpec.TryParse(parts[1]);
489+
varremote=AdbPortSpec.TryParse(parts[2]);
490+
if(localis{}l&&remoteis{}r)
491+
rules.Add(newAdbPortRule(r,l));
492+
}
493+
494+
returnrules;
495+
}
496+
358497
/// <summary>
359498
/// Parses the output lines from 'adb devices -l'.
360499
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

0 commit comments

Comments
 (0)
, '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.

Commit 5165523

Browse files
rmarinhoCopilot
andauthored
[AdbRunner] Add ADB forward port management (#351)
(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>
1 parent 06a0867 commit 5165523

4 files changed

Lines changed: 427 additions & 22 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,145 @@ internal static IReadOnlyList<AdbPortRule> ParseReverseListOutput (IEnumerable<s
355355
returnrules;
356356
}
357357

358+
/// <summary>
359+
/// Sets up forward port forwarding via 'adb -s &lt;serial&gt; forward &lt;local&gt; &lt;remote&gt;'.
360+
/// The host-side &lt;local&gt; socket is forwarded to the device-side &lt;remote&gt; socket,
361+
/// the symmetric pair to <see cref="ReversePortAsync"/>.
362+
/// </summary>
363+
/// <param name="serial">Device serial number.</param>
364+
/// <param name="local">Local (host-side) port spec.</param>
365+
/// <param name="remote">Remote (device-side) port spec.</param>
366+
/// <param name="cancellationToken">Cancellation token.</param>
367+
publicvirtualasyncTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default)
368+
{
369+
if(string.IsNullOrWhiteSpace(serial))
370+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
371+
if(localisnull)
372+
thrownewArgumentNullException(nameof(local));
373+
if(remoteisnull)
374+
thrownewArgumentNullException(nameof(remote));
375+
if(local.Port<=0||local.Port>65535)
376+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
377+
if(remote.Port<=0||remote.Port>65535)
378+
thrownewArgumentOutOfRangeException(nameof(remote),remote.Port,"Port must be between 1 and 65535.");
379+
380+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward",local.ToSocketSpec(),remote.ToSocketSpec());
381+
usingvarstdout=newStringWriter();
382+
usingvarstderr=newStringWriter();
383+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
384+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward {local}{remote}",stderr,stdout);
385+
}
386+
387+
/// <summary>
388+
/// Removes a specific forward port forwarding rule via
389+
/// 'adb -s &lt;serial&gt; forward --remove &lt;local&gt;'.
390+
/// </summary>
391+
/// <param name="serial">Device serial number.</param>
392+
/// <param name="local">Local (host-side) port spec to remove.</param>
393+
/// <param name="cancellationToken">Cancellation token.</param>
394+
publicvirtualasyncTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default)
395+
{
396+
if(string.IsNullOrWhiteSpace(serial))
397+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
398+
if(localisnull)
399+
thrownewArgumentNullException(nameof(local));
400+
if(local.Port<=0||local.Port>65535)
401+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
402+
403+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward","--remove",local.ToSocketSpec());
404+
usingvarstdout=newStringWriter();
405+
usingvarstderr=newStringWriter();
406+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
407+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward --remove {local}",stderr,stdout);
408+
}
409+
410+
/// <summary>
411+
/// Removes all forward port forwarding rules for the specified device.
412+
/// </summary>
413+
/// <remarks>
414+
/// The underlying <c>adb forward --remove-all</c> command (and its wire-protocol
415+
/// equivalent <c>host-serial:&lt;serial&gt;:killforward-all</c>) operates globally on the
416+
/// adb daemon — the <c>-s &lt;serial&gt;</c> flag does not scope it, and calling it
417+
/// would remove forwards for every connected device. To honour the per-device
418+
/// contract of this method, we list the forwards for <paramref name="serial"/>
419+
/// via <see cref="ListForwardPortsAsync"/> and remove them individually via
420+
/// <see cref="RemoveForwardPortAsync"/>.
421+
/// </remarks>
422+
publicvirtualasyncTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
423+
{
424+
if(string.IsNullOrWhiteSpace(serial))
425+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
426+
427+
varrules=awaitListForwardPortsAsync(serial,cancellationToken).ConfigureAwait(false);
428+
foreach(varruleinrules){
429+
cancellationToken.ThrowIfCancellationRequested();
430+
awaitRemoveForwardPortAsync(serial,rule.Local,cancellationToken).ConfigureAwait(false);
431+
}
432+
}
433+
434+
/// <summary>
435+
/// Lists active forward port forwarding rules for the specified device via
436+
/// 'adb forward --list'.
437+
/// The underlying command always lists rules across all devices, so the
438+
/// result is filtered to entries matching <paramref name="serial"/>.
439+
/// </summary>
440+
publicvirtualasyncTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
441+
{
442+
if(string.IsNullOrWhiteSpace(serial))
443+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
444+
445+
usingvarstdout=newStringWriter();
446+
usingvarstderr=newStringWriter();
447+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"forward","--list");
448+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
449+
ProcessUtils.ThrowIfFailed(exitCode,$"adb forward --list",stderr,stdout);
450+
451+
returnParseForwardListOutput(stdout.ToString().Split('\n'),serial);
452+
}
453+
454+
/// <summary>
455+
/// Parses the output of 'adb forward --list'.
456+
/// Each line is "&lt;serial&gt; &lt;local&gt; &lt;remote&gt;", e.g. "emulator-5554 tcp:5000 tcp:6000".
457+
/// Only rules matching <paramref name="serial"/> are returned. Lines with
458+
/// unparseable socket specs are skipped.
459+
/// </summary>
460+
/// <remarks>
461+
/// Note the field-order asymmetry vs <see cref="ParseReverseListOutput"/>:
462+
/// forward --list: &lt;serial&gt; &lt;local&gt; &lt;remote&gt;
463+
/// reverse --list: (reverse) &lt;remote&gt; &lt;local&gt;
464+
/// Both parsers construct an <see cref="AdbPortRule"/> whose constructor takes
465+
/// (Remote, Local), so the order in which we pass the parsed parts differs between
466+
/// the two parsers — keep that in mind when modifying either of them.
467+
/// </remarks>
468+
internalstaticIReadOnlyList<AdbPortRule>ParseForwardListOutput(IEnumerable<string>lines,stringserial)
469+
{
470+
varrules=newList<AdbPortRule>();
471+
if(string.IsNullOrEmpty(serial))
472+
returnrules;
473+
474+
foreach(varlineinlines){
475+
vartrimmed=line.Trim();
476+
if(string.IsNullOrEmpty(trimmed))
477+
continue;
478+
479+
// Expected format: "<serial> <local> <remote>" — see <remarks> above for
480+
// the field-order asymmetry with reverse --list.
481+
varparts=trimmed.Split((char[]?)null,StringSplitOptions.RemoveEmptyEntries);
482+
if(parts.Length<3)
483+
continue;
484+
485+
if(!string.Equals(parts[0],serial,StringComparison.Ordinal))
486+
continue;
487+
488+
varlocal=AdbPortSpec.TryParse(parts[1]);
489+
varremote=AdbPortSpec.TryParse(parts[2]);
490+
if(localis{}l&&remoteis{}r)
491+
rules.Add(newAdbPortRule(r,l));
492+
}
493+
494+
returnrules;
495+
}
496+
358497
/// <summary>
359498
/// Parses the output lines from 'adb devices -l'.
360499
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

0 commit comments

Comments
 (0)
, '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.

Commit 5165523

Browse files
rmarinhoCopilot
andauthored
[AdbRunner] Add ADB forward port management (#351)
(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>
1 parent 06a0867 commit 5165523

4 files changed

Lines changed: 427 additions & 22 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,145 @@ internal static IReadOnlyList<AdbPortRule> ParseReverseListOutput (IEnumerable<s
355355
returnrules;
356356
}
357357

358+
/// <summary>
359+
/// Sets up forward port forwarding via 'adb -s &lt;serial&gt; forward &lt;local&gt; &lt;remote&gt;'.
360+
/// The host-side &lt;local&gt; socket is forwarded to the device-side &lt;remote&gt; socket,
361+
/// the symmetric pair to <see cref="ReversePortAsync"/>.
362+
/// </summary>
363+
/// <param name="serial">Device serial number.</param>
364+
/// <param name="local">Local (host-side) port spec.</param>
365+
/// <param name="remote">Remote (device-side) port spec.</param>
366+
/// <param name="cancellationToken">Cancellation token.</param>
367+
publicvirtualasyncTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default)
368+
{
369+
if(string.IsNullOrWhiteSpace(serial))
370+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
371+
if(localisnull)
372+
thrownewArgumentNullException(nameof(local));
373+
if(remoteisnull)
374+
thrownewArgumentNullException(nameof(remote));
375+
if(local.Port<=0||local.Port>65535)
376+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
377+
if(remote.Port<=0||remote.Port>65535)
378+
thrownewArgumentOutOfRangeException(nameof(remote),remote.Port,"Port must be between 1 and 65535.");
379+
380+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward",local.ToSocketSpec(),remote.ToSocketSpec());
381+
usingvarstdout=newStringWriter();
382+
usingvarstderr=newStringWriter();
383+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
384+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward {local}{remote}",stderr,stdout);
385+
}
386+
387+
/// <summary>
388+
/// Removes a specific forward port forwarding rule via
389+
/// 'adb -s &lt;serial&gt; forward --remove &lt;local&gt;'.
390+
/// </summary>
391+
/// <param name="serial">Device serial number.</param>
392+
/// <param name="local">Local (host-side) port spec to remove.</param>
393+
/// <param name="cancellationToken">Cancellation token.</param>
394+
publicvirtualasyncTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default)
395+
{
396+
if(string.IsNullOrWhiteSpace(serial))
397+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
398+
if(localisnull)
399+
thrownewArgumentNullException(nameof(local));
400+
if(local.Port<=0||local.Port>65535)
401+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
402+
403+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward","--remove",local.ToSocketSpec());
404+
usingvarstdout=newStringWriter();
405+
usingvarstderr=newStringWriter();
406+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
407+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward --remove {local}",stderr,stdout);
408+
}
409+
410+
/// <summary>
411+
/// Removes all forward port forwarding rules for the specified device.
412+
/// </summary>
413+
/// <remarks>
414+
/// The underlying <c>adb forward --remove-all</c> command (and its wire-protocol
415+
/// equivalent <c>host-serial:&lt;serial&gt;:killforward-all</c>) operates globally on the
416+
/// adb daemon — the <c>-s &lt;serial&gt;</c> flag does not scope it, and calling it
417+
/// would remove forwards for every connected device. To honour the per-device
418+
/// contract of this method, we list the forwards for <paramref name="serial"/>
419+
/// via <see cref="ListForwardPortsAsync"/> and remove them individually via
420+
/// <see cref="RemoveForwardPortAsync"/>.
421+
/// </remarks>
422+
publicvirtualasyncTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
423+
{
424+
if(string.IsNullOrWhiteSpace(serial))
425+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
426+
427+
varrules=awaitListForwardPortsAsync(serial,cancellationToken).ConfigureAwait(false);
428+
foreach(varruleinrules){
429+
cancellationToken.ThrowIfCancellationRequested();
430+
awaitRemoveForwardPortAsync(serial,rule.Local,cancellationToken).ConfigureAwait(false);
431+
}
432+
}
433+
434+
/// <summary>
435+
/// Lists active forward port forwarding rules for the specified device via
436+
/// 'adb forward --list'.
437+
/// The underlying command always lists rules across all devices, so the
438+
/// result is filtered to entries matching <paramref name="serial"/>.
439+
/// </summary>
440+
publicvirtualasyncTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
441+
{
442+
if(string.IsNullOrWhiteSpace(serial))
443+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
444+
445+
usingvarstdout=newStringWriter();
446+
usingvarstderr=newStringWriter();
447+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"forward","--list");
448+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
449+
ProcessUtils.ThrowIfFailed(exitCode,$"adb forward --list",stderr,stdout);
450+
451+
returnParseForwardListOutput(stdout.ToString().Split('\n'),serial);
452+
}
453+
454+
/// <summary>
455+
/// Parses the output of 'adb forward --list'.
456+
/// Each line is "&lt;serial&gt; &lt;local&gt; &lt;remote&gt;", e.g. "emulator-5554 tcp:5000 tcp:6000".
457+
/// Only rules matching <paramref name="serial"/> are returned. Lines with
458+
/// unparseable socket specs are skipped.
459+
/// </summary>
460+
/// <remarks>
461+
/// Note the field-order asymmetry vs <see cref="ParseReverseListOutput"/>:
462+
/// forward --list: &lt;serial&gt; &lt;local&gt; &lt;remote&gt;
463+
/// reverse --list: (reverse) &lt;remote&gt; &lt;local&gt;
464+
/// Both parsers construct an <see cref="AdbPortRule"/> whose constructor takes
465+
/// (Remote, Local), so the order in which we pass the parsed parts differs between
466+
/// the two parsers — keep that in mind when modifying either of them.
467+
/// </remarks>
468+
internalstaticIReadOnlyList<AdbPortRule>ParseForwardListOutput(IEnumerable<string>lines,stringserial)
469+
{
470+
varrules=newList<AdbPortRule>();
471+
if(string.IsNullOrEmpty(serial))
472+
returnrules;
473+
474+
foreach(varlineinlines){
475+
vartrimmed=line.Trim();
476+
if(string.IsNullOrEmpty(trimmed))
477+
continue;
478+
479+
// Expected format: "<serial> <local> <remote>" — see <remarks> above for
480+
// the field-order asymmetry with reverse --list.
481+
varparts=trimmed.Split((char[]?)null,StringSplitOptions.RemoveEmptyEntries);
482+
if(parts.Length<3)
483+
continue;
484+
485+
if(!string.Equals(parts[0],serial,StringComparison.Ordinal))
486+
continue;
487+
488+
varlocal=AdbPortSpec.TryParse(parts[1]);
489+
varremote=AdbPortSpec.TryParse(parts[2]);
490+
if(localis{}l&&remoteis{}r)
491+
rules.Add(newAdbPortRule(r,l));
492+
}
493+
494+
returnrules;
495+
}
496+
358497
/// <summary>
359498
/// Parses the output lines from 'adb devices -l'.
360499
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

0 commit comments

Comments
 (0)
, '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.

Commit 5165523

Browse files
rmarinhoCopilot
andauthored
[AdbRunner] Add ADB forward port management (#351)
(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>
1 parent 06a0867 commit 5165523

4 files changed

Lines changed: 427 additions & 22 deletions

File tree

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
201201
virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202+
virtual Xamarin.Android.Tools.AdbRunner.ForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
203+
virtual Xamarin.Android.Tools.AdbRunner.ListForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbPortRule!>!>!
204+
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllForwardPortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
205+
virtual Xamarin.Android.Tools.AdbRunner.RemoveForwardPortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
202206
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203207
Xamarin.Android.Tools.AvdDeviceProfile
204208
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id) -> void

‎src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,145 @@ internal static IReadOnlyList<AdbPortRule> ParseReverseListOutput (IEnumerable<s
355355
returnrules;
356356
}
357357

358+
/// <summary>
359+
/// Sets up forward port forwarding via 'adb -s &lt;serial&gt; forward &lt;local&gt; &lt;remote&gt;'.
360+
/// The host-side &lt;local&gt; socket is forwarded to the device-side &lt;remote&gt; socket,
361+
/// the symmetric pair to <see cref="ReversePortAsync"/>.
362+
/// </summary>
363+
/// <param name="serial">Device serial number.</param>
364+
/// <param name="local">Local (host-side) port spec.</param>
365+
/// <param name="remote">Remote (device-side) port spec.</param>
366+
/// <param name="cancellationToken">Cancellation token.</param>
367+
publicvirtualasyncTaskForwardPortAsync(stringserial,AdbPortSpeclocal,AdbPortSpecremote,CancellationTokencancellationToken=default)
368+
{
369+
if(string.IsNullOrWhiteSpace(serial))
370+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
371+
if(localisnull)
372+
thrownewArgumentNullException(nameof(local));
373+
if(remoteisnull)
374+
thrownewArgumentNullException(nameof(remote));
375+
if(local.Port<=0||local.Port>65535)
376+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
377+
if(remote.Port<=0||remote.Port>65535)
378+
thrownewArgumentOutOfRangeException(nameof(remote),remote.Port,"Port must be between 1 and 65535.");
379+
380+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward",local.ToSocketSpec(),remote.ToSocketSpec());
381+
usingvarstdout=newStringWriter();
382+
usingvarstderr=newStringWriter();
383+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
384+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward {local}{remote}",stderr,stdout);
385+
}
386+
387+
/// <summary>
388+
/// Removes a specific forward port forwarding rule via
389+
/// 'adb -s &lt;serial&gt; forward --remove &lt;local&gt;'.
390+
/// </summary>
391+
/// <param name="serial">Device serial number.</param>
392+
/// <param name="local">Local (host-side) port spec to remove.</param>
393+
/// <param name="cancellationToken">Cancellation token.</param>
394+
publicvirtualasyncTaskRemoveForwardPortAsync(stringserial,AdbPortSpeclocal,CancellationTokencancellationToken=default)
395+
{
396+
if(string.IsNullOrWhiteSpace(serial))
397+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
398+
if(localisnull)
399+
thrownewArgumentNullException(nameof(local));
400+
if(local.Port<=0||local.Port>65535)
401+
thrownewArgumentOutOfRangeException(nameof(local),local.Port,"Port must be between 1 and 65535.");
402+
403+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"forward","--remove",local.ToSocketSpec());
404+
usingvarstdout=newStringWriter();
405+
usingvarstderr=newStringWriter();
406+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
407+
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} forward --remove {local}",stderr,stdout);
408+
}
409+
410+
/// <summary>
411+
/// Removes all forward port forwarding rules for the specified device.
412+
/// </summary>
413+
/// <remarks>
414+
/// The underlying <c>adb forward --remove-all</c> command (and its wire-protocol
415+
/// equivalent <c>host-serial:&lt;serial&gt;:killforward-all</c>) operates globally on the
416+
/// adb daemon — the <c>-s &lt;serial&gt;</c> flag does not scope it, and calling it
417+
/// would remove forwards for every connected device. To honour the per-device
418+
/// contract of this method, we list the forwards for <paramref name="serial"/>
419+
/// via <see cref="ListForwardPortsAsync"/> and remove them individually via
420+
/// <see cref="RemoveForwardPortAsync"/>.
421+
/// </remarks>
422+
publicvirtualasyncTaskRemoveAllForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
423+
{
424+
if(string.IsNullOrWhiteSpace(serial))
425+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
426+
427+
varrules=awaitListForwardPortsAsync(serial,cancellationToken).ConfigureAwait(false);
428+
foreach(varruleinrules){
429+
cancellationToken.ThrowIfCancellationRequested();
430+
awaitRemoveForwardPortAsync(serial,rule.Local,cancellationToken).ConfigureAwait(false);
431+
}
432+
}
433+
434+
/// <summary>
435+
/// Lists active forward port forwarding rules for the specified device via
436+
/// 'adb forward --list'.
437+
/// The underlying command always lists rules across all devices, so the
438+
/// result is filtered to entries matching <paramref name="serial"/>.
439+
/// </summary>
440+
publicvirtualasyncTask<IReadOnlyList<AdbPortRule>>ListForwardPortsAsync(stringserial,CancellationTokencancellationToken=default)
441+
{
442+
if(string.IsNullOrWhiteSpace(serial))
443+
thrownewArgumentException("Serial must not be empty.",nameof(serial));
444+
445+
usingvarstdout=newStringWriter();
446+
usingvarstderr=newStringWriter();
447+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"forward","--list");
448+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
449+
ProcessUtils.ThrowIfFailed(exitCode,$"adb forward --list",stderr,stdout);
450+
451+
returnParseForwardListOutput(stdout.ToString().Split('\n'),serial);
452+
}
453+
454+
/// <summary>
455+
/// Parses the output of 'adb forward --list'.
456+
/// Each line is "&lt;serial&gt; &lt;local&gt; &lt;remote&gt;", e.g. "emulator-5554 tcp:5000 tcp:6000".
457+
/// Only rules matching <paramref name="serial"/> are returned. Lines with
458+
/// unparseable socket specs are skipped.
459+
/// </summary>
460+
/// <remarks>
461+
/// Note the field-order asymmetry vs <see cref="ParseReverseListOutput"/>:
462+
/// forward --list: &lt;serial&gt; &lt;local&gt; &lt;remote&gt;
463+
/// reverse --list: (reverse) &lt;remote&gt; &lt;local&gt;
464+
/// Both parsers construct an <see cref="AdbPortRule"/> whose constructor takes
465+
/// (Remote, Local), so the order in which we pass the parsed parts differs between
466+
/// the two parsers — keep that in mind when modifying either of them.
467+
/// </remarks>
468+
internalstaticIReadOnlyList<AdbPortRule>ParseForwardListOutput(IEnumerable<string>lines,stringserial)
469+
{
470+
varrules=newList<AdbPortRule>();
471+
if(string.IsNullOrEmpty(serial))
472+
returnrules;
473+
474+
foreach(varlineinlines){
475+
vartrimmed=line.Trim();
476+
if(string.IsNullOrEmpty(trimmed))
477+
continue;
478+
479+
// Expected format: "<serial> <local> <remote>" — see <remarks> above for
480+
// the field-order asymmetry with reverse --list.
481+
varparts=trimmed.Split((char[]?)null,StringSplitOptions.RemoveEmptyEntries);
482+
if(parts.Length<3)
483+
continue;
484+
485+
if(!string.Equals(parts[0],serial,StringComparison.Ordinal))
486+
continue;
487+
488+
varlocal=AdbPortSpec.TryParse(parts[1]);
489+
varremote=AdbPortSpec.TryParse(parts[2]);
490+
if(localis{}l&&remoteis{}r)
491+
rules.Add(newAdbPortRule(r,l));
492+
}
493+
494+
returnrules;
495+
}
496+
358497
/// <summary>
359498
/// Parses the output lines from 'adb devices -l'.
360499
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

0 commit comments

Comments
 (0)