diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index e96b10336b..5e8868da21 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #include "Common/Assert.h" @@ -276,33 +278,65 @@ bool DeleteDir(const std::string& filename) return false; } +// Repeatedly invokes func until it returns true or max_attempts failures. +// Waits after each failure, with each delay doubling in length. +template +static bool AttemptMaxTimesWithExponentialDelay(int max_attempts, std::chrono::milliseconds delay, + std::string_view func_name, const FuncType& func) +{ + for (int failed_attempts = 0; failed_attempts < max_attempts; ++failed_attempts) + { + if (func()) + { + return true; + } + if (failed_attempts + 1 < max_attempts) + { + INFO_LOG_FMT(COMMON, "{} attempt failed, delaying for {} milliseconds", func_name, + delay.count()); + std::this_thread::sleep_for(delay); + delay *= 2; + } + } + return false; +} + // renames file srcFilename to destFilename, returns true on success bool Rename(const std::string& srcFilename, const std::string& destFilename) { INFO_LOG_FMT(COMMON, "Rename: {} --> {}", srcFilename, destFilename); #ifdef _WIN32 - auto sf = UTF8ToTStr(srcFilename); - auto df = UTF8ToTStr(destFilename); - // The Internet seems torn about whether ReplaceFile is atomic or not. - // Hopefully it's atomic enough... - if (ReplaceFile(df.c_str(), sf.c_str(), nullptr, REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, - nullptr)) - return true; - // Might have failed because the destination doesn't exist. - if (GetLastError() == ERROR_FILE_NOT_FOUND) - { - if (MoveFile(sf.c_str(), df.c_str())) - return true; - } - ERROR_LOG_FMT(COMMON, "Rename: MoveFile failed on {} --> {}: {}", srcFilename, destFilename, - GetLastErrorString()); + const std::wstring source_wstring = UTF8ToTStr(srcFilename); + const std::wstring destination_wstring = UTF8ToTStr(destFilename); + + // On Windows ReplaceFile can fail spuriously due to antivirus checking or other noise. + // Retry the operation with increasing delays, and if none of them work there's probably a + // persistent problem. + const bool success = AttemptMaxTimesWithExponentialDelay( + 3, std::chrono::milliseconds(5), "Rename", [&source_wstring, &destination_wstring] { + if (ReplaceFile(destination_wstring.c_str(), source_wstring.c_str(), nullptr, + REPLACEFILE_IGNORE_MERGE_ERRORS, nullptr, nullptr)) + { + return true; + } + // Might have failed because the destination doesn't exist. + if (GetLastError() == ERROR_FILE_NOT_FOUND) + { + return MoveFile(source_wstring.c_str(), destination_wstring.c_str()) != 0; + } + return false; + }); + constexpr auto error_string_func = GetLastErrorString; #else - if (rename(srcFilename.c_str(), destFilename.c_str()) == 0) - return true; - ERROR_LOG_FMT(COMMON, "Rename: rename failed on {} --> {}: {}", srcFilename, destFilename, - LastStrerrorString()); + const bool success = rename(srcFilename.c_str(), destFilename.c_str()) == 0; + constexpr auto error_string_func = LastStrerrorString; #endif - return false; + if (!success) + { + ERROR_LOG_FMT(COMMON, "Rename: rename failed on {} --> {}: {}", srcFilename, destFilename, + error_string_func()); + } + return success; } #ifndef _WIN32