Skip to content

Add ADB device tracking (host:track-devices) for real-time device monitoring#327

Open
rmarinho wants to merge 1 commit intomainfrom
features/323-adb-device-tracker
Open

Add ADB device tracking (host:track-devices) for real-time device monitoring#327
rmarinho wants to merge 1 commit intomainfrom
features/323-adb-device-tracker

Conversation

@rmarinho
Copy link
Copy Markdown
Member

@rmarinho rmarinho commented Apr 8, 2026

Summary

Add support for real-time device connection/disconnection monitoring via the ADB daemon socket protocol (\host:track-devices-l).

Changes

  • Added \AdbDeviceTracker\ class implementing \IDisposable\
  • Socket connection to \localhost:5037\ (ADB daemon)
  • Sends \host:track-devices-l\ command and reads length-prefixed device list updates
  • Callback-based \StartAsync()\ with \CancellationToken\ support
  • \CurrentDevices\ snapshot property for current device state
  • Auto-reconnect with exponential backoff (500ms → 16s) on connection drops
  • Reuses existing \AdbRunner.ParseAdbDevicesOutput()\ for parsing
  • 11 unit tests covering protocol parsing, lifecycle, and edge cases
  • Updated \PublicAPI.Unshipped.txt\ for both
    et10.0\ and
    etstandard2.0\

API

\\csharp
public sealed class AdbDeviceTracker : IDisposable
{
public AdbDeviceTracker(string? adbPath = null, int port = 5037, ...);
public IReadOnlyList CurrentDevices { get; }
public Task StartAsync(Action<IReadOnlyList> onDevicesChanged, CancellationToken cancellationToken = default);
public void Dispose();
}
\\

Closes #323

Add AdbDeviceTracker class for real-time device connection monitoring
via the ADB daemon socket protocol. Connects to localhost:5037, sends
host:track-devices-l, and pushes device list updates through a callback.

Features:
- Auto-reconnect with exponential backoff (500ms to 16s)
- Callback-based StartAsync() with CancellationToken support
- CurrentDevices snapshot property
- IDisposable lifecycle management
- Reuses AdbRunner.ParseAdbDevicesOutput() for parsing

Includes 11 unit tests covering protocol parsing, edge cases, and
lifecycle management. PublicAPI entries for both net10.0 and netstandard2.0.

Closes #323

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 8, 2026 16:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new AdbDeviceTracker public API to Xamarin.Android.Tools.AndroidSdk to monitor ADB device connect/disconnect events in real time using the host:track-devices-l daemon socket protocol.

Changes:

  • Introduces AdbDeviceTracker with reconnect/backoff, snapshot state (CurrentDevices), and callback-driven tracking via StartAsync.
  • Adds unit tests validating lifecycle guards and length-prefixed protocol parsing behavior.
  • Updates PublicAPI unshipped files for netstandard2.0 and net10.0.

Reviewed changes

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

File Description
tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbDeviceTrackerTests.cs Adds tests for tracker lifecycle and length-prefixed message parsing.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbDeviceTracker.cs Implements TCP-based host:track-devices-l tracking loop with reconnect/backoff and parsing.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt Registers the new public API surface for netstandard2.0.
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt Registers the new public API surface for net10.0.

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

using System.Linq; is not used in this file and will trigger CS8019 (and can fail Release builds when warnings are treated as errors). Remove the unnecessary using.

Suggested change
using System.Linq;

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +46
readonly int port;
readonly Action<TraceLevel, string> logger;
readonly string? adbPath;
readonly IDictionary<string, string>? environmentVariables;
IReadOnlyList<AdbDeviceInfo> currentDevices = Array.Empty<AdbDeviceInfo> ();
CancellationTokenSource? trackingCts;
bool disposed;

/// <summary>
/// Creates a new AdbDeviceTracker.
/// </summary>
/// <param name="adbPath">Optional path to the adb executable for starting the server if needed.</param>
/// <param name="port">ADB daemon port (default 5037).</param>
/// <param name="environmentVariables">Optional environment variables for adb processes.</param>
/// <param name="logger">Optional logger callback.</param>
public AdbDeviceTracker (string? adbPath = null, int port = 5037,
IDictionary<string, string>? environmentVariables = null,
Action<TraceLevel, string>? logger = null)
{
if (port <= 0 || port > 65535)
throw new ArgumentOutOfRangeException (nameof (port), "Port must be between 1 and 65535.");
this.adbPath = adbPath;
this.port = port;
this.environmentVariables = environmentVariables;
this.logger = logger ?? RunnerDefaults.NullLogger;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

adbPath and environmentVariables are assigned but never used, which will raise CS0414 (and can fail Release builds when warnings are treated as errors). Either implement the advertised behavior (e.g., start/ensure the ADB server using adbPath and pass environmentVariables to that process) or remove these fields/ctor parameters to avoid a misleading public API surface.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +90
trackingCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
var token = trackingCts.Token;
var backoffMs = InitialBackoffMs;

while (!token.IsCancellationRequested) {
try {
await TrackDevicesAsync (onDevicesChanged, token).ConfigureAwait (false);
} catch (OperationCanceledException) when (token.IsCancellationRequested) {
break;
} catch (Exception ex) {
logger.Invoke (TraceLevel.Warning, $"ADB tracking connection lost: {ex.Message}. Reconnecting in {backoffMs}ms...");
try {
await Task.Delay (backoffMs, token).ConfigureAwait (false);
} catch (OperationCanceledException) {
break;
}
backoffMs = Math.Min (backoffMs * 2, MaxBackoffMs);
continue;
}
// Reset backoff on clean connection
backoffMs = InitialBackoffMs;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

StartAsync creates and stores a linked CancellationTokenSource but never disposes it when StartAsync completes (only on Dispose()), and repeated StartAsync calls will overwrite trackingCts. Consider using a local CTS with a try/finally to dispose it, and guarding against multiple concurrent starts (throw or no-op) to avoid leaks/races.

Suggested change
trackingCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
var token = trackingCts.Token;
var backoffMs = InitialBackoffMs;
while (!token.IsCancellationRequested) {
try {
await TrackDevicesAsync (onDevicesChanged, token).ConfigureAwait (false);
} catch (OperationCanceledException) when (token.IsCancellationRequested) {
break;
} catch (Exception ex) {
logger.Invoke (TraceLevel.Warning, $"ADB tracking connection lost: {ex.Message}. Reconnecting in {backoffMs}ms...");
try {
await Task.Delay (backoffMs, token).ConfigureAwait (false);
} catch (OperationCanceledException) {
break;
}
backoffMs = Math.Min (backoffMs * 2, MaxBackoffMs);
continue;
}
// Reset backoff on clean connection
backoffMs = InitialBackoffMs;
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
if (Interlocked.CompareExchange (ref trackingCts, linkedCts, null) != null) {
linkedCts.Dispose ();
throw new InvalidOperationException ("Device tracking has already been started.");
}
try {
var token = linkedCts.Token;
var backoffMs = InitialBackoffMs;
while (!token.IsCancellationRequested) {
try {
await TrackDevicesAsync (onDevicesChanged, token).ConfigureAwait (false);
} catch (OperationCanceledException) when (token.IsCancellationRequested) {
break;
} catch (Exception ex) {
logger.Invoke (TraceLevel.Warning, $"ADB tracking connection lost: {ex.Message}. Reconnecting in {backoffMs}ms...");
try {
await Task.Delay (backoffMs, token).ConfigureAwait (false);
} catch (OperationCanceledException) {
break;
}
backoffMs = Math.Min (backoffMs * 2, MaxBackoffMs);
continue;
}
// Reset backoff on clean connection
backoffMs = InitialBackoffMs;
}
} finally {
Interlocked.CompareExchange (ref trackingCts, null, linkedCts);
linkedCts.Dispose ();

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ADB device tracking (host:track-devices) for real-time device monitoring

2 participants