From af71a06c84bbd22734f0ffa0bb9d7ec218cc80a7 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Jul 2026 08:52:51 -0500 Subject: [PATCH 1/3] Fix marshal startup logcat race in device test MarshalMethodsGCHangTests previously launched the app via RunProjectAndAssert and only then began logcat monitoring, which made the startup marker check racey under CI log volume. This change starts adb logcat monitoring first, then launches MainActivity via a callback once monitoring is attached, and clears logcat immediately before startup monitoring to keep the buffer focused on startup lines. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c491d761-523c-40ad-9374-4897fc12d51b --- .../Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs | 3 ++- .../Tests/MarshalMethodsGCHangTests.cs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs index 5a639c3e12e..613c5098180 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs @@ -355,7 +355,7 @@ protected TimeSpan ProfileFor (Func func, TimeSpan? timeout = null) return stopwatch.Elapsed; } - protected static bool MonitorAdbLogcat (Func action, string logcatFilePath, int timeout = 15) + protected static bool MonitorAdbLogcat (Func action, string logcatFilePath, int timeout = 15, Action? onMonitoringStarted = null) { string ext = Environment.OSVersion.Platform != PlatformID.Unix ? ".exe" : ""; string adb = Path.Combine (AndroidSdkPath, "platform-tools", "adb" + ext); @@ -381,6 +381,7 @@ protected static bool MonitorAdbLogcat (Func action, string logcat } }; proc.BeginOutputReadLine (); + onMonitoringStarted?.Invoke (); TimeSpan time = TimeSpan.FromSeconds (timeout); while (!stdout_done.IsSet && !didActionSucceed && time.TotalMilliseconds > 0) { proc.WaitForExit (10); diff --git a/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs b/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs index 82891bab472..edf099f9ea4 100644 --- a/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs @@ -114,13 +114,15 @@ public void MarshalMethodsAppRuns ([Values (AndroidRuntime.CoreCLR, AndroidRunti using var apkBuilder = CreateApkBuilder (); Assert.True (apkBuilder.Install (proj), "Project should have installed."); - RunProjectAndAssert (proj, apkBuilder); + ClearAdbLogcat (); const string expectedLogcatOutput = "XXX:OnStart done"; Assert.IsTrue ( MonitorAdbLogcat ( InstallAndRunTests.CreateLineChecker (expectedLogcatOutput), - logcatFilePath: Path.Combine (Root, apkBuilder.ProjectDirectory, "startup-logcat.log"), timeout: 60 + logcatFilePath: Path.Combine (Root, apkBuilder.ProjectDirectory, "startup-logcat.log"), + timeout: 60, + onMonitoringStarted: () => StartActivityAndAssert (proj) ), $"Output did not contain {expectedLogcatOutput}!" ); From b61f54b9d35356bbc45028f6db6a423e543172f3 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Jul 2026 09:15:23 -0500 Subject: [PATCH 2/3] Handle callback exceptions in logcat monitor cleanup Ensure MonitorAdbLogcat always stops and waits for the adb logcat process even when onMonitoringStarted throws. Capture callback exceptions, run process cleanup in a finally path, then rethrow with original stack trace. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c491d761-523c-40ad-9374-4897fc12d51b --- .../Utilities/DeviceTest.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs index 613c5098180..e943a244d5b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs @@ -13,6 +13,7 @@ using System.Xml.Linq; using System.Xml.XPath; using Xamarin.ProjectTools; +using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; namespace Xamarin.Android.Build.Tests @@ -370,6 +371,7 @@ protected static bool MonitorAdbLogcat (Func action, string logcat ManualResetEventSlim stdout_done = new ManualResetEventSlim (); using (var sw = File.CreateText (logcatFilePath)) { using (var proc = Process.Start (info)) { + Exception? callbackException = null; proc.OutputDataReceived += (sender, e) => { if (e.Data != null) { sw.WriteLine (e.Data); @@ -381,16 +383,28 @@ protected static bool MonitorAdbLogcat (Func action, string logcat } }; proc.BeginOutputReadLine (); - onMonitoringStarted?.Invoke (); - TimeSpan time = TimeSpan.FromSeconds (timeout); - while (!stdout_done.IsSet && !didActionSucceed && time.TotalMilliseconds > 0) { - proc.WaitForExit (10); - time -= TimeSpan.FromMilliseconds (10); + try { + onMonitoringStarted?.Invoke (); + } catch (Exception ex) { + callbackException = ex; + } + try { + TimeSpan time = TimeSpan.FromSeconds (timeout); + while (!stdout_done.IsSet && !didActionSucceed && callbackException == null && time.TotalMilliseconds > 0) { + proc.WaitForExit (10); + time -= TimeSpan.FromMilliseconds (10); + } + } finally { + if (!proc.HasExited) { + proc.Kill (); + } + proc.WaitForExit (); + stdout_done.Wait (); + sw.Flush (); + } + if (callbackException != null) { + ExceptionDispatchInfo.Capture (callbackException).Throw (); } - proc.Kill (); - proc.WaitForExit (); - stdout_done.Wait (); - sw.Flush (); return didActionSucceed; } } From 3dcd83ea48d7dc301f5902238cbb17ffee1cfb7d Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Jul 2026 09:17:23 -0500 Subject: [PATCH 3/3] Simplify logcat cleanup exception flow Use a single try/finally in MonitorAdbLogcat so onMonitoringStarted exceptions propagate naturally while adb logcat cleanup is always executed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c491d761-523c-40ad-9374-4897fc12d51b --- .../Utilities/DeviceTest.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs index e943a244d5b..7416672636d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/DeviceTest.cs @@ -13,7 +13,6 @@ using System.Xml.Linq; using System.Xml.XPath; using Xamarin.ProjectTools; -using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; namespace Xamarin.Android.Build.Tests @@ -371,7 +370,6 @@ protected static bool MonitorAdbLogcat (Func action, string logcat ManualResetEventSlim stdout_done = new ManualResetEventSlim (); using (var sw = File.CreateText (logcatFilePath)) { using (var proc = Process.Start (info)) { - Exception? callbackException = null; proc.OutputDataReceived += (sender, e) => { if (e.Data != null) { sw.WriteLine (e.Data); @@ -385,12 +383,8 @@ protected static bool MonitorAdbLogcat (Func action, string logcat proc.BeginOutputReadLine (); try { onMonitoringStarted?.Invoke (); - } catch (Exception ex) { - callbackException = ex; - } - try { TimeSpan time = TimeSpan.FromSeconds (timeout); - while (!stdout_done.IsSet && !didActionSucceed && callbackException == null && time.TotalMilliseconds > 0) { + while (!stdout_done.IsSet && !didActionSucceed && time.TotalMilliseconds > 0) { proc.WaitForExit (10); time -= TimeSpan.FromMilliseconds (10); } @@ -402,9 +396,6 @@ protected static bool MonitorAdbLogcat (Func action, string logcat stdout_done.Wait (); sw.Flush (); } - if (callbackException != null) { - ExceptionDispatchInfo.Capture (callbackException).Throw (); - } return didActionSucceed; } }