Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue8273 corrupt nu get cache #8275

Merged
merged 4 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
81 changes: 81 additions & 0 deletions src/Tasks.UnitTests/Copy_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2096,6 +2096,87 @@ public void InvalidErrorIfLinkFailed()
Assert.False(result);
engine.AssertLogContains("MSB3892");
}

/// <summary>
/// An existing link source should not be modified.
/// </summary>
/// <remarks>
/// Related to issue [#8273](https://github.com/dotnet/msbuild/issues/8273)
/// </remarks>
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public void DoNotCorruptSourceOfLink(bool useHardLink, bool useSymbolicLink)
{
string sourceFile1 = FileUtilities.GetTemporaryFile();
string sourceFile2 = FileUtilities.GetTemporaryFile();
string temp = Path.GetTempPath();
string destFolder = Path.Combine(temp, "2A333ED756AF4dc392E728D0F864A398");
string destFile = Path.Combine(destFolder, "The Destination");

try
{
File.WriteAllText(sourceFile1, "This is the first source temp file."); // HIGHCHAR: Test writes in UTF8 without preamble.
File.WriteAllText(sourceFile2, "This is the second source temp file."); // HIGHCHAR: Test writes in UTF8 without preamble.

// Don't create the dest folder, let task do that

ITaskItem[] sourceFiles = { new TaskItem(sourceFile1) };
ITaskItem[] destinationFiles = { new TaskItem(destFile) };

var me = new MockEngine(true);
var t = new Copy
{
RetryDelayMilliseconds = 1, // speed up tests!
BuildEngine = me,
SourceFiles = sourceFiles,
DestinationFiles = destinationFiles,
SkipUnchangedFiles = true,
UseHardlinksIfPossible = useHardLink,
UseSymboliclinksIfPossible = useSymbolicLink,
};

bool success = t.Execute();

Assert.True(success); // "success"
Assert.True(File.Exists(destFile)); // "destination exists"

string destinationFileContents = File.ReadAllText(destFile);
Assert.Equal("This is the first source temp file.", destinationFileContents);

sourceFiles = new TaskItem[] { new TaskItem(sourceFile2) };

t = new Copy
{
RetryDelayMilliseconds = 1, // speed up tests!
BuildEngine = me,
SourceFiles = sourceFiles,
DestinationFiles = destinationFiles,
SkipUnchangedFiles = true,
UseHardlinksIfPossible = false,
UseSymboliclinksIfPossible = false,
};

success = t.Execute();

Assert.True(success); // "success"
Assert.True(File.Exists(destFile)); // "destination exists"

destinationFileContents = File.ReadAllText(destFile);
Assert.Equal("This is the second source temp file.", destinationFileContents);

// Read the source file (it should not have been overwritten)
string sourceFileContents = File.ReadAllText(sourceFile1);
Assert.Equal("This is the first source temp file.", sourceFileContents);

((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries
}
finally
{
Helpers.DeleteFiles(sourceFile1, sourceFile2, destFile);
}
}
}

public class CopyHardLink_Tests : Copy_Tests
Expand Down
62 changes: 30 additions & 32 deletions src/Tasks/Copy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,6 @@ private void LogDiagnostic(string message, params object[] messageArgs)
FileState sourceFileState, // The source file
FileState destinationFileState) // The destination file
{
bool destinationFileExists = false;

if (destinationFileState.DirectoryExists)
{
Log.LogErrorWithCodeFromResources("Copy.DestinationIsDirectory", sourceFileState.Name, destinationFileState.Name);
Expand Down Expand Up @@ -269,7 +267,14 @@ private void LogDiagnostic(string message, params object[] messageArgs)
if (OverwriteReadOnlyFiles)
{
MakeFileWriteable(destinationFileState, true);
destinationFileExists = destinationFileState.FileExists;
}

// If the destination file is a hard or symbolic link, File.Copy would overwrite the source.
// To prevent this, we need to delete the existing entry before we Copy or create a link.
// We could try to figure out if the file is a link, but I can't think of a reason to not simply delete it always.
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_6) && destinationFileState.FileExists && !destinationFileState.IsReadOnly)
Copy link
Member

Choose a reason for hiding this comment

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

Should we also take into account OverwriteReadOnlyFiles and also delete them in that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if OverwriteReadOnlyFiles is set, the file is made writable and hence deleted here.

Copy link
Member

Choose a reason for hiding this comment

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

...and now I see it 6 lines up. Stupid question, thanks!

{
FileUtilities.DeleteNoThrow(destinationFileState.Name);
}

bool symbolicLinkCreated = false;
Expand All @@ -279,7 +284,7 @@ private void LogDiagnostic(string message, params object[] messageArgs)
// Create hard links if UseHardlinksIfPossible is true
if (UseHardlinksIfPossible)
{
TryCopyViaLink(HardLinkComment, MessageImportance.Normal, sourceFileState, destinationFileState, ref destinationFileExists, out hardLinkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethods.MakeHardLink(destination, source, ref errorMessage, Log));
TryCopyViaLink(HardLinkComment, MessageImportance.Normal, sourceFileState, destinationFileState, out hardLinkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethods.MakeHardLink(destination, source, ref errorMessage, Log));
if (!hardLinkCreated)
{
if (UseSymboliclinksIfPossible)
Expand All @@ -297,13 +302,14 @@ private void LogDiagnostic(string message, params object[] messageArgs)
// Create symbolic link if UseSymboliclinksIfPossible is true and hard link is not created
if (!hardLinkCreated && UseSymboliclinksIfPossible)
{
TryCopyViaLink(SymbolicLinkComment, MessageImportance.Normal, sourceFileState, destinationFileState, ref destinationFileExists, out symbolicLinkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethodsShared.MakeSymbolicLink(destination, source, ref errorMessage));
if (!NativeMethodsShared.IsWindows)
{
errorMessage = Log.FormatResourceString("Copy.NonWindowsLinkErrorMessage", "symlink()", errorMessage);
}
TryCopyViaLink(SymbolicLinkComment, MessageImportance.Normal, sourceFileState, destinationFileState, out symbolicLinkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethodsShared.MakeSymbolicLink(destination, source, ref errorMessage));
if (!symbolicLinkCreated)
{
if (!NativeMethodsShared.IsWindows)
{
errorMessage = Log.FormatResourceString("Copy.NonWindowsLinkErrorMessage", "symlink()", errorMessage);
}

Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.Name, destinationFileState.Name, errorMessage);
}
}
Expand All @@ -324,41 +330,28 @@ private void LogDiagnostic(string message, params object[] messageArgs)
Log.LogMessage(MessageImportance.Normal, FileComment, sourceFilePath, destinationFilePath);

File.Copy(sourceFileState.Name, destinationFileState.Name, true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that the destination is always deleted, I think it would be a good idea to set the overwrite flag to false to protect us from accidental overwrites.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is probably better to fail in case someone gets in between than overwriting a link. I will update the PR.

Copy link
Member

Choose a reason for hiding this comment

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

I do not believe it is necessary. Sometimes logic might change to not delete it, or new Copy task argument will be introduced, or some weird concurrent logic is relaying with last win. I would recommend to keep it as is, but if you decide to change it, it has to go under same ChangeWave or same conditions as deleting files.


// If the destinationFile file exists, then make sure it's read-write.
// The File.Copy command copies attributes, but our copy needs to
// leave the file writeable.
if (sourceFileState.IsReadOnly)
{
destinationFileState.Reset();
MakeFileWriteable(destinationFileState, false);
}
}

// Files were successfully copied or linked. Those are equivalent here.
WroteAtLeastOneFile = true;

destinationFileState.Reset();

// If the destinationFile file exists, then make sure it's read-write.
// The File.Copy command copies attributes, but our copy needs to
// leave the file writeable.
if (sourceFileState.IsReadOnly)
{
MakeFileWriteable(destinationFileState, false);
}

return true;
}

private void TryCopyViaLink(string linkComment, MessageImportance messageImportance, FileState sourceFileState, FileState destinationFileState, ref bool destinationFileExists, out bool linkCreated, ref string errorMessage, Func<string, string, string, bool> createLink)
private void TryCopyViaLink(string linkComment, MessageImportance messageImportance, FileState sourceFileState, FileState destinationFileState, out bool linkCreated, ref string errorMessage, Func<string, string, string, bool> createLink)
{
// Do not log a fake command line as well, as it's superfluous, and also potentially expensive
Log.LogMessage(MessageImportance.Normal, linkComment, sourceFileState.Name, destinationFileState.Name);

if (!OverwriteReadOnlyFiles)
{
destinationFileExists = destinationFileState.FileExists;
}

// CreateHardLink and CreateSymbolicLink cannot overwrite an existing file or link
// so we need to delete the existing entry before we create the hard or symbolic link.
if (destinationFileExists)
{
FileUtilities.DeleteNoThrow(destinationFileState.Name);
}

linkCreated = createLink(sourceFileState.Name, destinationFileState.Name, errorMessage);
}

Expand Down Expand Up @@ -826,6 +819,11 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF
LogDiagnostic("Retrying on ERROR_ACCESS_DENIED because MSBUILDALWAYSRETRY = 1");
}
}
else if (code == NativeMethods.ERROR_INVALID_FILENAME)
{
// Invalid characters used in file name, no point retrying.
throw;
}

if (e is UnauthorizedAccessException)
{
Expand Down
1 change: 1 addition & 0 deletions src/Tasks/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ internal static class NativeMethods

internal const int HRESULT_E_CLASSNOTREGISTERED = -2147221164;

internal const int ERROR_INVALID_FILENAME = -2147024773; // Illegal characters in name
internal const int ERROR_ACCESS_DENIED = -2147024891; // ACL'd or r/o
internal const int ERROR_SHARING_VIOLATION = -2147024864; // File locked by another use

Expand Down