Files
UnrealEngine/Engine/Source/Programs/UnrealBuildAccelerator/Common/Private/UbaFileAccessor.cpp
2025-05-18 13:04:45 +08:00

587 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "UbaFileAccessor.h"
#include "UbaFile.h"
#include "UbaLogger.h"
#include "UbaStats.h"
#if PLATFORM_LINUX
#include <sys/sendfile.h>
#elif PLATFORM_MAC
#include <copyfile.h>
#endif
#define UBA_USE_SPARSE_FILE 0
namespace uba
{
constexpr u64 WriteUnit = 4096;
#if PLATFORM_WINDOWS
void GetProcessHoldingFile(StringBufferBase& out, const tchar* fileName);
HANDLE asHANDLE(FileHandle fh);
#else
int asFileDescriptor(FileHandle fh);
Atomic<u32> g_tempFileCounter;
#endif
#if PLATFORM_WINDOWS
bool SetDeleteOnClose(Logger& logger, const tchar* fileName, FileHandle& handle, bool value)
{
ExtendedTimerScope ts(KernelStats::GetCurrent().setFileInfo);
FILE_DISPOSITION_INFO info;
info.DeleteFile = value;
if (!::SetFileInformationByHandle(asHANDLE(handle), FileDispositionInfo, &info, sizeof(info)))
return logger.Error(TC("SetFileInformationByHandle (FileDispositionInfo) failed on %llu %s (%s)"), uintptr_t(handle), fileName, LastErrorToText().data);
return true;
}
u64 GetFilePointer(Logger& logger, FileHandle& handle)
{
LARGE_INTEGER pos;
if (SetFilePointerEx(asHANDLE(handle), ToLargeInteger(0), &pos, FILE_CURRENT))
return pos.QuadPart;
logger.Error(TC("SetFilePointerEx failed (%s)"), LastErrorToText().data);
return ~0ull;
}
#endif
FileAccessor::FileAccessor(Logger& logger, const tchar* fileName)
: m_logger(logger)
, m_fileName(fileName)
{
}
FileAccessor::~FileAccessor()
{
InternalClose(false, nullptr);
}
bool FileAccessor::CreateWrite(bool allowRead, u32 flagsAndAttributes, u64 fileSize, const tchar* tempPath)
{
return InternalCreateWrite(allowRead, flagsAndAttributes, fileSize, tempPath, false);
}
bool FileAccessor::InternalCreateWrite(bool allowRead, u32 flagsAndAttributes, u64 fileSize, const tchar* tempPath, bool isMemoryMap)
{
UBA_ASSERT(flagsAndAttributes != 0);
m_size = fileSize;
m_flagsAndAttributes = flagsAndAttributes;
const tchar* realFileName = m_fileName;
#if !PLATFORM_WINDOWS
m_tempPath = tempPath;
StringBuffer<> tempFile;
if (tempPath)
{
#if PLATFORM_LINUX
allowRead = true; // Needed if rename does not work
#endif
m_tempFileIndex = g_tempFileCounter++;
realFileName = tempFile.Append(tempPath).Append("Temp_").AppendValue(m_tempFileIndex).data;
}
#endif
u32 createDisp = CREATE_ALWAYS;
u32 dwDesiredAccess = GENERIC_WRITE | DELETE;
if (allowRead)
dwDesiredAccess |= GENERIC_READ;
u32 dwShareMode = 0;// FILE_SHARE_READ | FILE_SHARE_WRITE;
u32 retryCount = 0;
StringBuffer<256> additionalInfo;
while (true)
{
m_fileHandle = uba::CreateFileW(realFileName, dwDesiredAccess, dwShareMode, createDisp, flagsAndAttributes);
if (m_fileHandle != InvalidFileHandle)
{
if (retryCount)
{
LogEntryType logType = retryCount > 10 ? LogEntryType_Warning : LogEntryType_Info;
m_logger.Logf(logType, TC("Had to retry for %u seconds to open file %s for write (because it was being used%s)"), retryCount/2, realFileName, additionalInfo.data);
}
break;
}
u32 lastError = GetLastError();
#if PLATFORM_WINDOWS
if (lastError == ERROR_SHARING_VIOLATION || lastError == ERROR_USER_MAPPED_FILE || lastError == ERROR_ACCESS_DENIED)
{
// Since we have unmap of files in jobs it might be that the file is not unmapped yet, therefor we should retry
if (retryCount == 1)
GetProcessHoldingFile(additionalInfo, m_fileName);
if (retryCount < 40)
{
Sleep(500);
++retryCount;
continue;
}
}
#endif
const tchar* retryText = retryCount ? TC(" after retrying for 20 seconds") : TC("");
return m_logger.Error(TC("ERROR opening file %s for write%s (%s%s)"), realFileName, retryText, LastErrorToText(lastError).data, additionalInfo.data);
}
#if PLATFORM_WINDOWS
if (!SetDeleteOnClose(m_logger, m_fileName, m_fileHandle, true))
return false;
if (fileSize != 0)
{
u64 allocSize = m_size;
if ((m_flagsAndAttributes & FILE_FLAG_NO_BUFFERING) != 0)
allocSize = AlignUp(fileSize, 4*1024);
if ((m_flagsAndAttributes & FILE_FLAG_OVERLAPPED) != 0)
(u64&)m_fileHandle |= OverlappedIoFlag;
#if UBA_USE_SPARSE_FILE
{
HANDLE handle = asHANDLE(m_fileHandle);
DWORD dwTemp;
if (!::DeviceIoControl(handle, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &dwTemp, NULL))
{
DWORD lastError = GetLastError();
if (lastError != ERROR_INVALID_FUNCTION) // Some file systems don't support this
return m_logger.Error(TC("Failed to make file %s sparse (%s)"), realFileName, LastErrorToText(lastError).data);
}
if (!SetEndOfFile(m_logger, realFileName, m_fileHandle, allocSize))
return false;
}
#else
{
FILE_ALLOCATION_INFO info;
info.AllocationSize = ToLargeInteger(allocSize);
if (!::SetFileInformationByHandle(asHANDLE(m_fileHandle), FileAllocationInfo, &info, sizeof(info)))
if (!IsRunningWine())
return m_logger.Error(TC("SetFileInformationByHandle (FileDispositionInfo) failed on %llu %s (%s)"), uintptr_t(m_fileHandle), realFileName, LastErrorToText().data);
}
#endif
}
#elif PLATFORM_LINUX
if (isMemoryMap && fileSize != 0) // We want to make sure we allocate memory on the file system to catch out-of-diskspace
{
if (fallocate(asFileDescriptor(m_fileHandle), 0, 0, fileSize) == -1)
if (errno != ENOSYS && errno != EOPNOTSUPP)
return m_logger.Error(TC("fallocate failed on %i %s (%s)"), asFileDescriptor(m_fileHandle), realFileName, strerror(errno));
}
#endif
m_isWrite = true;
return true;
}
bool FileAccessor::CreateMemoryWrite(bool allowRead, u32 flagsAndAttributes, u64 size, const tchar* tempPath)
{
allowRead = true; // It is not possible to have write only access to file mappings it seems
UBA_ASSERT(flagsAndAttributes != 0);
if (!InternalCreateWrite(allowRead, flagsAndAttributes, size, tempPath, true))
return false;
const tchar* realFileName = m_fileName;
#if !PLATFORM_WINDOWS
StringBuffer<> tempFile;
if (m_tempPath)
realFileName = tempFile.Append(m_tempPath).Append("Temp_").AppendValue(m_tempFileIndex).data;
#endif
m_mappingHandle = uba::CreateFileMappingW(m_logger, m_fileHandle, PAGE_READWRITE, size, realFileName);
if (!m_mappingHandle.IsValid())
return m_logger.Error(TC("Failed to create memory map %s with size %llu (%s)"), realFileName, size, LastErrorToText().data);
m_data = MapViewOfFile(m_logger, m_mappingHandle, FILE_MAP_WRITE, 0, size);
if (!m_data)
return m_logger.Error(TC("Failed to map view of file %s with size %llu, for write (%s)"), realFileName, size, LastErrorToText().data);
return true;
}
bool FileAccessor::Close(u64* lastWriteTime)
{
return InternalClose(true, lastWriteTime);
}
bool FileAccessor::Write(const void* data, u64 dataLen, u64 offset, bool lastWrite)
{
auto& stats = KernelStats::GetCurrent();
ExtendedTimerScope ts(stats.writeFile);
if (!m_isWrite)
return m_logger.Error(TC("File %s is not opened for write"), m_fileName);
stats.writeFile.bytes += dataLen;
return InternalWrite(data, dataLen, offset, lastWrite);
}
bool FileAccessor::InternalWrite(const void* data, u64 dataLen, u64 offset, bool lastWrite)
{
#if PLATFORM_WINDOWS
bool noBuffering = (m_flagsAndAttributes & FILE_FLAG_NO_BUFFERING) != 0;
if (noBuffering)
{
if (m_writeThroughBufferSize)
{
if (offset != m_writeThroughBufferPos)
return m_logger.Error(TC("NoBuffering require file to be written sequentially or at 4k sizes (%s)"), m_fileName);
UBA_ASSERT(!((u64)m_fileHandle & OverlappedIoFlag) || !offset);
u64 toWrite = Min(WriteUnit - m_writeThroughBufferSize, dataLen);
memcpy(m_writeThroughBuffer + m_writeThroughBufferSize, data, toWrite);
m_writeThroughBufferSize += u32(toWrite);
dataLen -= toWrite;
data = (const u8*)data + toWrite;
if (m_writeThroughBufferSize < WriteUnit)
return true;
m_writeThroughBufferSize = 0;
if (!InternalWrite(m_writeThroughBuffer, WriteUnit, 0, lastWrite))
return false;
}
}
if ((u64)m_fileHandle & OverlappedIoFlag)
{
constexpr u64 BlockSize = 1024 * 1024;
constexpr u64 BlockCount = 32;
OVERLAPPED ol[BlockCount];
Event ev[BlockCount];
u64 writeOffset = offset;
u64 writeLeft = dataLen;
u8* pos = (u8*)data;
u64 i = 0;
u8 tailBuffer[WriteUnit];
auto WaitAndCheckError = [&](u64 index)
{
if (!ev[index].IsSet())
return m_logger.Error(L"Overlapped I/O WriteFile FAILED on waiting for event!");
u32 error = u32(ol[index].Internal);
if (error != ERROR_SUCCESS)
return m_logger.Error(L"Overlapped I/O WriteFile FAILED!: %s", LastErrorToText(error).data);
return true;
};
auto eg = MakeGuard([&]()
{
for (u64 j=0, e=Min(i, BlockCount); j!=e; ++j)
if (!WaitAndCheckError(j))
return false;
return true;
});
#if 0//UBA_USE_SPARSE_FILE
u8 experiment[WriteUnit];
if (m_writeThroughBufferPos == 0 && lastWrite && dataLen > WriteUnit*4)
{
u64 lastOffset = (dataLen / WriteUnit) * WriteUnit - WriteUnit;
if (lastOffset)
{
ev[0].Create(true);
ol[0] = {};
ol[0].hEvent = ev[0].GetHandle();
ol[0].Offset = ToLow(lastOffset);
ol[0].OffsetHigh = ToHigh(lastOffset);
if (!::WriteFile(asHANDLE(m_fileHandle), experiment, WriteUnit, NULL, ol))
{
u32 lastError = GetLastError();
if (lastError != ERROR_IO_PENDING)
return m_logger.Error(L"FAILED!: %s", LastErrorToText(lastError).data);
}
if (!WaitAndCheckError(0))
return false;
}
}
#endif
while (writeLeft)
{
u64 index = i % BlockCount;
if (i < BlockCount)
ev[i].Create(true);
else
{
if (!WaitAndCheckError(index))
return false;
}
u64 toWrite = Min(writeLeft, BlockSize);
u64 toActuallyWrite = toWrite;
if (noBuffering && toWrite < BlockSize)
{
toActuallyWrite = (toWrite / WriteUnit) * WriteUnit;
if (!toActuallyWrite)
{
if (lastWrite)
{
memcpy(tailBuffer, pos, toWrite);
pos = tailBuffer;
toActuallyWrite = WriteUnit;
m_writeThroughBufferSize = 0;
}
else
{
if (!m_writeThroughBuffer)
m_writeThroughBuffer = new u8[WriteUnit];
memcpy(m_writeThroughBuffer, pos, toWrite);
m_writeThroughBufferSize = u32(toWrite);
break;
}
}
else
toWrite = toActuallyWrite;
}
ol[index] = {};
ol[index].hEvent = ev[index].GetHandle();
ol[index].Offset = ToLow(writeOffset);
ol[index].OffsetHigh = ToHigh(writeOffset);
ev[index].Reset();
if (!::WriteFile(asHANDLE(m_fileHandle), pos, u32(toActuallyWrite), NULL, ol + index))
{
u32 lastError = GetLastError();
if (lastError != ERROR_IO_PENDING)
return m_logger.Error(L"FAILED!: %s", LastErrorToText(lastError).data);
}
++i;
writeOffset += toActuallyWrite;
pos += toWrite;
writeLeft -= toWrite;
}
if (!eg.Execute())
return false;
m_writeThroughBufferPos = u64(pos - (const u8*)data);
return true;
}
#endif
u64 writeLeft = dataLen;
u8* pos = (u8*)data;
while (writeLeft)
{
u32 toWrite = u32(Min(writeLeft, 256llu * 1024 * 1024));
u32 toActuallyWrite = toWrite;
#if PLATFORM_WINDOWS
DWORD written;
if (!::WriteFile(asHANDLE(m_fileHandle), pos, toActuallyWrite, &written, NULL))
{
DWORD lastError = GetLastError();
m_logger.Error(TC("ERROR writing file %s writing %u bytes (%llu bytes written out of %llu. FilePos: %llu) (%s)"), m_fileName, toActuallyWrite, (dataLen - writeLeft), dataLen, GetFilePointer(m_logger, m_fileHandle), LastErrorToText(lastError).data);
if (lastError == ERROR_DISK_FULL)
ExitProcess(ERROR_DISK_FULL);
return false;
}
if (written > toWrite)
written = toWrite;
#else
ssize_t written = write(asFileDescriptor(m_fileHandle), pos, toActuallyWrite);
if (written == -1)
{
UBA_ASSERTF(false, TC("WriteFile error handling not implemented for %i (%s)"), errno, strerror(errno));
return false;
}
#endif
writeLeft -= written;
pos += written;
}
return true;
}
bool FileAccessor::OpenRead()
{
UBA_ASSERT(false);
return false;
}
bool FileAccessor::OpenMemoryRead(u64 offset, bool errorOnFail)
{
if (!OpenFileSequentialRead(m_logger, m_fileName, m_fileHandle))
return errorOnFail ? m_logger.Error(TC("Failed to open file %s for read"), m_fileName) : false;
FileBasicInformation info;
if (!GetFileBasicInformationByHandle(info))
return m_logger.Error(TC("GetFileInformationByHandle failed on %s"), m_fileName);
m_size = info.size;
#if PLATFORM_WINDOWS
if (m_size)
m_mappingHandle = uba::CreateFileMappingW(m_logger, m_fileHandle, PAGE_READONLY, m_size, m_fileName);
else
m_mappingHandle = uba::CreateFileMappingW(m_logger, InvalidFileHandle, PAGE_READONLY, 1, m_fileName);
if (!m_mappingHandle.IsValid())
return m_logger.Error(TC("Failed to create mapping handle for %s with size %llu (%s)"), m_fileName, m_size, LastErrorToText().data);
#else
m_mappingHandle = { asFileDescriptor(m_fileHandle) };
if (offset == m_size)
return true;
#endif
m_data = MapViewOfFile(m_logger, m_mappingHandle, FILE_MAP_READ, offset, m_size);
if (!m_data)
return m_logger.Error(TC("%s - MapViewOfFile failed (%s)"), m_fileName, LastErrorToText().data);
return true;
}
bool FileAccessor::GetFileBasicInformationByHandle(FileBasicInformation& out)
{
return uba::GetFileBasicInformationByHandle(out, m_logger, m_fileName, m_fileHandle);
}
bool FileAccessor::InternalClose(bool success, u64* lastWriteTime)
{
if (m_data)
{
if (!UnmapViewOfFile(m_logger, m_data, m_size, m_fileName))
return m_logger.Error(TC("Failed to unmap memory for %s (%s)"), m_fileName, LastErrorToText().data);
m_data = nullptr;
}
if (m_mappingHandle.IsValid())
{
if (!CloseFileMapping(m_logger, m_mappingHandle, m_fileName))
return m_logger.Error(TC("Failed to close file mapping for %s (%s)"), m_fileName, LastErrorToText().data);
m_mappingHandle = {};
}
if (m_fileHandle == InvalidFileHandle)
return true;
const tchar* realFileName = m_fileName;
StringBuffer<> tempFile;
auto closeFile = MakeGuard([&]()
{
if (!CloseFile(realFileName, m_fileHandle))
return m_logger.Error(TC("Failed to close file %s (%s)"), realFileName, LastErrorToText().data);
m_fileHandle = InvalidFileHandle;
return true;
});
if (!m_isWrite)
return closeFile.Execute();
if (m_writeThroughBufferSize)
{
m_writeThroughBufferSize = 0;
if (!Write(m_writeThroughBuffer, WriteUnit, m_writeThroughBufferPos))
return false;
}
if ((m_flagsAndAttributes & FILE_FLAG_NO_BUFFERING) != 0)
if (!SetEndOfFile(m_logger, m_fileName, m_fileHandle, m_size))
return false;
if (m_writeThroughBuffer)
{
delete[] m_writeThroughBuffer;
m_writeThroughBuffer = nullptr;
}
#if !PLATFORM_WINDOWS
if (m_tempPath)
realFileName = tempFile.Append(m_tempPath).Append("Temp_").AppendValue(m_tempFileIndex).data;
#endif
if (success)
{
#if PLATFORM_WINDOWS
if (!SetDeleteOnClose(m_logger, realFileName, m_fileHandle, false))
return m_logger.Error(TC("Failed to remove delete on close for file %s (%s)"), realFileName, LastErrorToText().data);
#else
if (m_tempPath)
{
if (rename(realFileName, m_fileName) == -1)
{
if (errno != EXDEV)
return m_logger.Error(TC("Failed to rename temporary file %s to %s (%s)"), realFileName, m_fileName, strerror(errno));
// Need to copy, can't rename over devices
int targetFd = open(m_fileName, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, m_flagsAndAttributes);
auto closeTargetFile = MakeGuard([&]() { close(targetFd); });
if (targetFd == -1)
return m_logger.Error(TC("Failed to create file %s for move from temporary file %s (%s)"), m_fileName, realFileName, strerror(errno));
int sourceFd = asFileDescriptor(m_fileHandle);
#if PLATFORM_MAC
if (fcopyfile(sourceFd, targetFd, 0, COPYFILE_ALL) == -1)
return m_logger.Error(TC("Failed to do fcopyfile from temporary %s to file %s (%s)"), realFileName, m_fileName, strerror(errno));
#else
if (lseek(sourceFd, 0, SEEK_SET) == -1)
return m_logger.Error(TC("Failed to do lseek to beginning for sendfile (%s)"), strerror(errno));
u64 left = m_size;
while (left)
{
auto written = copy_file_range(sourceFd, NULL, targetFd, NULL, left, 0);
if (written == -1)
break;
left -= written;
}
if (left)
{
left = m_size;
if (lseek(sourceFd, 0, SEEK_SET) == -1)
return m_logger.Error(TC("Failed to do lseek to beginning of sourceFd (%s)"), strerror(errno));
if (lseek(targetFd, 0, SEEK_SET) == -1)
return m_logger.Error(TC("Failed to do lseek to beginning of targetFd (%s)"), strerror(errno));
while (left)
{
u8 buffer[256*1024];
u64 toCopy = Min(left, u64(sizeof_array(buffer)));
auto bytesRead = read(sourceFd, buffer, toCopy);
if (bytesRead <= 0)
return m_logger.Error(TC("Failed to do read from file %s (%s)"), realFileName, strerror(errno));
u8* readPos = buffer;
u64 toWrite = bytesRead;
while (toWrite)
{
auto written = write(targetFd, readPos, toWrite);
if (written == -1)
return m_logger.Error(TC("Failed to write to file %s (%s)"), m_fileName, strerror(errno));
toWrite -= written;
readPos += written;
}
left -= bytesRead;
}
}
#endif
remove(realFileName); // Remove real file now when we have copied it over
}
if (lastWriteTime) // Have to reopen it seems
{
if (!CloseFile(m_fileName, m_fileHandle))
return m_logger.Error(TC("Failed to close file %s (%s)"), m_fileName, LastErrorToText().data);
if (!OpenFileSequentialRead(m_logger, m_fileName, m_fileHandle))
return m_logger.Error(TC("Failed to re-open file %s (%s)"), m_fileName, LastErrorToText().data);
}
}
#endif
if (lastWriteTime)
{
*lastWriteTime = 0;
if (!GetFileLastWriteTime(*lastWriteTime, m_fileHandle))
m_logger.Warning(TC("Failed to get file time for %s %llu (%s)"), m_fileName, m_fileHandle, LastErrorToText().data);
}
}
else
{
#if !PLATFORM_WINDOWS
if (m_tempPath && remove(realFileName) == -1)
return m_logger.Error(TC("Failed to remove temporary file %s (%s)"), realFileName, strerror(errno));
#endif
}
return closeFile.Execute();
}
}