From 7c91cdef2a07537e61fb590cf5bcaaa2080a81b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:15:01 +0000 Subject: [PATCH 1/5] Initial plan From 21c55e4aae1959e98365bafbf3bc36066d10272c Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 19 Feb 2026 16:20:18 -0800 Subject: [PATCH 2/5] Wait for Package Manager readiness after emulator boot completes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sys.boot_completed fires before the Android Package Manager is fully initialized. When CheckAdbTarget runs 'pm path com.android.shell' during this window, it returns a non-zero exit code, causing AcquireAndroidTarget to launch a second emulator on the same port. Two QEMU processes on the same port destabilizes adb — am instrument produces zero output and subsequent PM queries fail with 'Can't find service: package', ultimately causing NUnit2 result file not found errors. Add a Phase 2 poll to WaitForAndroidEmulator that verifies PM readiness via 'adb shell pm path com.android.shell' after sys.boot_completed=1, using ShouldRun lambdas to alternate between the boot check and PM check commands. Fixes https://github.com/dotnet/android/issues/10847 --- .../WaitForAndroidEmulator.cs | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs index 94729534194..a2d6023924a 100644 --- a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs +++ b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs @@ -2,15 +2,25 @@ using System.Collections.Generic; using System.Threading; +using Microsoft.Build.Framework; + namespace Xamarin.Android.Tools.BootstrapTasks { public class WaitForAndroidEmulator : Adb { + const int StateCheckBoot = 0; + const int StateCheckPM = 1; + int bootCompleted = -1; + bool pmReady; + int currentState = -1; + bool waitingForPM; public override bool Execute () { var endTime = DateTime.UtcNow.AddMilliseconds (Timeout); + + // Phase 1: wait for sys.boot_completed while (DateTime.UtcNow < endTime && bootCompleted != 1) { base.Execute (); Thread.Sleep (3000); @@ -18,6 +28,28 @@ public override bool Execute () if (bootCompleted != 1) { Log.LogError ($"Emulator '{AdbTarget}' did not finish launching in {Timeout} ms."); + return !Log.HasLoggedErrors; + } + + // Phase 2: wait for Package Manager to be ready. + // sys.boot_completed fires before PM is fully initialized, and + // subsequent adb checks that query PM can fail if we return too + // early, causing a second emulator to be launched on the same + // port (see CheckAdbTarget). + waitingForPM = true; + Log.LogMessage (MessageImportance.Normal, "Boot completed, waiting for Package Manager..."); + while (DateTime.UtcNow < endTime && !pmReady) { + pmReady = false; + base.Execute (); + if (pmReady) + break; + Thread.Sleep (3000); + } + + if (!pmReady) { + Log.LogError ($"Emulator '{AdbTarget}' Package Manager did not become ready in {Timeout} ms."); + } else { + Log.LogMessage (MessageImportance.Normal, "Package Manager is ready."); } return !Log.HasLoggedErrors; @@ -28,19 +60,44 @@ public override bool Execute () return new List { new CommandInfo { ArgumentsString = $"{AdbTarget} shell getprop sys.boot_completed", + ShouldRun = () => !waitingForPM, + }, + new CommandInfo { + ArgumentsString = $"{AdbTarget} shell pm path com.android.shell", + IgnoreExitCode = true, + LogIgnoredExitCodeAsWarning = false, + MergeStdoutAndStderr = false, + ShouldRun = () => waitingForPM, }, }; } + protected override void BeforeCommand (int commandIndex, CommandInfo info) + { + currentState = commandIndex; + } + protected override void ProcessStdout (string line) { if (string.IsNullOrEmpty (line)) return; - if (!int.TryParse (line, out int bootCompletedPropValue)) - return; + switch (currentState) { + case StateCheckBoot: + if (int.TryParse (line, out int bootCompletedPropValue)) + bootCompleted = bootCompletedPropValue; + break; - bootCompleted = bootCompletedPropValue; + case StateCheckPM: + if (line.StartsWith ("package:", StringComparison.OrdinalIgnoreCase)) + pmReady = true; + break; + } + } + + protected override void ProcessStderr (string line) + { + // PM errors on stderr should not mark the target as ready } } } From 6ed61371f97b8796cf171c178962367793a4ca94 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 20 Feb 2026 10:19:08 -0800 Subject: [PATCH 3/5] Simplify state management and error returns Derive command selection and output parsing from bootCompleted directly, eliminating currentState, BeforeCommand, waitingForPM, and the StateCheck constants. Return false directly after LogError instead of the redundant !Log.HasLoggedErrors idiom. --- .../WaitForAndroidEmulator.cs | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs index a2d6023924a..b833fa7fd79 100644 --- a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs +++ b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs @@ -8,13 +8,8 @@ namespace Xamarin.Android.Tools.BootstrapTasks { public class WaitForAndroidEmulator : Adb { - const int StateCheckBoot = 0; - const int StateCheckPM = 1; - int bootCompleted = -1; bool pmReady; - int currentState = -1; - bool waitingForPM; public override bool Execute () { @@ -23,12 +18,14 @@ public override bool Execute () // Phase 1: wait for sys.boot_completed while (DateTime.UtcNow < endTime && bootCompleted != 1) { base.Execute (); + if (bootCompleted == 1) + break; Thread.Sleep (3000); } if (bootCompleted != 1) { Log.LogError ($"Emulator '{AdbTarget}' did not finish launching in {Timeout} ms."); - return !Log.HasLoggedErrors; + return false; } // Phase 2: wait for Package Manager to be ready. @@ -36,10 +33,8 @@ public override bool Execute () // subsequent adb checks that query PM can fail if we return too // early, causing a second emulator to be launched on the same // port (see CheckAdbTarget). - waitingForPM = true; Log.LogMessage (MessageImportance.Normal, "Boot completed, waiting for Package Manager..."); while (DateTime.UtcNow < endTime && !pmReady) { - pmReady = false; base.Execute (); if (pmReady) break; @@ -48,10 +43,11 @@ public override bool Execute () if (!pmReady) { Log.LogError ($"Emulator '{AdbTarget}' Package Manager did not become ready in {Timeout} ms."); - } else { - Log.LogMessage (MessageImportance.Normal, "Package Manager is ready."); + return false; } + Log.LogMessage (MessageImportance.Normal, "Package Manager is ready."); + return !Log.HasLoggedErrors; } @@ -60,44 +56,31 @@ public override bool Execute () return new List { new CommandInfo { ArgumentsString = $"{AdbTarget} shell getprop sys.boot_completed", - ShouldRun = () => !waitingForPM, + ShouldRun = () => bootCompleted != 1, }, new CommandInfo { ArgumentsString = $"{AdbTarget} shell pm path com.android.shell", IgnoreExitCode = true, LogIgnoredExitCodeAsWarning = false, MergeStdoutAndStderr = false, - ShouldRun = () => waitingForPM, + ShouldRun = () => bootCompleted == 1, }, }; } - protected override void BeforeCommand (int commandIndex, CommandInfo info) - { - currentState = commandIndex; - } - protected override void ProcessStdout (string line) { if (string.IsNullOrEmpty (line)) return; - switch (currentState) { - case StateCheckBoot: - if (int.TryParse (line, out int bootCompletedPropValue)) - bootCompleted = bootCompletedPropValue; - break; - - case StateCheckPM: - if (line.StartsWith ("package:", StringComparison.OrdinalIgnoreCase)) - pmReady = true; - break; + if (bootCompleted != 1) { + if (int.TryParse (line, out int value)) + bootCompleted = value; + } else { + if (line.StartsWith ("package:", StringComparison.OrdinalIgnoreCase)) + pmReady = true; } } - protected override void ProcessStderr (string line) - { - // PM errors on stderr should not mark the target as ready - } } } From 5449329553b013531b6d225507dbb94823c9c57d Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 20 Feb 2026 10:53:17 -0800 Subject: [PATCH 4/5] Update build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../WaitForAndroidEmulator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs index b833fa7fd79..b331fb4a4d8 100644 --- a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs +++ b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs @@ -77,7 +77,7 @@ protected override void ProcessStdout (string line) if (int.TryParse (line, out int value)) bootCompleted = value; } else { - if (line.StartsWith ("package:", StringComparison.OrdinalIgnoreCase)) + if (line.StartsWith ("package:", StringComparison.Ordinal)) pmReady = true; } } From fad47ad207c3ef7366526ba43b6a67dc170ac97a Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 20 Feb 2026 11:09:46 -0800 Subject: [PATCH 5/5] Document that the emulator wait timeout is shared between both phases --- .../WaitForAndroidEmulator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs index b331fb4a4d8..fb682b70ae8 100644 --- a/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs +++ b/build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/WaitForAndroidEmulator.cs @@ -13,6 +13,9 @@ public class WaitForAndroidEmulator : Adb public override bool Execute () { + // The timeout is shared between both phases (boot_completed and + // Package Manager readiness), so the total wall-clock time for + // the entire method is bounded by a single Timeout period. var endTime = DateTime.UtcNow.AddMilliseconds (Timeout); // Phase 1: wait for sys.boot_completed