Skip to content

Commit

Permalink
Try to fix safeWriteToFile producing empty files on Windows (#14085)
Browse files Browse the repository at this point in the history
Use win32 APIs to write the temporary file before copying to the final
destination. Because we've observed the final file being empty, we
suspect that std::ostream::flush is not flushing.

Also add a test for it.
  • Loading branch information
garymm committed Dec 13, 2023
1 parent a98200b commit 6eb9269
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 17 deletions.
28 changes: 22 additions & 6 deletions src/filesys.cpp
Expand Up @@ -758,14 +758,32 @@ bool safeWriteToFile(const std::string &path, const std::string &content)
std::string tmp_file = path + ".~mt";

// Write to a tmp file
bool tmp_success = false;

#ifdef _WIN32
// We've observed behavior suggesting that the MSVC implementation of std::ofstream::flush doesn't
// actually flush, so we use win32 APIs.
HANDLE tmp_handle = CreateFile(
tmp_file.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (tmp_handle == INVALID_HANDLE_VALUE) {
return false;
}
DWORD bytes_written;
tmp_success = (WriteFile(tmp_handle, content.c_str(), content.size(), &bytes_written, nullptr) &&
FlushFileBuffers(tmp_handle));
CloseHandle(tmp_handle);
#else
std::ofstream os(tmp_file.c_str(), std::ios::binary);
if (!os.good())
if (!os.good()) {
return false;
}
os << content;
os.flush();
os.close();
if (os.fail()) {
// Remove the temporary file because writing it failed and it's useless.
tmp_success = !os.fail();
#endif

if (!tmp_success) {
remove(tmp_file.c_str());
return false;
}
Expand All @@ -777,14 +795,12 @@ bool safeWriteToFile(const std::string &path, const std::string &content)
// When creating the file, it can cause Windows Search indexer, virus scanners and other apps
// to query the file. This can make the move file call below fail.
// We retry up to 5 times, with a 1ms sleep between, before we consider the whole operation failed
int number_attempts = 0;
while (number_attempts < 5) {
for (int attempt = 0; attempt < 5; attempt++) {
rename_success = MoveFileEx(tmp_file.c_str(), path.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
if (rename_success)
break;
sleep_ms(1);
++number_attempts;
}
#else
// On POSIX compliant systems rename() is specified to be able to swap the
Expand Down
2 changes: 1 addition & 1 deletion src/unittest/CMakeLists.txt
Expand Up @@ -9,7 +9,7 @@ set (UNITTEST_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/test_compression.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_connection.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_craft.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_filepath.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_filesys.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_inventory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_irrptr.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_lua.cpp
Expand Down
35 changes: 25 additions & 10 deletions src/unittest/test_filepath.cpp → src/unittest/test_filesys.cpp
Expand Up @@ -26,10 +26,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "nodedef.h"
#include "noise.h"

class TestFilePath : public TestBase {
class TestFileSys : public TestBase
{
public:
TestFilePath() { TestManager::registerTestModule(this); }
const char *getName() { return "TestFilePath"; }
TestFileSys() { TestManager::registerTestModule(this); }
const char *getName() { return "TestFileSys"; }

void runTests(IGameDef *gamedef);

Expand All @@ -38,17 +39,19 @@ class TestFilePath : public TestBase {
void testRemoveLastPathComponent();
void testRemoveLastPathComponentWithTrailingDelimiter();
void testRemoveRelativePathComponent();
void testSafeWriteToFile();
};

static TestFilePath g_test_instance;
static TestFileSys g_test_instance;

void TestFilePath::runTests(IGameDef *gamedef)
void TestFileSys::runTests(IGameDef *gamedef)
{
TEST(testIsDirDelimiter);
TEST(testPathStartsWith);
TEST(testRemoveLastPathComponent);
TEST(testRemoveLastPathComponentWithTrailingDelimiter);
TEST(testRemoveRelativePathComponent);
TEST(testSafeWriteToFile);
}

////////////////////////////////////////////////////////////////////////////////
Expand All @@ -74,7 +77,7 @@ std::string p(std::string path)
}


void TestFilePath::testIsDirDelimiter()
void TestFileSys::testIsDirDelimiter()
{
UASSERT(fs::IsDirDelimiter('/') == true);
UASSERT(fs::IsDirDelimiter('A') == false);
Expand All @@ -87,7 +90,7 @@ void TestFilePath::testIsDirDelimiter()
}


void TestFilePath::testPathStartsWith()
void TestFileSys::testPathStartsWith()
{
const int numpaths = 12;
std::string paths[numpaths] = {
Expand Down Expand Up @@ -163,7 +166,7 @@ void TestFilePath::testPathStartsWith()
}


void TestFilePath::testRemoveLastPathComponent()
void TestFileSys::testRemoveLastPathComponent()
{
std::string path, result, removed;

Expand Down Expand Up @@ -200,7 +203,7 @@ void TestFilePath::testRemoveLastPathComponent()
}


void TestFilePath::testRemoveLastPathComponentWithTrailingDelimiter()
void TestFileSys::testRemoveLastPathComponentWithTrailingDelimiter()
{
std::string path, result, removed;

Expand Down Expand Up @@ -236,7 +239,7 @@ void TestFilePath::testRemoveLastPathComponentWithTrailingDelimiter()
}


void TestFilePath::testRemoveRelativePathComponent()
void TestFileSys::testRemoveRelativePathComponent()
{
std::string path, result;

Expand All @@ -262,3 +265,15 @@ void TestFilePath::testRemoveRelativePathComponent()
result = fs::RemoveRelativePathComponents(path);
UASSERT(result == p("/a/e"));
}


void TestFileSys::testSafeWriteToFile()
{
const std::string dest_path = fs::TempPath() + DIR_DELIM + "testSafeWriteToFile.txt";
const std::string test_data("hello\0world", 11);
fs::safeWriteToFile(dest_path, test_data);
UASSERT(fs::PathExists(dest_path));
std::string contents_actual;
UASSERT(fs::ReadFile(dest_path, contents_actual));
UASSERT(contents_actual == test_data);
}

0 comments on commit 6eb9269

Please sign in to comment.