From 9bd7da15a895801d960b182e2457b3b364e75ce9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:54:15 +0000 Subject: [PATCH 1/3] Initial plan From df31a19e94fcf654941c7a924aa05d64a01fa8ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:22:06 +0000 Subject: [PATCH 2/3] [tests] Add delete-with-retry to fix flaky Windows IO race Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Common/ProjectBuilder.cs | 10 ++----- .../Common/SolutionBuilder.cs | 2 +- .../Utilities/FileSystemUtils.cs | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs index db607aa6475..0a74a6a2adc 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs @@ -104,10 +104,7 @@ public void Save (XamarinProject project, bool doNotCleanupOnUpdate = false, boo if (!BuiltBefore) { if (project.ShouldPopulate) { - if (Directory.Exists (ProjectDirectory)) { - FileSystemUtils.SetDirectoryWriteable (ProjectDirectory); - Directory.Delete (ProjectDirectory, true); - } + FileSystemUtils.DeleteDirectoryWithRetry (ProjectDirectory); project.Populate (ProjectDirectory, files); } @@ -202,10 +199,7 @@ public void Cleanup () BuiltBefore = false; var projectDirectory = Path.Combine (XABuildPaths.TestOutputDirectory, ProjectDirectory); - if (Directory.Exists (projectDirectory)) { - FileSystemUtils.SetDirectoryWriteable (projectDirectory); - Directory.Delete (projectDirectory, true); - } + FileSystemUtils.DeleteDirectoryWithRetry (projectDirectory); } public struct RuntimeInfo diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs index e66a1568c67..aeec60a3440 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs @@ -99,7 +99,7 @@ protected override void Dispose (bool disposing) if (disposing) if (BuildSucceeded && !string.IsNullOrEmpty (SolutionPath)) try { - Directory.Delete (SolutionPath, recursive: true); + FileSystemUtils.DeleteDirectoryWithRetry (SolutionPath); } catch (Exception) { // This happens on CI occasionally, let's not fail the test } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs index 55fb9c0d199..cf83c06e478 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs @@ -40,6 +40,36 @@ public static void SetDirectoryWriteable (string directory) } } + /// + /// Recursively deletes a directory, retrying on transient failures. + /// + /// The directory path to delete. + /// The maximum number of retries before giving up. + /// + /// On Windows, a handle to a just-written file can still be held by another process + /// (e.g. the Roslyn shared-compilation server, an anti-virus scanner, or the search + /// indexer), causing to throw + /// or . This method + /// backs off and retries to let the other process release the handle. + /// + /// + public static void DeleteDirectoryWithRetry (string directory, int retries = 10) + { + if (!Directory.Exists (directory)) + return; + + SetDirectoryWriteable (directory); + for (int i = 0; ; i++) { + try { + Directory.Delete (directory, true); + return; + } catch (Exception e) when ((e is UnauthorizedAccessException || e is IOException) && i < retries) { + Thread.Sleep (200 * (i + 1)); // back off; let AV/Roslyn release the handle + SetDirectoryWriteable (directory); + } + } + } + /// /// Sets a single file to be writable by removing the read-only attribute if present. /// From 848ccdfb4fe85e014628eb3806f7741fc331000d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:33:32 +0000 Subject: [PATCH 3/3] Handle DirectoryNotFoundException in delete-with-retry Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs index cf83c06e478..b49eec03c4c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs @@ -58,14 +58,15 @@ public static void DeleteDirectoryWithRetry (string directory, int retries = 10) if (!Directory.Exists (directory)) return; - SetDirectoryWriteable (directory); for (int i = 0; ; i++) { try { + SetDirectoryWriteable (directory); Directory.Delete (directory, true); return; + } catch (DirectoryNotFoundException) { + return; } catch (Exception e) when ((e is UnauthorizedAccessException || e is IOException) && i < retries) { Thread.Sleep (200 * (i + 1)); // back off; let AV/Roslyn release the handle - SetDirectoryWriteable (directory); } } }