EmulatorRunner.LaunchEmulator: return EmulatorLaunchResult with serial, ports, and log path#340
Conversation
22a0a7f to
cb82c52
Compare
… log path Agent-Logs-Url: https://github.com/dotnet/android-tools/sessions/7b51f319-794a-4574-b504-246c49426705 Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
…epProcesses Agent-Logs-Url: https://github.com/dotnet/android-tools/sessions/7b51f319-794a-4574-b504-246c49426705 Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
- Replace null! default with constructor requiring Process and logPath - Remove banner section-separator comments from test file - Log stderr lines as Verbose instead of Warning (emulator routinely writes info to stderr) - Update PublicAPI files (remove Process.init, LogPath.init; add constructor) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cb82c52 to
3ec585d
Compare
|
/review |
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
Pull request overview
This PR enriches emulator launching in Xamarin.Android.Tools.AndroidSdk by returning an EmulatorLaunchResult (serial/ports/log path) instead of a raw Process, avoiding caller-side races and AOSP path assumptions.
Changes:
- Introduces
EmulatorLaunchResultto carry process metadata, ports/serial, log path, and aPortsResolvedAsyncsignal. - Updates
EmulatorRunner.LaunchEmulatorto support pre-assigned ports, log path resolution, and late-bound port parsing from emulator output. - Adds/updates tests and updates PublicAPI unshipped files for both TFMs.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs |
Changes LaunchEmulator to return EmulatorLaunchResult, resolve log path, and asynchronously resolve ports from output. |
src/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorLaunchResult.cs |
Adds the new public result type surfaced by LaunchEmulator. |
tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs |
Adds tests for pre-assigned ports, output-parsed ports, and log path resolution. |
src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/*/PublicAPI.Unshipped.txt |
Records the new/changed public API surface for netstandard2.0 and net10.0. |
There was a problem hiding this comment.
@copilot 🤖
What's good:
EmulatorLaunchResultis a clean, well-documented type that solves a real problem (callers no longer raceadb devicesor hardcode AOSP log paths)TryResolvePortsFromLineis nicely extracted as a testableinternal staticmethod- Thorough test coverage including unit tests for parsing and integration tests with fake emulators
- Good use of
TaskCompletionSourcewithRunContinuationsAsynchronouslyto avoid inline continuations ResolveAvdLogPathcorrectly respectsANDROID_AVD_HOME/ANDROID_USER_HOMEviaEnvironmentVariableNamesconstants- PublicAPI files updated for both TFMs
Issues:
| # | Severity | Category | Summary |
|---|---|---|---|
| 1 | Async pattern | Exited event can race with OutputDataReceived, faulting PortsResolvedAsync even when port lines were emitted |
|
| 2 | Error handling | consolePort doc says "even number in 5554–5682" but no validation is performed; adbPort without consolePort silently ignored |
|
| 3 | 💡 | Pattern | Stderr log level downgraded from Warning to Verbose — may bury important emulator errors |
| 4 | 💡 | Error handling | Bare catch { exitCode = -1; } doesn't log the exception |
CI: "Agent" check is still in progress; "Prepare" and "license/cla" passed.
Generated by Android Tools PR Reviewer for issue #340 · ● 3.7M
… adbPort validation - Add ArgumentNullException guard in EmulatorLaunchResult constructor - Change PortsResolvedAsync from public init to internal set - Throw when adbPort is provided without consolePort - Use dummy Process in TryResolvePortsFromLine tests (no more null!) - Update PublicAPI files (remove PortsResolvedAsync.init) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- consolePort doc: "typically an even number" instead of claiming a strict range - Log exception in bare catch when reading ExitCode in Exited handler Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ArgumentNullException guard for logPath in EmulatorLaunchResult - Use IsNullOrWhiteSpace instead of Length > 0 for logFile check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
API redesign per review feedback: - Make PortsResolvedAsync internal (no longer public API) - Add LaunchEmulatorAsync() that wraps LaunchEmulator + awaits port resolution - Callers use LaunchEmulator (sync, ports known) or LaunchEmulatorAsync (async, waits) Test cleanup: - Track Process from LaunchEmulator, kill by PID instead of by process name - Remove KillSleepProcesses/FindEmulatorProcess helpers - Reduce fake emulator sleep from 60s to 3-5s so lingering processes die quickly Bug fix: - Capture ExitCode before Dispose in BootEmulatorAsync (was reading after Dispose) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@copilot can you rebase and fix conflicts |
…eout Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
…unchEmulator Agent-Logs-Url: https://github.com/dotnet/android-tools/sessions/b2811729-bd23-44ab-a6d1-fe1b221d9c6a Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
Done — merged |
…494-810b-246682127c8d
|
/review |
|
✅ Android Tools PR Reviewer completed successfully! |
| if (!string.IsNullOrWhiteSpace (logFile)) { | ||
| resolvedLogPath = logFile!; | ||
| args.Add ("-logfile"); | ||
| args.Add (logFile!); | ||
| } else { |
| var result = LaunchEmulator (avdName, coldBoot, consolePort, adbPort, logFile, additionalArgs); | ||
|
|
||
| using var registration = cancellationToken.Register (() => { | ||
| try { result.Process.Kill (); } catch { } | ||
| }); | ||
|
|
||
| await result.PortsResolvedAsync.ConfigureAwait (false); | ||
| return result; |
| /// <returns> | ||
| /// An <see cref="EmulatorLaunchResult"/> with the running process and launch details. | ||
| /// Await <see cref="EmulatorLaunchResult.PortsResolvedAsync"/> before reading | ||
| /// <see cref="EmulatorLaunchResult.ConsolePort"/> / <see cref="EmulatorLaunchResult.Serial"/> | ||
| /// when ports were not pre-assigned. | ||
| /// </returns> |
There was a problem hiding this comment.
@copilot
Summary
Well-structured change that replaces a bare Process return with a rich EmulatorLaunchResult — the port/serial/log-path bundling eliminates caller boilerplate and the stdout/stderr parsing for late-bound ports is a solid approach. Good test coverage with unit tests for the parsing logic and integration tests for both pre-assigned and late-bound port paths. The cleanup of test helpers (TryKillProcess replacing GetProcessesByName patterns) is a welcome improvement.
Issues
| Severity | Category | File | Description |
|---|---|---|---|
| Async pattern | EmulatorRunner.cs:190-194 |
LaunchEmulatorAsync cancellation throws InvalidOperationException instead of OperationCanceledException — breaks the async cancellation contract |
|
| Pattern | EmulatorRunner.cs:97-100 |
Null-forgiving operator logFile! used after null check — use property pattern for proper flow analysis |
|
| 💡 | Code organization | EmulatorLaunchResult.cs:34-40 |
Thread-safety invariants for ConsolePort/AdbPort should be documented |
| 💡 | API design | EmulatorRunner.cs:208 |
Good testability via internal static TryResolvePortsFromLine |
Positive callouts
StartFireAndForgetis a clean extraction intoProcessUtils— follows the repo convention of centralizing process managementResolveAvdLogPathcorrectly usesEnvironmentVariableNamesconstants and respects the runner-levelenvironmentVariablesoverridesTaskCreationOptions.RunContinuationsAsynchronouslyon the TCS prevents potential deadlocks from inline continuations- Test improvements:
timeout /t N /nobreakreplacesping -n Non Windows, process cleanup is per-handle rather than by-name — both are more robust
Generated by Android Tools PR Reviewer for issue #340 · ● 4M
| using var registration = cancellationToken.Register (() => { | ||
| try { result.Process.Kill (); } catch { } | ||
| }); | ||
|
|
||
| await result.PortsResolvedAsync.ConfigureAwait (false); |
There was a problem hiding this comment.
🤖 cancellationToken, the registration kills the process, which triggers onExited → tcs.TrySetException(new InvalidOperationException(...)). The await then throws InvalidOperationException instead of the expected OperationCanceledException, breaking the standard async cancellation contract.
Callers that catch (OperationCanceledException) (including the existing BootEmulatorAsync) will not handle this correctly — the InvalidOperationException will propagate as an unhandled failure.
Consider wrapping the await to translate cancellation-caused faults:
try {
await result.PortsResolvedAsync.ConfigureAwait (false);
} catch (InvalidOperationException) when (cancellationToken.IsCancellationRequested) {
cancellationToken.ThrowIfCancellationRequested ();
}Rule: CancellationToken contract (Async pattern)
| if (!string.IsNullOrWhiteSpace (logFile)) { | ||
| resolvedLogPath = logFile!; |
There was a problem hiding this comment.
🤖 logFile! is used to silence the nullable warning after the IsNullOrWhiteSpace check, but the repo convention bans postfix ! after null checks. string.IsNullOrWhiteSpace doesn't provide the compiler with flow-analysis narrowing, so the workaround is a property pattern:
if (logFile is { } lf && !string.IsNullOrWhiteSpace (lf)) {
resolvedLogPath = lf;
args.Add ("-logfile");
args.Add (lf);
}This gives proper non-null flow analysis on all target frameworks. Same applies on line 100.
Rule: No null-forgiving operator
| public int? ConsolePort { get; internal set; } | ||
|
|
||
| /// <summary> | ||
| /// The emulator ADB port (e.g., 5555). Populated either from the pre-assigned | ||
| /// <c>-ports</c> argument or once <see cref="PortsResolvedAsync"/> completes. | ||
| /// </summary> | ||
| public int? AdbPort { get; internal set; } |
There was a problem hiding this comment.
🤖 💡 Code organization — ConsolePort and AdbPort are mutated from TryResolvePortsFromLine, which runs on process DataReceived event handler threads (stdout/stderr events can fire from different thread pool threads). The class doesn't document its thread-safety assumptions.
Per repo convention, consider adding a <remarks> on the class:
/// <remarks>
/// <see cref="ConsolePort"/> and <see cref="AdbPort"/> are written by process
/// output callbacks and should only be read after <see cref="PortsResolvedAsync"/> completes.
/// </remarks>This makes the ordering contract explicit for future callers.
Rule: Document thread-safety invariants
| /// <c>emulator: ADB Server has started successfully on port NNNN</c> (adb port) | ||
| /// These lines have been stable across emulator releases for years. | ||
| /// </remarks> | ||
| internal static void TryResolvePortsFromLine (string line, EmulatorLaunchResult result, TaskCompletionSource<bool> tcs) |
There was a problem hiding this comment.
🤖 💡 API design — Clean design choice making TryResolvePortsFromLine a testable internal static method. The unit tests (TryResolvePortsFromLine_ConsolePort_Parsed, _BothPorts_CompletesTask, _UnrelatedLine_NoEffect) provide good coverage of the parsing logic independent of process management.
Rule: Testability
LaunchEmulatorreturned a bareProcess, forcing callers to raceadb devicesto find the serial and hardcode AOSP path conventions to find the log file. All of this data is available toEmulatorRunnerat launch time.Changes
New
EmulatorLaunchResulttype (Models/EmulatorLaunchResult.cs)Replaces
Processas the return type ofLaunchEmulator:Process,Pid— process handle and OS PIDint? ConsolePort,int? AdbPort— populated immediately when pre-assigned, or oncePortsResolvedAsynccompletesstring? Serial— derived asemulator-{ConsolePort}, null until ports are knownstring LogPath— resolved at launch timeTask PortsResolvedAsync(internal) — already-completed when ports are pre-assigned; completes when both emulator port-announcement lines are parsed from stdout/stderr; faulted if the process exits before emitting themConstructor requires
ProcessandlogPath(non-null, validated).PortsResolvedAsyncisinternal set— onlyEmulatorRunnercontrols it.Updated
LaunchEmulatorsignaturePre-assigned ports — when
consolePortis provided,-ports console,adbis passed to the emulator andPortsResolvedAsyncis already completed, soSerialis known synchronously:Late-bound ports — when no port is pre-assigned, stdout/stderr are parsed for the stable emulator boot lines and
PortsResolvedAsynccompletes once both are found:LaunchEmulatorAsyncA companion
LaunchEmulatorAsyncmethod awaitsPortsResolvedAsyncbefore returning, providing a single awaitable entry point for callers that need ports resolved before proceeding.Log path resolution (
ResolveAvdLogPath)Priority: explicit
-logfilearg →ANDROID_AVD_HOMEenv var →ANDROID_USER_HOME/avd→~/.android/avd/<name>.avd/emulator.log. Runner-levelenvironmentVariablesoverrides are respected. Whitespace-onlylogFilevalues fall through to the default.ProcessUtils.StartFireAndForget(new internal)Added
ProcessUtils.StartFireAndForget(Process, onOutputLine, onErrorLine, onExited)to consolidate fire-and-forget process launching (event-handler setup, start, begin async reads).LaunchEmulatoruses it instead of managing those steps inline.Unix SIGINT isolation
On Unix, the emulator is now launched via
sh -c "trap '' INT; exec ..."so the emulator process survives Ctrl+C in the parent terminal. Merged frommain.BootEmulatorAsyncInternally updated to consume the new
LaunchEmulatorreturn — no change to its public signature orEmulatorBootResult.Public API
PublicAPI.Unshipped.txtupdated for bothnet10.0andnetstandard2.0.Tests
timeout /t N /nobreak >nulinstead ofping -n N 127.0.0.1 >nulProcessreturned byLaunchEmulatorrather than killing by process nameLaunchEmulator_SurvivesSigint,InvalidEmulatorBinary_ReturnsLaunchFailed,ShellQuote_EscapesSingleQuotes, plus unit tests forTryResolvePortsFromLineandResolveAvdLogPath