Skip to content

Commit df31a19

Browse files
[tests] Add delete-with-retry to fix flaky Windows IO race
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
1 parent 9bd7da1 commit df31a19

3 files changed

Lines changed: 33 additions & 9 deletions

File tree

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,7 @@ public void Save (XamarinProject project, bool doNotCleanupOnUpdate = false, boo
104104

105105
if (!BuiltBefore) {
106106
if (project.ShouldPopulate) {
107-
if (Directory.Exists (ProjectDirectory)) {
108-
FileSystemUtils.SetDirectoryWriteable (ProjectDirectory);
109-
Directory.Delete (ProjectDirectory, true);
110-
}
107+
FileSystemUtils.DeleteDirectoryWithRetry (ProjectDirectory);
111108
project.Populate (ProjectDirectory, files);
112109
}
113110

@@ -202,10 +199,7 @@ public void Cleanup ()
202199
BuiltBefore = false;
203200

204201
var projectDirectory = Path.Combine (XABuildPaths.TestOutputDirectory, ProjectDirectory);
205-
if (Directory.Exists (projectDirectory)) {
206-
FileSystemUtils.SetDirectoryWriteable (projectDirectory);
207-
Directory.Delete (projectDirectory, true);
208-
}
202+
FileSystemUtils.DeleteDirectoryWithRetry (projectDirectory);
209203
}
210204

211205
public struct RuntimeInfo

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/SolutionBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ protected override void Dispose (bool disposing)
9999
if (disposing)
100100
if (BuildSucceeded && !string.IsNullOrEmpty (SolutionPath))
101101
try {
102-
Directory.Delete (SolutionPath, recursive: true);
102+
FileSystemUtils.DeleteDirectoryWithRetry (SolutionPath);
103103
} catch (Exception) {
104104
// This happens on CI occasionally, let's not fail the test
105105
}

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Utilities/FileSystemUtils.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,36 @@ public static void SetDirectoryWriteable (string directory)
4040
}
4141
}
4242

43+
/// <summary>
44+
/// Recursively deletes a directory, retrying on transient failures.
45+
/// </summary>
46+
/// <param name="directory">The directory path to delete.</param>
47+
/// <param name="retries">The maximum number of retries before giving up.</param>
48+
/// <remarks>
49+
/// On Windows, a handle to a just-written file can still be held by another process
50+
/// (e.g. the Roslyn shared-compilation server, an anti-virus scanner, or the search
51+
/// indexer), causing <see cref="Directory.Delete(string, bool)"/> to throw
52+
/// <see cref="UnauthorizedAccessException"/> or <see cref="IOException"/>. This method
53+
/// backs off and retries to let the other process release the handle.
54+
/// </remarks>
55+
/// <seealso cref="SetDirectoryWriteable(string)"/>
56+
public static void DeleteDirectoryWithRetry (string directory, int retries = 10)
57+
{
58+
if (!Directory.Exists (directory))
59+
return;
60+
61+
SetDirectoryWriteable (directory);
62+
for (int i = 0; ; i++) {
63+
try {
64+
Directory.Delete (directory, true);
65+
return;
66+
} catch (Exception e) when ((e is UnauthorizedAccessException || e is IOException) && i < retries) {
67+
Thread.Sleep (200 * (i + 1)); // back off; let AV/Roslyn release the handle
68+
SetDirectoryWriteable (directory);
69+
}
70+
}
71+
}
72+
4373
/// <summary>
4474
/// Sets a single file to be writable by removing the read-only attribute if present.
4575
/// </summary>

0 commit comments

Comments
 (0)