Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/Microsoft.Android.Build.BaseTasks/Files.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@
using System.Text;
using Xamarin.Tools.Zip;
using Microsoft.Build.Utilities;
using System.Threading;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Collections;

namespace Microsoft.Android.Build.Tasks
{
public static class Files
{
const int ERROR_ACCESS_DENIED = 5;

const int DEFAULT_FILE_WRITE_RETRY_ATTEMPTS = 10;

const int DEFAULT_FILE_WRITE_RETRY_DELAY_MS = 1000;

/// <summary>
/// Windows has a MAX_PATH limit of 260 characters
/// See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
Expand All @@ -28,6 +38,33 @@ public static class Files
public static readonly Encoding UTF8withoutBOM = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false);
readonly static byte[] Utf8Preamble = Encoding.UTF8.GetPreamble ();

/// <summary>
/// Checks for the environment variable DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS to
/// see if a custom value for the number of times to retry writing a file has been
/// set.
/// </summary>
/// <returns>The value of DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS or the default of DEFAULT_FILE_WRITE_RETRY_ATTEMPTS</returns>
public static int GetFileWriteRetryAttempts ()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made these public so we can use them from XA.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an other environment variables like this? I wonder if they should go in a class like KnownEnvironment.cs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't afaik.

{
var retryVariable = Environment.GetEnvironmentVariable ("DOTNET_ANDROID_FILE_WRITE_RETRY_ATTEMPTS");
if (!string.IsNullOrEmpty (retryVariable) && int.TryParse (retryVariable, out int retry))
return retry;
return DEFAULT_FILE_WRITE_RETRY_ATTEMPTS;
}

/// <summary>
/// Checks for the environment variable DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS to
/// see if a custom value for the delay between trying to write a file has been
/// set.
/// </summary>
/// <returns>The value of DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS or the default of DEFAULT_FILE_WRITE_RETRY_DELAY_MS</returns>
public static int GetFileWriteRetryDelay ()
{
var delayVariable = Environment.GetEnvironmentVariable ("DOTNET_ANDROID_FILE_WRITE_RETRY_DELAY_MS");
if (!string.IsNullOrEmpty (delayVariable) && int.TryParse (delayVariable, out int delay))
return delay;
return DEFAULT_FILE_WRITE_RETRY_DELAY_MS;
}
/// <summary>
/// Converts a full path to a \\?\ prefixed path that works on all Windows machines when over 260 characters
/// NOTE: requires a *full path*, use sparingly
Expand Down Expand Up @@ -111,6 +148,33 @@ public static bool ArchiveZip (string target, Action<string> archiver)
}

public static bool CopyIfChanged (string source, string destination)
{
int retryCount = 0;
int attempts = GetFileWriteRetryAttempts ();
int delay = GetFileWriteRetryDelay ();
while (retryCount < attempts) {
try {
return CopyIfChangedRetry (source, destination);
} catch (Exception e) {
switch (e) {
case UnauthorizedAccessException:
case IOException:
int code = Marshal.GetHRForException (e);
if (code != ERROR_ACCESS_DENIED || retryCount == attempts) {
throw;
};
break;
default:
throw;
}
}
retryCount++;
Thread.Sleep (delay);
}
return false;
}

public static bool CopyIfChangedRetry (string source, string destination)
{
if (HasFileChanged (source, destination)) {
var directory = Path.GetDirectoryName (destination);
Expand Down Expand Up @@ -157,6 +221,33 @@ public static bool CopyIfBytesChanged (byte[] bytes, string destination)
}

public static bool CopyIfStreamChanged (Stream stream, string destination)
{
int retryCount = 0;
int attempts = GetFileWriteRetryAttempts ();
int delay = GetFileWriteRetryDelay ();
while (retryCount < attempts) {
try {
return CopyIfStreamChangedRetry (stream, destination);
} catch (Exception e) {
switch (e) {
case UnauthorizedAccessException:
case IOException:
int code = Marshal.GetHRForException (e);
if (code != ERROR_ACCESS_DENIED || retryCount == attempts) {
throw;
};
break;
default:
throw;
}
}
retryCount++;
Thread.Sleep (delay);
}
return false;
}

public static bool CopyIfStreamChangedRetry (Stream stream, string destination)
{
if (HasStreamChanged (stream, destination)) {
var directory = Path.GetDirectoryName (destination);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
<AssemblyName>$(VendorPrefix)Microsoft.Android.Build.BaseTasks$(VendorSuffix)</AssemblyName>
<LangVersion>13.0</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
25 changes: 25 additions & 0 deletions tests/Microsoft.Android.Build.BaseTasks-Tests/FilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Tools.Zip;
using Microsoft.Android.Build.Tasks;

Expand Down Expand Up @@ -436,6 +437,30 @@ public void CopyIfStreamChanged_CasingChange ()
}
}

[Test]
public async Task CopyIfChanged_LockedFile ()
{
var dest = NewFile (contents: "foo", fileName: "foo_locked");
var src = NewFile (contents: "foo0", fileName: "foo");
using (var file = File.OpenWrite (dest)) {
Assert.Throws<IOException> (() => Files.CopyIfChanged (src, dest));
}
src = NewFile (contents: "foo1", fileName: "foo");
Assert.IsTrue (Files.CopyIfChanged (src, dest));
src = NewFile (contents: "foo2", fileName: "foo");
var task = Task.Run (async () => {
var file = File.Open (dest, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
try {
await Task.Delay (200);
} finally {
file.Close();
file.Dispose ();
}
});
Assert.IsTrue (Files.CopyIfChanged (src, dest));
await task;
}

[Test]
public void ExtractAll ()
{
Expand Down
Loading