Skip to content

Commit 80e31f1

Browse files
committed
Support: Rewrite Windows implementation of sys::fs::rename to be more POSIXy.
The current implementation of rename uses ReplaceFile if the destination file already exists. According to the documentation for ReplaceFile, the source file is opened without a sharing mode. This means that there is a short interval of time between when ReplaceFile renames the file and when it closes the file during which the destination file cannot be opened. This behaviour is not POSIX compliant because rename is supposed to be atomic. It was also causing intermittent link failures when linking with a ThinLTO cache; the ThinLTO cache implementation expects all cache files to be openable. This patch addresses that problem by re-implementing rename using CreateFile and SetFileInformationByHandle. It is roughly a reimplementation of ReplaceFile with a better sharing policy as well as support for renaming in the case where the destination file does not exist. This implementation is still not fully POSIX. Specifically in the case where the destination file is open at the point when rename is called, there will be a short interval of time during which the destination file will not exist. It isn't clear whether it is possible to avoid this using the Windows API. Differential Revision: https://reviews.llvm.org/D38570 llvm-svn: 315079
1 parent fd65895 commit 80e31f1

File tree

3 files changed

+187
-62
lines changed

3 files changed

+187
-62
lines changed

llvm/include/llvm/Support/FileSystem.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,11 @@ std::error_code remove(const Twine &path, bool IgnoreNonExisting = true);
343343
/// platform-specific error code.
344344
std::error_code remove_directories(const Twine &path, bool IgnoreErrors = true);
345345

346-
/// @brief Rename \a from to \a to. Files are renamed as if by POSIX rename().
346+
/// @brief Rename \a from to \a to.
347+
///
348+
/// Files are renamed as if by POSIX rename(), except that on Windows there may
349+
/// be a short interval of time during which the destination file does not
350+
/// exist.
347351
///
348352
/// @param from The path to rename from.
349353
/// @param to The path to rename to. This is created.

llvm/lib/Support/Windows/Path.inc

Lines changed: 108 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -359,65 +359,126 @@ std::error_code is_local(int FD, bool &Result) {
359359
return is_local_internal(FinalPath, Result);
360360
}
361361

362-
std::error_code rename(const Twine &from, const Twine &to) {
363-
// Convert to utf-16.
364-
SmallVector<wchar_t, 128> wide_from;
365-
SmallVector<wchar_t, 128> wide_to;
366-
if (std::error_code ec = widenPath(from, wide_from))
367-
return ec;
368-
if (std::error_code ec = widenPath(to, wide_to))
369-
return ec;
370-
371-
std::error_code ec = std::error_code();
362+
static std::error_code rename_internal(HANDLE FromHandle, const Twine &To,
363+
bool ReplaceIfExists) {
364+
SmallVector<wchar_t, 0> ToWide;
365+
if (auto EC = widenPath(To, ToWide))
366+
return EC;
372367

373-
// Retry while we see recoverable errors.
374-
// System scanners (eg. indexer) might open the source file when it is written
375-
// and closed.
368+
std::vector<char> RenameInfoBuf(sizeof(FILE_RENAME_INFO) - sizeof(wchar_t) +
369+
(ToWide.size() * sizeof(wchar_t)));
370+
FILE_RENAME_INFO &RenameInfo =
371+
*reinterpret_cast<FILE_RENAME_INFO *>(RenameInfoBuf.data());
372+
RenameInfo.ReplaceIfExists = ReplaceIfExists;
373+
RenameInfo.RootDirectory = 0;
374+
RenameInfo.FileNameLength = ToWide.size();
375+
std::copy(ToWide.begin(), ToWide.end(), RenameInfo.FileName);
376+
377+
if (!SetFileInformationByHandle(FromHandle, FileRenameInfo, &RenameInfo,
378+
RenameInfoBuf.size()))
379+
return mapWindowsError(GetLastError());
376380

377-
bool TryReplace = true;
381+
return std::error_code();
382+
}
378383

379-
for (int i = 0; i < 2000; i++) {
380-
if (i > 0)
381-
::Sleep(1);
384+
std::error_code rename(const Twine &From, const Twine &To) {
385+
// Convert to utf-16.
386+
SmallVector<wchar_t, 128> WideFrom;
387+
SmallVector<wchar_t, 128> WideTo;
388+
if (std::error_code EC = widenPath(From, WideFrom))
389+
return EC;
390+
if (std::error_code EC = widenPath(To, WideTo))
391+
return EC;
382392

383-
if (TryReplace) {
384-
// Try ReplaceFile first, as it is able to associate a new data stream
385-
// with the destination even if the destination file is currently open.
386-
if (::ReplaceFileW(wide_to.data(), wide_from.data(), NULL, 0, NULL, NULL))
387-
return std::error_code();
393+
ScopedFileHandle FromHandle;
394+
// Retry this a few times to defeat badly behaved file system scanners.
395+
for (unsigned Retry = 0; Retry != 200; ++Retry) {
396+
if (Retry != 0)
397+
::Sleep(10);
398+
FromHandle =
399+
::CreateFileW(WideFrom.begin(), GENERIC_READ | DELETE,
400+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
401+
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
402+
if (FromHandle)
403+
break;
404+
}
405+
if (!FromHandle)
406+
return mapWindowsError(GetLastError());
388407

389-
DWORD ReplaceError = ::GetLastError();
390-
ec = mapWindowsError(ReplaceError);
408+
// We normally expect this loop to succeed after a few iterations. If it
409+
// requires more than 200 tries, it's more likely that the failures are due to
410+
// a true error, so stop trying.
411+
for (unsigned Retry = 0; Retry != 200; ++Retry) {
412+
auto EC = rename_internal(FromHandle, To, true);
413+
if (!EC || EC != errc::permission_denied)
414+
return EC;
391415

392-
// If ReplaceFileW returned ERROR_UNABLE_TO_MOVE_REPLACEMENT or
393-
// ERROR_UNABLE_TO_MOVE_REPLACEMENT_2, retry but only use MoveFileExW().
394-
if (ReplaceError == ERROR_UNABLE_TO_MOVE_REPLACEMENT ||
395-
ReplaceError == ERROR_UNABLE_TO_MOVE_REPLACEMENT_2) {
396-
TryReplace = false;
416+
// The destination file probably exists and is currently open in another
417+
// process, either because the file was opened without FILE_SHARE_DELETE or
418+
// it is mapped into memory (e.g. using MemoryBuffer). Rename it in order to
419+
// move it out of the way of the source file. Use FILE_FLAG_DELETE_ON_CLOSE
420+
// to arrange for the destination file to be deleted when the other process
421+
// closes it.
422+
ScopedFileHandle ToHandle(
423+
::CreateFileW(WideTo.begin(), GENERIC_READ | DELETE,
424+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
425+
NULL, OPEN_EXISTING,
426+
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE, NULL));
427+
if (!ToHandle) {
428+
auto EC = mapWindowsError(GetLastError());
429+
// Another process might have raced with us and moved the existing file
430+
// out of the way before we had a chance to open it. If that happens, try
431+
// to rename the source file again.
432+
if (EC == errc::no_such_file_or_directory)
397433
continue;
398-
}
399-
// If ReplaceFileW returned ERROR_UNABLE_TO_REMOVE_REPLACED, retry
400-
// using ReplaceFileW().
401-
if (ReplaceError == ERROR_UNABLE_TO_REMOVE_REPLACED)
402-
continue;
403-
// We get ERROR_FILE_NOT_FOUND if the destination file is missing.
404-
// MoveFileEx can handle this case.
405-
if (ReplaceError != ERROR_ACCESS_DENIED &&
406-
ReplaceError != ERROR_FILE_NOT_FOUND &&
407-
ReplaceError != ERROR_SHARING_VIOLATION)
408-
break;
434+
return EC;
409435
}
410436

411-
if (::MoveFileExW(wide_from.begin(), wide_to.begin(),
412-
MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING))
413-
return std::error_code();
437+
BY_HANDLE_FILE_INFORMATION FI;
438+
if (!GetFileInformationByHandle(ToHandle, &FI))
439+
return mapWindowsError(GetLastError());
440+
441+
// Try to find a unique new name for the destination file.
442+
for (unsigned UniqueId = 0; UniqueId != 200; ++UniqueId) {
443+
std::string TmpFilename = (To + ".tmp" + utostr(UniqueId)).str();
444+
if (auto EC = rename_internal(ToHandle, TmpFilename, false)) {
445+
if (EC == errc::file_exists || EC == errc::permission_denied) {
446+
// Again, another process might have raced with us and moved the file
447+
// before we could move it. Check whether this is the case, as it
448+
// might have caused the permission denied error. If that was the
449+
// case, we don't need to move it ourselves.
450+
ScopedFileHandle ToHandle2(::CreateFileW(
451+
WideTo.begin(), 0,
452+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
453+
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL));
454+
if (!ToHandle2) {
455+
auto EC = mapWindowsError(GetLastError());
456+
if (EC == errc::no_such_file_or_directory)
457+
break;
458+
return EC;
459+
}
460+
BY_HANDLE_FILE_INFORMATION FI2;
461+
if (!GetFileInformationByHandle(ToHandle2, &FI2))
462+
return mapWindowsError(GetLastError());
463+
if (FI.nFileIndexHigh != FI2.nFileIndexHigh ||
464+
FI.nFileIndexLow != FI2.nFileIndexLow ||
465+
FI.dwVolumeSerialNumber != FI2.dwVolumeSerialNumber)
466+
break;
467+
continue;
468+
}
469+
return EC;
470+
}
471+
break;
472+
}
414473

415-
DWORD MoveError = ::GetLastError();
416-
ec = mapWindowsError(MoveError);
417-
if (MoveError != ERROR_ACCESS_DENIED) break;
474+
// Okay, the old destination file has probably been moved out of the way at
475+
// this point, so try to rename the source file again. Still, another
476+
// process might have raced with us to create and open the destination
477+
// file, so we need to keep doing this until we succeed.
418478
}
419479

420-
return ec;
480+
// The most likely root cause.
481+
return errc::permission_denied;
421482
}
422483

423484
std::error_code resize_file(int FD, uint64_t Size) {

llvm/unittests/Support/ReplaceFileTest.cpp

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,21 @@ class ScopedFD {
5252
~ScopedFD() { Process::SafelyCloseFileDescriptor(FD); }
5353
};
5454

55+
bool FDHasContent(int FD, StringRef Content) {
56+
auto Buffer = MemoryBuffer::getOpenFile(FD, "", -1);
57+
assert(Buffer);
58+
return Buffer.get()->getBuffer() == Content;
59+
}
60+
61+
bool FileHasContent(StringRef File, StringRef Content) {
62+
int FD = 0;
63+
auto EC = fs::openFileForRead(File, FD);
64+
(void)EC;
65+
assert(!EC);
66+
ScopedFD EventuallyCloseIt(FD);
67+
return FDHasContent(FD, Content);
68+
}
69+
5570
TEST(rename, FileOpenedForReadingCanBeReplaced) {
5671
// Create unique temporary directory for this test.
5772
SmallString<128> TestDirectory;
@@ -79,25 +94,15 @@ TEST(rename, FileOpenedForReadingCanBeReplaced) {
7994

8095
// We should still be able to read the old data through the existing
8196
// descriptor.
82-
auto Buffer = MemoryBuffer::getOpenFile(ReadFD, TargetFileName, -1);
83-
ASSERT_TRUE(static_cast<bool>(Buffer));
84-
EXPECT_EQ(Buffer.get()->getBuffer(), "!!target!!");
97+
EXPECT_TRUE(FDHasContent(ReadFD, "!!target!!"));
8598

8699
// The source file should no longer exist
87100
EXPECT_FALSE(fs::exists(SourceFileName));
88101
}
89102

90-
{
91-
// If we obtain a new descriptor for the target file, we should find that it
92-
// contains the content that was in the source file.
93-
int ReadFD = 0;
94-
ASSERT_NO_ERROR(fs::openFileForRead(TargetFileName, ReadFD));
95-
ScopedFD EventuallyCloseIt(ReadFD);
96-
auto Buffer = MemoryBuffer::getOpenFile(ReadFD, TargetFileName, -1);
97-
ASSERT_TRUE(static_cast<bool>(Buffer));
98-
99-
EXPECT_EQ(Buffer.get()->getBuffer(), "!!source!!");
100-
}
103+
// If we obtain a new descriptor for the target file, we should find that it
104+
// contains the content that was in the source file.
105+
EXPECT_TRUE(FileHasContent(TargetFileName, "!!source!!"));
101106

102107
// Rename the target file back to the source file name to confirm that rename
103108
// still works if the destination does not already exist.
@@ -110,4 +115,59 @@ TEST(rename, FileOpenedForReadingCanBeReplaced) {
110115
ASSERT_NO_ERROR(fs::remove(TestDirectory.str()));
111116
}
112117

118+
TEST(rename, ExistingTemp) {
119+
// Test that existing .tmpN files don't get deleted by the Windows
120+
// sys::fs::rename implementation.
121+
SmallString<128> TestDirectory;
122+
ASSERT_NO_ERROR(
123+
fs::createUniqueDirectory("ExistingTemp-test", TestDirectory));
124+
125+
SmallString<128> SourceFileName(TestDirectory);
126+
path::append(SourceFileName, "source");
127+
128+
SmallString<128> TargetFileName(TestDirectory);
129+
path::append(TargetFileName, "target");
130+
131+
SmallString<128> TargetTmp0FileName(TestDirectory);
132+
path::append(TargetTmp0FileName, "target.tmp0");
133+
134+
SmallString<128> TargetTmp1FileName(TestDirectory);
135+
path::append(TargetTmp1FileName, "target.tmp1");
136+
137+
ASSERT_NO_ERROR(CreateFileWithContent(SourceFileName, "!!source!!"));
138+
ASSERT_NO_ERROR(CreateFileWithContent(TargetFileName, "!!target!!"));
139+
ASSERT_NO_ERROR(CreateFileWithContent(TargetTmp0FileName, "!!target.tmp0!!"));
140+
141+
{
142+
// Use mapped_file_region to make sure that the destination file is mmap'ed.
143+
// This will cause SetInformationByHandle to fail when renaming to the
144+
// destination, and we will follow the code path that tries to give target
145+
// a temporary name.
146+
int TargetFD;
147+
std::error_code EC;
148+
ASSERT_NO_ERROR(fs::openFileForRead(TargetFileName, TargetFD));
149+
ScopedFD X(TargetFD);
150+
sys::fs::mapped_file_region MFR(
151+
TargetFD, sys::fs::mapped_file_region::readonly, 10, 0, EC);
152+
ASSERT_FALSE(EC);
153+
154+
ASSERT_NO_ERROR(fs::rename(SourceFileName, TargetFileName));
155+
156+
#ifdef _WIN32
157+
// Make sure that target was temporarily renamed to target.tmp1 on Windows.
158+
// This is signified by a permission denied error as opposed to no such file
159+
// or directory when trying to open it.
160+
int Tmp1FD;
161+
EXPECT_EQ(errc::permission_denied,
162+
fs::openFileForRead(TargetTmp1FileName, Tmp1FD));
163+
#endif
164+
}
165+
166+
EXPECT_TRUE(FileHasContent(TargetTmp0FileName, "!!target.tmp0!!"));
167+
168+
ASSERT_NO_ERROR(fs::remove(TargetFileName));
169+
ASSERT_NO_ERROR(fs::remove(TargetTmp0FileName));
170+
ASSERT_NO_ERROR(fs::remove(TestDirectory.str()));
171+
}
172+
113173
} // anonymous namespace

0 commit comments

Comments
 (0)