From 87cfaf1a56b6ec034724108dce3e8c8b2a9fe6a6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Jun 2026 21:16:06 +0000
Subject: [PATCH 1/3] Initial plan
From 137f0c6632f83414d1ab57e57143cd587ff94f3c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Jun 2026 21:43:53 +0000
Subject: [PATCH 2/3] [FastDeploy] Retry run-as on install race for all users
(XA0137)
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Tasks/FastDeploy.cs | 44 ++++++++++++++++++-
1 file changed, 43 insertions(+), 1 deletion(-)
diff --git a/src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy.cs b/src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy.cs
index aa940b1d75d..7771606b8e5 100644
--- a/src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy.cs
+++ b/src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy.cs
@@ -362,7 +362,7 @@ async Task CheckAppInstalledAndDebuggable (string packageName)
packageInfo.UserId = UserID;
packageInfo.PackageName = packageName;
await EnsureUserIsRunning ();
- packageInfo.InternalPath = packageInfo.InternalPath ?? await Device.RunAs (packageInfo, "pwd");
+ packageInfo.InternalPath = packageInfo.InternalPath ?? await QueryInternalPathWithRetry ();
if (packageInfo.InternalPath.IndexOf ("Permission denied", StringComparison.OrdinalIgnoreCase) >= 0) {
packageInfo.InternalPath = await Device.RunAs (packageInfo, "readlink", "-f", ".");
}
@@ -395,6 +395,48 @@ async Task CheckAppInstalledAndDebuggable (string packageName)
return;
}
+ ///
+ /// Issues the first run-as <pkg> pwd query, retrying briefly while the
+ /// per-user data directory is not yet stat-able through run-as.
+ ///
+ ///
+ /// Immediately after pm install, the per-user data directory
+ /// /data/user/N/<pkg> may not yet be stat-able through run-as,
+ /// even for the primary user (id 0). During that window run-as returns
+ /// run-as: couldn't stat /data/user/N/<pkg>: No such file or directory,
+ /// which otherwise raises XA0137 and disables Fast Deployment. This races
+ /// install on the primary user ~daily in CI. Poll for a bounded period to let the
+ /// directory materialize before giving up. See
+ /// https://github.com/dotnet/android/issues/7821 and
+ /// https://github.com/dotnet/android/issues/11808.
+ ///
+ async Task QueryInternalPathWithRetry ()
+ {
+ const int maxAttempts = 10;
+ var delay = TimeSpan.FromMilliseconds (500);
+ string result = await Device.RunAs (packageInfo, "pwd");
+ for (int attempt = 1; attempt < maxAttempts && IsTransientRunAsStatRace (result); attempt++) {
+ LogDiagnostic ($"run-as could not stat the data directory for {packageInfo.PackageName} yet (attempt {attempt}/{maxAttempts}); retrying in {delay.TotalMilliseconds:0} ms. Output: {result?.Trim ()}");
+ await Task.Delay (delay, CancellationToken);
+ result = await Device.RunAs (packageInfo, "pwd");
+ }
+ return result;
+ }
+
+ ///
+ /// Returns when a run-as result matches the transient
+ /// install-vs-run-as race signature (couldn't stat … No such file or directory),
+ /// i.e. the per-user data directory has not yet materialized after pm install.
+ ///
+ static bool IsTransientRunAsStatRace (string result)
+ {
+ if (string.IsNullOrEmpty (result)) {
+ return false;
+ }
+ return result.IndexOf ("couldn't stat", StringComparison.OrdinalIgnoreCase) >= 0 &&
+ result.IndexOf ("No such file or directory", StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+
///
/// Ensures the secondary Android user targeted by this deployment is in the
/// 'running' state before any run-as query is issued against it.
From 73b023d34269f07a03c7efd679bf437f852b42d2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 1 Jul 2026 08:48:11 +0000
Subject: [PATCH 3/3] Add unit tests for IsTransientRunAsStatRace and document
retry policy
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Properties/AssemblyInfo.cs | 2 +
.../Tasks/FastDeploy.cs | 7 ++-
.../DebuggingTasksTests.cs | 50 +++++++++++++++++++
3 files changed, 58 insertions(+), 1 deletion(-)
diff --git a/src/Xamarin.Android.Build.Debugging.Tasks/Properties/AssemblyInfo.cs b/src/Xamarin.Android.Build.Debugging.Tasks/Properties/AssemblyInfo.cs
index ad636f063cf..c8a29cb2537 100644
--- a/src/Xamarin.Android.Build.Debugging.Tasks/Properties/AssemblyInfo.cs
+++ b/src/Xamarin.Android.Build.Debugging.Tasks/Properties/AssemblyInfo.cs
@@ -17,3 +17,5 @@
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid ("42bb9aea-f4d8-4b43-8956-8e1fee857697")]
+
+[assembly: InternalsVisibleTo ("Xamarin.Android.Build.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")]
diff --git a/src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy.cs b/src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy.cs
index 7771606b8e5..13f72c72f78 100644
--- a/src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy.cs
+++ b/src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy.cs
@@ -409,6 +409,11 @@ async Task CheckAppInstalledAndDebuggable (string packageName)
/// directory materialize before giving up. See
/// https://github.com/dotnet/android/issues/7821 and
/// https://github.com/dotnet/android/issues/11808.
+ /// Retry policy: up to 10 attempts with a 500 ms delay between each, giving
+ /// a maximum wait of 4.5 seconds before the error is surfaced as XA0137.
+ /// Only the transient couldn't stat … No such file or directory signature
+ /// (detected by ) triggers a retry; all other
+ /// run-as failures are surfaced immediately.
///
async Task QueryInternalPathWithRetry ()
{
@@ -428,7 +433,7 @@ async Task QueryInternalPathWithRetry ()
/// install-vs-run-as race signature (couldn't stat … No such file or directory),
/// i.e. the per-user data directory has not yet materialized after pm install.
///
- static bool IsTransientRunAsStatRace (string result)
+ internal static bool IsTransientRunAsStatRace (string result)
{
if (string.IsNullOrEmpty (result)) {
return false;
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DebuggingTasksTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DebuggingTasksTests.cs
index d5d824dc491..e04b015d4ed 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DebuggingTasksTests.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DebuggingTasksTests.cs
@@ -74,4 +74,54 @@ public void TestResolveToolsExists ()
}
}
+
+ ///
+ /// Unit tests for helper methods that do not require a device.
+ ///
+ [TestFixture]
+ public class FastDeployTests
+ {
+ // Canonical transient race output produced by the Android run-as tool when
+ // the per-user data directory has not yet materialized after pm install.
+ static readonly string [] TransientRaceOutputs = {
+ "run-as: couldn't stat /data/user/0/com.example.app: No such file or directory",
+ "run-as: couldn't stat /data/user/10/com.example.app: No such file or directory",
+ // Verify case-insensitivity of the detection.
+ "run-as: Couldn't Stat /data/user/0/com.example.app: No Such File Or Directory",
+ // Extra surrounding whitespace / newlines as they may appear in raw adb output.
+ " run-as: couldn't stat /data/user/0/com.example.app: No such file or directory\n",
+ };
+
+ // Genuine run-as failures that must NOT be swallowed by the retry loop.
+ static readonly string [] NonTransientOutputs = {
+ // Null / empty — first guard in the implementation.
+ null,
+ "",
+ // Successful pwd output — the data directory already exists.
+ "/data/user/0/com.example.app",
+ // Package not debuggable.
+ "run-as: package 'com.example.app' is not debuggable",
+ // Package not installed.
+ "run-as: package 'com.example.app' is unknown",
+ // Permission denied (SELinux / policy).
+ "run-as: couldn't stat /data/user/0/com.example.app: Permission denied",
+ // Only one of the two required substrings — must not match.
+ "run-as: couldn't stat /data/user/0/com.example.app",
+ "No such file or directory",
+ };
+
+ [TestCaseSource (nameof (TransientRaceOutputs))]
+ public void IsTransientRunAsStatRace_ReturnsTrueForRaceSignature (string output)
+ {
+ Assert.IsTrue (FastDeploy.IsTransientRunAsStatRace (output),
+ $"Expected transient-race detection for: {output}");
+ }
+
+ [TestCaseSource (nameof (NonTransientOutputs))]
+ public void IsTransientRunAsStatRace_ReturnsFalseForNonTransientOutput (string output)
+ {
+ Assert.IsFalse (FastDeploy.IsTransientRunAsStatRace (output),
+ $"Expected no transient-race detection for: {output}");
+ }
+ }
}