@@ -244,6 +244,117 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati
244244 return null ;
245245 }
246246
247+ /// <summary>
248+ /// Sets up reverse port forwarding via 'adb -s <serial> reverse <remote> <local>'.
249+ /// </summary>
250+ /// <param name="serial">Device serial number.</param>
251+ /// <param name="remote">Remote (device-side) port spec.</param>
252+ /// <param name="local">Local (host-side) port spec.</param>
253+ /// <param name="cancellationToken">Cancellation token.</param>
254+ public virtual async Task ReversePortAsync ( string serial , AdbPortSpec remote , AdbPortSpec local , CancellationToken cancellationToken = default )
255+ {
256+ if ( string . IsNullOrWhiteSpace ( serial ) )
257+ throw new ArgumentException ( "Serial must not be empty." , nameof ( serial ) ) ;
258+ if ( remote is null )
259+ throw new ArgumentNullException ( nameof ( remote ) ) ;
260+ if ( local is null )
261+ throw new ArgumentNullException ( nameof ( local ) ) ;
262+ if ( remote . Port <= 0 || remote . Port > 65535 )
263+ throw new ArgumentOutOfRangeException ( nameof ( remote ) , remote . Port , "Port must be between 1 and 65535." ) ;
264+ if ( local . Port <= 0 || local . Port > 65535 )
265+ throw new ArgumentOutOfRangeException ( nameof ( local ) , local . Port , "Port must be between 1 and 65535." ) ;
266+
267+ var psi = ProcessUtils . CreateProcessStartInfo ( adbPath , "-s" , serial , "reverse" , remote . ToSocketSpec ( ) , local . ToSocketSpec ( ) ) ;
268+ using var stderr = new StringWriter ( ) ;
269+ var exitCode = await ProcessUtils . StartProcess ( psi , null , stderr , cancellationToken , environmentVariables ) . ConfigureAwait ( false ) ;
270+ ProcessUtils . ThrowIfFailed ( exitCode , $ "adb -s { serial } reverse { remote } { local } ", stderr ) ;
271+ }
272+
273+ /// <summary>
274+ /// Removes a specific reverse port forwarding rule via
275+ /// 'adb -s <serial> reverse --remove <remote>'.
276+ /// </summary>
277+ /// <param name="serial">Device serial number.</param>
278+ /// <param name="remote">Remote (device-side) port spec to remove.</param>
279+ /// <param name="cancellationToken">Cancellation token.</param>
280+ public virtual async Task RemoveReversePortAsync ( string serial , AdbPortSpec remote , CancellationToken cancellationToken = default )
281+ {
282+ if ( string . IsNullOrWhiteSpace ( serial ) )
283+ throw new ArgumentException ( "Serial must not be empty." , nameof ( serial ) ) ;
284+ if ( remote is null )
285+ throw new ArgumentNullException ( nameof ( remote ) ) ;
286+ if ( remote . Port <= 0 || remote . Port > 65535 )
287+ throw new ArgumentOutOfRangeException ( nameof ( remote ) , remote . Port , "Port must be between 1 and 65535." ) ;
288+
289+ var psi = ProcessUtils . CreateProcessStartInfo ( adbPath , "-s" , serial , "reverse" , "--remove" , remote . ToSocketSpec ( ) ) ;
290+ using var stderr = new StringWriter ( ) ;
291+ var exitCode = await ProcessUtils . StartProcess ( psi , null , stderr , cancellationToken , environmentVariables ) . ConfigureAwait ( false ) ;
292+ ProcessUtils . ThrowIfFailed ( exitCode , $ "adb -s { serial } reverse --remove { remote } ", stderr ) ;
293+ }
294+
295+ /// <summary>
296+ /// Removes all reverse port forwarding rules via
297+ /// 'adb -s <serial> reverse --remove-all'.
298+ /// </summary>
299+ public virtual async Task RemoveAllReversePortsAsync ( string serial , CancellationToken cancellationToken = default )
300+ {
301+ if ( string . IsNullOrWhiteSpace ( serial ) )
302+ throw new ArgumentException ( "Serial must not be empty." , nameof ( serial ) ) ;
303+
304+ var psi = ProcessUtils . CreateProcessStartInfo ( adbPath , "-s" , serial , "reverse" , "--remove-all" ) ;
305+ using var stderr = new StringWriter ( ) ;
306+ var exitCode = await ProcessUtils . StartProcess ( psi , null , stderr , cancellationToken , environmentVariables ) . ConfigureAwait ( false ) ;
307+ ProcessUtils . ThrowIfFailed ( exitCode , $ "adb -s { serial } reverse --remove-all", stderr ) ;
308+ }
309+
310+ /// <summary>
311+ /// Lists all active reverse port forwarding rules via
312+ /// 'adb -s <serial> reverse --list'.
313+ /// </summary>
314+ public virtual async Task < IReadOnlyList < AdbPortRule > > ListReversePortsAsync ( string serial , CancellationToken cancellationToken = default )
315+ {
316+ if ( string . IsNullOrWhiteSpace ( serial ) )
317+ throw new ArgumentException ( "Serial must not be empty." , nameof ( serial ) ) ;
318+
319+ using var stdout = new StringWriter ( ) ;
320+ using var stderr = new StringWriter ( ) ;
321+ var psi = ProcessUtils . CreateProcessStartInfo ( adbPath , "-s" , serial , "reverse" , "--list" ) ;
322+ var exitCode = await ProcessUtils . StartProcess ( psi , stdout , stderr , cancellationToken , environmentVariables ) . ConfigureAwait ( false ) ;
323+ ProcessUtils . ThrowIfFailed ( exitCode , $ "adb -s { serial } reverse --list", stderr , stdout ) ;
324+
325+ return ParseReverseListOutput ( stdout . ToString ( ) . Split ( '\n ' ) ) ;
326+ }
327+
328+ /// <summary>
329+ /// Parses the output of 'adb reverse --list'.
330+ /// Each line is "(reverse) <remote> <local>", e.g. "(reverse) tcp:5000 tcp:5000".
331+ /// Lines with unparseable socket specs are skipped.
332+ /// </summary>
333+ internal static IReadOnlyList < AdbPortRule > ParseReverseListOutput ( IEnumerable < string > lines )
334+ {
335+ var rules = new List < AdbPortRule > ( ) ;
336+
337+ foreach ( var line in lines ) {
338+ var trimmed = line . Trim ( ) ;
339+ if ( string . IsNullOrEmpty ( trimmed ) )
340+ continue ;
341+
342+ // Expected format: "(reverse) tcp:5000 tcp:5000"
343+ if ( ! trimmed . StartsWith ( "(reverse)" , StringComparison . Ordinal ) )
344+ continue ;
345+
346+ var parts = trimmed . Substring ( "(reverse)" . Length ) . Trim ( ) . Split ( ( char [ ] ? ) null , StringSplitOptions . RemoveEmptyEntries ) ;
347+ if ( parts . Length >= 2 ) {
348+ var remote = AdbPortSpec . TryParse ( parts [ 0 ] ) ;
349+ var local = AdbPortSpec . TryParse ( parts [ 1 ] ) ;
350+ if ( remote is { } r && local is { } l )
351+ rules . Add ( new AdbPortRule ( r , l ) ) ;
352+ }
353+ }
354+
355+ return rules ;
356+ }
357+
247358 /// <summary>
248359 /// Parses the output lines from 'adb devices -l'.
249360 /// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.
0 commit comments