/// <summary> /// Copy one file with the appropriate number of retries if it fails. /// </summary> private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationFileState, CopyFileWithState copyFile) { int retries = 0; while (!_cancellationTokenSource.IsCancellationRequested) { try { bool?result = copyFile(sourceFileState, destinationFileState); if (result.HasValue) { return(result.Value); } } catch (OperationCanceledException) { break; } catch (Exception e) when(ExceptionHandling.IsIoRelatedException(e)) { if (e is ArgumentException || // Invalid chars e is NotSupportedException || // Colon in the middle of the path e is PathTooLongException) { // No use retrying these cases throw; } if (e is UnauthorizedAccessException || e is IOException) // Not clear why we can get one and not the other { int code = Marshal.GetHRForException(e); LogDiagnostic("Got {0} copying {1} to {2} and HR is {3}", e.ToString(), sourceFileState.Name, destinationFileState.Name, code); if (code == NativeMethods.ERROR_ACCESS_DENIED) { // ERROR_ACCESS_DENIED can either mean there's an ACL preventing us, or the file has the readonly bit set. // In either case, that's likely not a race, and retrying won't help. // Retrying is mainly for ERROR_SHARING_VIOLATION, where someone else is using the file right now. // However, there is a limited set of circumstances where a copy failure will show up as access denied due // to a failure to reset the readonly bit properly, in which case retrying will succeed. This seems to be // a pretty edge scenario, but since some of our internal builds appear to be hitting it, provide a secret // environment variable to allow overriding the default behavior and forcing retries in this circumstance as well. if (!s_alwaysRetryCopy) { throw; } else { LogDiagnostic("Retrying on ERROR_ACCESS_DENIED because MSBUILDALWAYSRETRY = 1"); } } } if (e is IOException && DestinationFolder != null && FileSystems.Default.FileExists(DestinationFolder.ItemSpec)) { // We failed to create the DestinationFolder because it's an existing file. No sense retrying. // We don't check for this case upstream because it'd be another hit to the filesystem. throw; } if (e is IOException) { // if this was just because the source and destination files are the // same file, that's not a failure. // Note -- we check this exceptional case here, not before the copy, for perf. if (PathsAreIdentical(sourceFileState.Name, destinationFileState.Name)) { return(true); } } if (retries < Retries) { retries++; Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Name, destinationFileState.Name, retries, RetryDelayMilliseconds, e.Message, GetLockedFileMessage(destinationFileState.Name)); // if we have to retry for some reason, wipe the state -- it may not be correct anymore. destinationFileState.Reset(); Thread.Sleep(RetryDelayMilliseconds); continue; } else if (Retries > 0) { // Exception message is logged in caller Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Name, destinationFileState.Name, Retries, GetLockedFileMessage(destinationFileState.Name)); throw; } else { throw; } } if (retries < Retries) { retries++; Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Name, destinationFileState.Name, retries, RetryDelayMilliseconds, String.Empty /* no details */, GetLockedFileMessage(destinationFileState.Name)); // if we have to retry for some reason, wipe the state -- it may not be correct anymore. destinationFileState.Reset(); Thread.Sleep(RetryDelayMilliseconds); } else if (Retries > 0) { Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Name, destinationFileState.Name, Retries, GetLockedFileMessage(destinationFileState.Name)); return(false); } else { return(false); } } // Canceling return(false); }
/// <summary> /// Copy one file from source to destination. Create the target directory if necessary and /// leave the file read-write. /// </summary> /// <returns>Return true to indicate success, return false to indicate failure and NO retry, return NULL to indicate retry.</returns> private bool?CopyFileWithLogging ( FileState sourceFileState, // The source file FileState destinationFileState // The destination file ) { bool destinationFileExists = false; if (destinationFileState.DirectoryExists) { Log.LogErrorWithCodeFromResources("Copy.DestinationIsDirectory", sourceFileState.Name, destinationFileState.Name); return(false); } if (sourceFileState.DirectoryExists) { // If the source file passed in is actually a directory instead of a file, log a nice // error telling the user so. Otherwise, .NET Framework's File.Copy method will throw // an UnauthorizedAccessException saying "access is denied", which is not very useful // to the user. Log.LogErrorWithCodeFromResources("Copy.SourceIsDirectory", sourceFileState.Name); return(false); } if (!sourceFileState.FileExists) { Log.LogErrorWithCodeFromResources("Copy.SourceFileNotFound", sourceFileState.Name); return(false); } string destinationFolder = Path.GetDirectoryName(destinationFileState.Name); if (!string.IsNullOrEmpty(destinationFolder) && !_directoriesKnownToExist.ContainsKey(destinationFolder)) { if (!FileSystems.Default.DirectoryExists(destinationFolder)) { Log.LogMessageFromResources(MessageImportance.Normal, "Copy.CreatesDirectory", destinationFolder); Directory.CreateDirectory(destinationFolder); } // It's very common for a lot of files to be copied to the same folder. // Eg., "c:\foo\a"->"c:\bar\a", "c:\foo\b"->"c:\bar\b" and so forth. // We don't want to check whether this folder exists for every single file we copy. So store which we've checked. _directoriesKnownToExist.TryAdd(destinationFolder, true); } if (OverwriteReadOnlyFiles) { MakeFileWriteable(destinationFileState, true); destinationFileExists = destinationFileState.FileExists; } bool linkCreated = false; string errorMessage = string.Empty; // If we want to create hard or symbolic links, then try that first if (UseHardlinksIfPossible) { TryCopyViaLink("Copy.HardLinkComment", MessageImportance.Normal, sourceFileState, destinationFileState, ref destinationFileExists, out linkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethods.MakeHardLink(destination, source, ref errorMessage)); } else if (UseSymboliclinksIfPossible) { TryCopyViaLink("Copy.SymbolicLinkComment", MessageImportance.Normal, sourceFileState, destinationFileState, ref destinationFileExists, out linkCreated, ref errorMessage, (source, destination, errMessage) => NativeMethods.MakeSymbolicLink(destination, source, ref errorMessage)); } if (ErrorIfLinkFails && !linkCreated) { Log.LogErrorWithCodeFromResources("Copy.LinkFailed", sourceFileState.Name, destinationFileState.Name); return(false); } // If the link was not created (either because the user didn't want one, or because it couldn't be created) // then let's copy the file if (!linkCreated) { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive string sourceFilePath = FileUtilities.GetFullPathNoThrow(sourceFileState.Name); string destinationFilePath = FileUtilities.GetFullPathNoThrow(destinationFileState.Name); Log.LogMessageFromResources(MessageImportance.Normal, "Copy.FileComment", sourceFilePath, destinationFilePath); File.Copy(sourceFileState.Name, destinationFileState.Name, 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); }