Skip to content

Commit 278e101

Browse files
dellis1972jonathanpeppers
authored andcommitted
[Xamarin.Android.Build.Tasks] Add retry ability to RemoveDirFixed (#9409)
Fixes: #9133 Context: dotnet/android-tools@60fae19 Much to our chagrin, in .NET 6+ it is possible for Design-Time Builds (DTBs) to run concurrently with "normal" builds, as there is nothing within the [.NET Project System][0] or [Common Project System (CPS)][1] which would actively *prevent* such concurrency. Consequently, it is possible to encounter locked files and directories during the build process, and user may see errors such as: Error (active) XARDF7019 System.UnauthorizedAccessException: Access to the path 'GoogleGson.dll' is denied. at System.IO.Directory.DeleteHelper(String fullPath, String userPath, Boolean recursive, Boolean throwOnTopLevelDirectoryNotFound, WIN32_FIND_DATA& data) at System.IO.Directory.Delete(String fullPath, String userPath, Boolean recursive, Boolean checkHost) at Xamarin.Android.Tasks.RemoveDirFixed.RunTask() in /Users/runner/work/1/s/xamarin-android/src/Xamarin.Android.Build.Tasks/Tasks/RemoveDirFixed.cs:line 54 MauiApp2 (net9.0-android) C:\Program Files\dotnet\packs\Microsoft.Android.Sdk.Windows\34.99.0-preview.6.340\tools\Xamarin.Android.Common.targets 2503 dotnet/android-tools@60fae192 introduced the concept of a "Retry" in the cases of `UnauthorizedAccessException`s or `IOException`s when the code is `ACCESS_DENIED` or `ERROR_SHARING_VIOLATION`. Builds upon that work to use the API's added to add retry semantics to the `<RemoveDirFixed/>` task. This also simplifies the Task somewhat as it had quite complex exception handling. [0]: https://github.com/dotnet/project-system [1]: https://github.com/microsoft/VSProjectSystem
1 parent 152295c commit 278e101

File tree

2 files changed

+76
-25
lines changed

2 files changed

+76
-25
lines changed

src/Xamarin.Android.Build.Tasks/Tasks/RemoveDirFixed.cs

+35-25
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
using System;
2929
using System.Collections.Generic;
3030
using System.IO;
31+
using System.Threading;
32+
using System.Runtime.InteropServices;
3133
using Microsoft.Build.Framework;
3234
using Xamarin.Android.Tools;
3335
using Microsoft.Android.Build.Tasks;
@@ -38,6 +40,9 @@ public class RemoveDirFixed : AndroidTask
3840
{
3941
public override string TaskPrefix => "RDF";
4042

43+
const int ERROR_ACCESS_DENIED = -2147024891;
44+
const int ERROR_SHARING_VIOLATION = -2147024864;
45+
4146
public override bool RunTask ()
4247
{
4348
var temporaryRemovedDirectories = new List<ITaskItem> (Directories.Length);
@@ -48,36 +53,41 @@ public override bool RunTask ()
4853
Log.LogDebugMessage ($"Directory did not exist: {fullPath}");
4954
continue;
5055
}
56+
int retryCount = 0;
57+
int attempts = Files.GetFileWriteRetryAttempts ();
58+
int delay = Files.GetFileWriteRetryDelay ();
5159
try {
52-
// try to do a normal "fast" delete of the directory.
53-
Directory.Delete (fullPath, true);
54-
temporaryRemovedDirectories.Add (directory);
55-
} catch (UnauthorizedAccessException ex) {
56-
// if that fails we probably have readonly files (or locked files)
57-
// so try to make them writable and try again.
58-
try {
59-
Files.SetDirectoryWriteable (fullPath);
60-
Directory.Delete (fullPath, true);
61-
temporaryRemovedDirectories.Add (directory);
62-
} catch (Exception inner) {
63-
Log.LogUnhandledException (TaskPrefix, ex);
64-
Log.LogUnhandledException (TaskPrefix, inner);
65-
}
66-
} catch (DirectoryNotFoundException ex) {
67-
// This could be a file inside the directory over MAX_PATH.
68-
// We can attempt using the \\?\ syntax.
69-
if (OS.IsWindows) {
60+
while (retryCount <= attempts) {
7061
try {
71-
fullPath = Files.ToLongPath (fullPath);
72-
Log.LogDebugMessage ("Trying long path: " + fullPath);
62+
// try to do a normal "fast" delete of the directory.
63+
// only do the set writable on the second attempt
64+
if (retryCount == 1)
65+
Files.SetDirectoryWriteable (fullPath);
7366
Directory.Delete (fullPath, true);
7467
temporaryRemovedDirectories.Add (directory);
75-
} catch (Exception inner) {
76-
Log.LogUnhandledException (TaskPrefix, ex);
77-
Log.LogUnhandledException (TaskPrefix, inner);
68+
break;
69+
} catch (Exception e) {
70+
switch (e) {
71+
case DirectoryNotFoundException:
72+
if (OS.IsWindows) {
73+
fullPath = Files.ToLongPath (fullPath);
74+
Log.LogDebugMessage ("Trying long path: " + fullPath);
75+
break;
76+
}
77+
throw;
78+
case UnauthorizedAccessException:
79+
case IOException:
80+
int code = Marshal.GetHRForException(e);
81+
if ((code != ERROR_ACCESS_DENIED && code != ERROR_SHARING_VIOLATION) || retryCount >= attempts) {
82+
throw;
83+
};
84+
break;
85+
default:
86+
throw;
87+
}
88+
Thread.Sleep (delay);
89+
retryCount++;
7890
}
79-
} else {
80-
Log.LogUnhandledException (TaskPrefix, ex);
8191
}
8292
} catch (Exception ex) {
8393
Log.LogUnhandledException (TaskPrefix, ex);

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/RemoveDirTests.cs

+41
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using Xamarin.Android.Tasks;
88
using Xamarin.Android.Tools;
99
using Microsoft.Android.Build.Tasks;
10+
using TPL = System.Threading.Tasks;
11+
using System.Threading;
1012

1113
namespace Xamarin.Android.Build.Tests
1214
{
@@ -83,5 +85,44 @@ public void LongPath ()
8385
Assert.AreEqual (1, task.RemovedDirectories.Length, "Changes should have been made.");
8486
DirectoryAssert.DoesNotExist (tempDirectory);
8587
}
88+
89+
[Test, Category ("SmokeTests")]
90+
public void DirectoryInUse ()
91+
{
92+
if (OS.IsMac) {
93+
Assert.Ignore ("This is not an issue on macos.");
94+
return;
95+
}
96+
var file = NewFile ();
97+
var task = CreateTask ();
98+
using (var f = File.OpenWrite (file)) {
99+
Assert.IsFalse (task.Execute (), "task.Execute() should have failed.");
100+
Assert.AreEqual (0, task.RemovedDirectories.Length, "Changes should not have been made.");
101+
DirectoryAssert.Exists (tempDirectory);
102+
}
103+
}
104+
105+
[Test, Category ("SmokeTests")]
106+
public async TPL.Task DirectoryInUseWithRetry ()
107+
{
108+
if (OS.IsMac) {
109+
Assert.Ignore ("This is not an issue on macos.");
110+
return;
111+
}
112+
var file = NewFile ();
113+
var task = CreateTask ();
114+
var ev = new ManualResetEvent (false);
115+
var t = TPL.Task.Run (async () => {
116+
using (var f = File.OpenWrite (file)) {
117+
ev.Set ();
118+
await TPL.Task.Delay (2500);
119+
}
120+
});
121+
ev.WaitOne ();
122+
Assert.IsTrue (task.Execute (), "task.Execute() should have succeeded.");
123+
Assert.AreEqual (1, task.RemovedDirectories.Length, "Changes should have been made.");
124+
DirectoryAssert.DoesNotExist (tempDirectory);
125+
await t;
126+
}
86127
}
87128
}

0 commit comments

Comments
 (0)