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..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 @@ -40,6 +40,37 @@ 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; + + 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 + } + } + } + /// /// Sets a single file to be writable by removing the read-only attribute if present. ///