MoveWithRollback fails intermittently with 'Access denied' on Windows due to Defender file scanning
- Dominant language
- C#
- Stars
- 2.1k
- Forks
- 579
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 252
Description
## Summary
`ExtractAndInstall` in `Xamarin.Android.Tools.AndroidSdk` (`DownloadUtils` / `FileUtil`) intermittently fails on Windows with:
`
[android-tools:error] Failed to move to 'C:\Users\...\Android\sdk\cmdline-tools\19.0': Access to the path '...\extract-\cmdline-tools' is denied.
`
## Root Cause
`ExtractAndInstall` calls `ExtractZipSafe` followed immediately by `MoveWithRollback`:
```csharp
// FileUtil.ExtractAndInstall (decompiled)
DownloadUtils.ExtractZipSafe(archivePath, tempDir, cancellationToken);
// ↑ ZipArchive is disposed, but Defender's minifilter is still scanning
MoveWithRollback(extractedSubDir, targetPath, logger);
// ↑ Directory.Move() fails because Defender holds a read handle
```
On Windows, **Windows Defender Real-Time Protection** (and other anti-virus minifilter drivers) asynchronously scan newly created files. `ExtractZipSafe` creates files via `entry.ExtractToFile()`, and while the `ZipArchive` is properly disposed, Windows Defender may still hold read handles on the extracted files when `Directory.Move()` executes immediately after. `MoveFileExW` (the Win32 call behind `Directory.Move`) requires exclusive access to the source directory tree, so it fails with `UnauthorizedAccessException`.
This is intermittent — it depends on timing between extraction completion and the Defender scan completing.
## Reproduction
1. On a Windows machine with Defender Real-Time Protection enabled
2. Call `ExtractAndInstall` for cmdline-tools (or any SDK component)
3. Observe intermittent "Access to the path ... is denied" errors
The issue reproduces more frequently on slower machines or when Defender is under load.
## Suggested Fix
Add retry with exponential backoff in `MoveWithRollback`:
```csharp
internal static void MoveWithRollback(string sourcePath, string targetPath, Action logger)
{
// ... existing backup logic ...
const int maxRetries = 3;
int delayMs = 500;
for (int attempt = 0; ; attempt++)
{
try
{
Directory.Move(sourcePath, targetPath);
break;
}
catch (Exception ex) when (attempt < maxRetries && IsRetryableIOError(ex))
{
logger(TraceLevel.Warning,
$"Directory.Move failed (attempt {attempt + 1}/{maxRetries + 1}), retrying in {delayMs}ms: {ex.Message}");
Thread.Sleep(delayMs);
delayMs *= 2;
}
}
// ... existing cleanup logic ...
}
private static bool IsRetryableIOError(Exception ex)
{
return ex is UnauthorizedAccessException
|| (ex is IOException io && io.Message.Contains("being used by another process"));
}
```
## Workaround
In `vscode-maui` we've added retry logic around the CLI invocation as a workaround, but this is wasteful since it re-downloads the archive on each retry. The retry belongs inside `MoveWithRollback` where only the move is retried.
## Environment
- Windows 11, Windows Defender Real-Time Protection enabled
- `Xamarin.Android.Tools.AndroidSdk` 1.0.143-preview.10
- Triggered via MAUI CLI (`maui android install`) from VS Code MAUI extension
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.