2064 lines
51 KiB
C++
2064 lines
51 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "UnsyncFile.h"
|
|
#include "UnsyncCore.h"
|
|
#include "UnsyncFilter.h"
|
|
#include "UnsyncHash.h"
|
|
#include "UnsyncMemory.h"
|
|
#include "UnsyncScheduler.h"
|
|
#include "UnsyncThread.h"
|
|
|
|
#include <mutex>
|
|
|
|
#if UNSYNC_PLATFORM_UNIX
|
|
# include <errno.h>
|
|
# include <sys/stat.h>
|
|
# include <sys/types.h>
|
|
# include <unistd.h>
|
|
#endif // UNSYNC_PLATFORM_UNIX
|
|
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
UNSYNC_THIRD_PARTY_INCLUDES_START
|
|
# include <winioctl.h>
|
|
UNSYNC_THIRD_PARTY_INCLUDES_END
|
|
#endif // UNSYNC_PLATFORM_WINDOWS
|
|
|
|
namespace unsync {
|
|
|
|
bool GForceBufferedFiles = false;
|
|
|
|
// Windows epoch : 1601-01-01T00:00:00Z
|
|
// Unix epoch : 1970-01-01T00:00:00Z
|
|
static constexpr uint64 SECONDS_BETWEEN_WINDOWS_AND_UNIX = 11'644'473'600ull;
|
|
static constexpr uint64 NANOS_PER_WINDOWS_TICK = 100ull;
|
|
static constexpr uint64 WINDOWS_TICKS_PER_SECOND = 1'000'000'000ull / NANOS_PER_WINDOWS_TICK; // each tick is 100ns
|
|
|
|
// Returns extended absolute path of a form \\?\D:\verylongpath or \\?\UNC\servername\verylongpath
|
|
// Expects an absolute path input. Returns original path on non-Windows.
|
|
// https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
|
|
FPath
|
|
MakeExtendedAbsolutePath(const FPath& InAbsolutePath)
|
|
{
|
|
if (InAbsolutePath.empty())
|
|
{
|
|
return FPath();
|
|
}
|
|
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
UNSYNC_ASSERTF(InAbsolutePath.is_absolute(), L"Input path '%ls' must be absolute", InAbsolutePath.wstring().c_str());
|
|
const std::wstring& InFilenameString = InAbsolutePath.native();
|
|
if (InFilenameString.starts_with(L"\\\\?\\"))
|
|
{
|
|
return InAbsolutePath;
|
|
}
|
|
else if (InFilenameString.starts_with(L"\\\\"))
|
|
{
|
|
return std::wstring(L"\\\\?\\UNC\\") + InFilenameString.substr(2);
|
|
}
|
|
else
|
|
{
|
|
return std::wstring(L"\\\\?\\") + InFilenameString;
|
|
}
|
|
#else // UNSYNC_PLATFORM_WINDOWS
|
|
return InAbsolutePath;
|
|
#endif // UNSYNC_PLATFORM_WINDOWS
|
|
}
|
|
|
|
// Removes \\, \\?\UNC\, \\.\UNC\, \\.\ or \\?\ prefix from a path.
|
|
// \\server\foo\bar -> server\foo\bar
|
|
// \\?\d:\foo\bar -> d:\foo\bar
|
|
// d:\foo\bar -> d:\foo\bar
|
|
// Returns original path on non-Windows.
|
|
static inline FPathStringView
|
|
RemoveUNCPrefix(const FPath& InPath)
|
|
{
|
|
FPathStringView InPathString = InPath.native();
|
|
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
if (InPathString.starts_with(L"\\\\?\\UNC\\"))
|
|
{
|
|
return InPathString.substr(8);
|
|
}
|
|
else if (InPathString.starts_with(L"\\\\?\\"))
|
|
{
|
|
return InPathString.substr(4);
|
|
}
|
|
else if (InPathString.starts_with(L"\\\\"))
|
|
{
|
|
return InPathString.substr(2);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
return InPathString;
|
|
}
|
|
}
|
|
|
|
FPath
|
|
RemoveExtendedPathPrefix(const FPath& InPath)
|
|
{
|
|
FPathStringView InPathString = InPath.native();
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
if (InPathString.starts_with(L"\\\\?\\UNC\\"))
|
|
{
|
|
FPathStringView Remainder = InPathString.substr(8);
|
|
std::wstring Result;
|
|
Result.reserve(Remainder.length() + 2);
|
|
Result += L"\\\\";
|
|
Result += Remainder;
|
|
return FPath(Result);
|
|
}
|
|
else if (InPathString.starts_with(L"\\\\?\\"))
|
|
{
|
|
return InPathString.substr(4);
|
|
}
|
|
else
|
|
{
|
|
return InPathString;
|
|
}
|
|
#else // UNSYNC_PLATFORM_WINDOWS
|
|
return InPathString;
|
|
#endif // UNSYNC_PLATFORM_WINDOWS
|
|
}
|
|
|
|
std::filesystem::file_time_type
|
|
FromWindowsFileTime(uint64 Ticks)
|
|
{
|
|
using FileTimeDuration = std::filesystem::file_time_type::duration;
|
|
|
|
uint64 RawSeconds = Ticks / WINDOWS_TICKS_PER_SECOND;
|
|
uint64 RawSubsecondTicks = Ticks - (RawSeconds * WINDOWS_TICKS_PER_SECOND);
|
|
uint64 RawSubsecondNanos = RawSubsecondTicks * NANOS_PER_WINDOWS_TICK;
|
|
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
FileTimeDuration Seconds = std::chrono::duration_cast<FileTimeDuration>(std::chrono::seconds(RawSeconds));
|
|
#else // UNSYNC_PLATFORM_WINDOWS
|
|
FileTimeDuration Seconds = std::chrono::seconds(RawSeconds - SECONDS_BETWEEN_WINDOWS_AND_UNIX);
|
|
#endif // UNSYNC_PLATFORM_WINDOWS
|
|
|
|
FileTimeDuration SubsecondNanos = std::chrono::duration_cast<FileTimeDuration>(std::chrono::nanoseconds(RawSubsecondNanos));
|
|
|
|
FileTimeDuration DurationFromNativeEpoch = Seconds + SubsecondNanos;
|
|
|
|
std::filesystem::file_time_type Result(DurationFromNativeEpoch);
|
|
|
|
return Result;
|
|
}
|
|
|
|
FPath
|
|
GetRelativePath(const FPath& Path, const FPath& Base)
|
|
{
|
|
FPathStringView ResultView = GetRelativePathView(Path, Base);
|
|
return ResultView;
|
|
}
|
|
|
|
FPathStringView
|
|
GetRelativePathView(const FPath& Path, const FPath& Base)
|
|
{
|
|
// Try a trivial case first, without touching the filesystem
|
|
FPathStringView PathView = RemoveUNCPrefix(Path);
|
|
FPathStringView BaseView = RemoveUNCPrefix(Base);
|
|
|
|
if (PathView.starts_with(BaseView))
|
|
{
|
|
FPathStringView PathViewRemainder = PathView.substr(BaseView.length());
|
|
if (PathViewRemainder.starts_with(FPath::preferred_separator))
|
|
{
|
|
FPathStringView RelativePath = PathView.substr(BaseView.length());
|
|
while (RelativePath.starts_with(FPath::preferred_separator))
|
|
{
|
|
RelativePath = RelativePath.substr(1);
|
|
}
|
|
return RelativePath;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void
|
|
ConvertDirectorySeparatorsToNative(std::string& Path)
|
|
{
|
|
std::replace_if(Path.begin(), Path.end(), [](char C) { return C == '/' || C == '\\'; }, PATH_SEPARATOR);
|
|
}
|
|
|
|
void
|
|
ConvertDirectorySeparatorsToUnix(std::string& Path)
|
|
{
|
|
std::replace_if(Path.begin(), Path.end(), [](char C) { return C == '\\'; }, char('/'));
|
|
}
|
|
|
|
void
|
|
ConvertDirectorySeparatorsToNative(std::wstring& Path)
|
|
{
|
|
std::replace_if(Path.begin(), Path.end(), [](wchar_t C) { return C == '/' || C == '\\'; }, wchar_t(FPath::preferred_separator));
|
|
}
|
|
|
|
void
|
|
ConvertDirectorySeparatorsToUnix(std::wstring& Path)
|
|
{
|
|
std::replace_if(Path.begin(), Path.end(), [](wchar_t C) { return C == '\\'; }, wchar_t('/'));
|
|
}
|
|
|
|
std::error_code
|
|
CopyFileIfNewer(const FPath& Source, const FPath& Target)
|
|
{
|
|
FFileAttributes SourceAttr = GetFileAttrib(Source);
|
|
FFileAttributes TargetAttr = GetFileAttrib(Target);
|
|
std::error_code Ec;
|
|
if (SourceAttr.Size != TargetAttr.Size || SourceAttr.Mtime != TargetAttr.Mtime)
|
|
{
|
|
FileCopyOverwrite(Source, Target, Ec);
|
|
}
|
|
return Ec;
|
|
}
|
|
|
|
bool
|
|
IsNonCaseSensitiveFileSystem(const FPath& ExistingPath)
|
|
{
|
|
UNSYNC_ASSERTF(PathExists(ExistingPath), L"IsCaseSensitiveFileSystem must be called with a path that exists on disk");
|
|
|
|
// Assume file system is case-sensitive if all-upper and all-lower versions of the path exist and resolve to the same FS entry.
|
|
// This is not 100% robust due to symlinks, but is good enough for most practical purposes.
|
|
|
|
FPath PathUpper = StringToUpper(ExistingPath.wstring());
|
|
FPath PathLower = StringToLower(ExistingPath.wstring());
|
|
|
|
if (PathExists(PathUpper) && PathExists(PathLower))
|
|
{
|
|
return std::filesystem::equivalent(ExistingPath, PathUpper) && std::filesystem::equivalent(PathLower, PathUpper);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool
|
|
IsCaseSensitiveFileSystem(const FPath& ExistingPath)
|
|
{
|
|
return !IsNonCaseSensitiveFileSystem(ExistingPath);
|
|
}
|
|
|
|
FFileAttributes
|
|
GetCachedFileAttrib(const FPath& Path, FFileAttributeCache& AttribCache)
|
|
{
|
|
FFileAttributes Result;
|
|
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
|
|
auto It = AttribCache.Map.find(ExtendedPath);
|
|
if (It != AttribCache.Map.end())
|
|
{
|
|
Result = It->second;
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
inline uint64
|
|
MakeU64(FILETIME Ft)
|
|
{
|
|
return MakeU64(Ft.dwHighDateTime, Ft.dwLowDateTime);
|
|
}
|
|
|
|
struct FCreateFileInfo
|
|
{
|
|
DWORD FileAccess = 0;
|
|
DWORD Share = 0;
|
|
DWORD Disposition = 0;
|
|
DWORD Protection = 0;
|
|
DWORD MapAccess = 0;
|
|
DWORD FileFlags = FILE_ATTRIBUTE_NORMAL;
|
|
|
|
FCreateFileInfo(EFileMode Mode)
|
|
{
|
|
switch (Mode & EFileMode::CommonModeMask)
|
|
{
|
|
default:
|
|
case EFileMode::ReadOnly:
|
|
case EFileMode::ReadOnlyUnbuffered:
|
|
FileAccess = GENERIC_READ;
|
|
Share = FILE_SHARE_READ;
|
|
Disposition = OPEN_EXISTING;
|
|
Protection = PAGE_READONLY;
|
|
MapAccess = FILE_MAP_READ;
|
|
break;
|
|
case EFileMode::CreateReadWrite:
|
|
case EFileMode::CreateWriteOnly:
|
|
UNSYNC_ASSERT(!GDryRun || EnumHasAnyFlags(Mode, EFileMode::IgnoreDryRun));
|
|
FileAccess = GENERIC_READ | GENERIC_WRITE;
|
|
Share = FILE_SHARE_WRITE;
|
|
Disposition = CREATE_ALWAYS;
|
|
Protection = PAGE_READWRITE;
|
|
MapAccess = FILE_MAP_ALL_ACCESS;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
struct FWindowsAsyncFileReader final : FAsyncReader
|
|
{
|
|
UNSYNC_DISALLOW_COPY_ASSIGN(FWindowsAsyncFileReader)
|
|
|
|
struct FOverlappedCommand
|
|
{
|
|
OVERLAPPED Overlapped = {};
|
|
uint64 RequestedOffset = 0;
|
|
uint64 RequestedSize = 0;
|
|
uint64 AlignedOffset = 0;
|
|
uint64 AlignedSize = 0;
|
|
uint64 Transferred = 0;
|
|
uint64 UserData = 0;
|
|
uint32 ErrorCode = 0;
|
|
bool bIoActive = false;
|
|
bool bComplete = true;
|
|
FIOBuffer Buffer;
|
|
IOCallback Callback = {};
|
|
};
|
|
|
|
FWindowsAsyncFileReader(FWindowsFile& InReader, uint32 InMaxPipelineDepth);
|
|
|
|
virtual ~FWindowsAsyncFileReader();
|
|
virtual uint64 GetSize() override { return FileSize; }
|
|
virtual bool IsValid() override { return !ErrorCode && !bClosed; }
|
|
virtual bool EnqueueRead(uint64 SourceOffset, uint64 Size, uint64 UserData, IOCallback Callback);
|
|
virtual void Flush() override;
|
|
|
|
bool BeginReadingNextSegment(FOverlappedCommand& Cmd);
|
|
bool FinishReadingSegment(FOverlappedCommand& Cmd);
|
|
|
|
void CompleteReadCommand(FOverlappedCommand& Cmd);
|
|
|
|
FWindowsFile& Inner;
|
|
const uint32 MaxQueueDepth;
|
|
|
|
static constexpr uint32 MAX_OVERLAPPED_COMMANDS = MAX_IO_PIPELINE_DEPTH;
|
|
|
|
HANDLE OverlappedEvents[MAX_OVERLAPPED_COMMANDS] = {};
|
|
FOverlappedCommand Commands[MAX_OVERLAPPED_COMMANDS] = {};
|
|
uint64 NumCommandsIssued = 0;
|
|
|
|
HANDLE FileHandle = INVALID_HANDLE_VALUE;
|
|
uint64 FileSize = 0;
|
|
FAtomicError ErrorCode;
|
|
std::atomic_bool bClosed;
|
|
};
|
|
|
|
FWindowsAsyncFileReader::FWindowsAsyncFileReader(FWindowsFile& InReader, uint32 InMaxPipelineDepth)
|
|
: Inner(InReader)
|
|
, FileHandle(InReader.FileHandle)
|
|
, FileSize(InReader.GetSize())
|
|
, MaxQueueDepth(InMaxPipelineDepth)
|
|
{
|
|
UNSYNC_ASSERT(IsReadOnly(InReader.Mode));
|
|
|
|
if (!InReader.IsValid())
|
|
{
|
|
ErrorCode.Set(SystemError(L"FWindowsAsyncFileReader source file is invalid", InReader.GetError()));
|
|
InReader.GetError();
|
|
}
|
|
|
|
for (uint32 I = 0; I < MaxQueueDepth; ++I)
|
|
{
|
|
OverlappedEvents[I] = CreateEvent(nullptr, true, true, nullptr);
|
|
Commands[I].Overlapped.hEvent = OverlappedEvents[I];
|
|
}
|
|
|
|
Inner.AddAsyncReader(this);
|
|
}
|
|
|
|
FWindowsAsyncFileReader::~FWindowsAsyncFileReader()
|
|
{
|
|
Flush();
|
|
|
|
for (HANDLE EventHandle : OverlappedEvents)
|
|
{
|
|
if (EventHandle)
|
|
{
|
|
CloseHandle(EventHandle);
|
|
}
|
|
}
|
|
|
|
Inner.RemoveAsyncReader(this);
|
|
}
|
|
|
|
FWindowsFile::FWindowsFile(const FPath& InFilename, EFileMode InMode, uint64 InSize) : Mode(InMode)
|
|
{
|
|
Filename = MakeExtendedAbsolutePath(InFilename);
|
|
|
|
bool bOpenedOk = OpenFileHandle(InMode);
|
|
|
|
if (bOpenedOk)
|
|
{
|
|
if (IsReadOnly(InMode))
|
|
{
|
|
LARGE_INTEGER LiFileSize = {};
|
|
bool bSizeOk = GetFileSizeEx(FileHandle, &LiFileSize);
|
|
if (!bSizeOk)
|
|
{
|
|
LastError = GetLastError();
|
|
return;
|
|
}
|
|
|
|
FileSize = LiFileSize.QuadPart;
|
|
}
|
|
else if (IsWritable(InMode) && InSize)
|
|
{
|
|
DWORD BytesReturned = 0;
|
|
BOOL SparseFileOk = DeviceIoControl(FileHandle, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &BytesReturned, nullptr);
|
|
if (!SparseFileOk)
|
|
{
|
|
UNSYNC_WARNING(L"Failed to mark file '%ls' as sparse.", Filename.wstring().c_str());
|
|
}
|
|
|
|
LARGE_INTEGER LiFileSize = {};
|
|
LiFileSize.QuadPart = InSize;
|
|
BOOL SizeOk = SetFilePointerEx(FileHandle, LiFileSize, nullptr, FILE_BEGIN);
|
|
if (!SizeOk)
|
|
{
|
|
CloseHandle(FileHandle);
|
|
LastError = GetLastError();
|
|
return;
|
|
}
|
|
|
|
BOOL EndOfFileOk = SetEndOfFile(FileHandle);
|
|
if (!EndOfFileOk)
|
|
{
|
|
CloseHandle(FileHandle);
|
|
LastError = GetLastError();
|
|
return;
|
|
}
|
|
|
|
FileSize = LiFileSize.QuadPart;
|
|
}
|
|
else if (IsWritable(InMode) && (InSize == 0))
|
|
{
|
|
// nothing to do when creating an empty file
|
|
}
|
|
else
|
|
{
|
|
UNSYNC_ERROR(L"Unexpected file mode %d", (int)InMode);
|
|
}
|
|
}
|
|
}
|
|
FWindowsFile::~FWindowsFile()
|
|
{
|
|
Close();
|
|
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
UNSYNC_ASSERT(AsyncReaders.empty());
|
|
}
|
|
|
|
void
|
|
FWindowsFile::AddAsyncReader(FWindowsAsyncFileReader* Reader)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
AsyncReaders.push_back(Reader);
|
|
}
|
|
|
|
void
|
|
FWindowsFile::RemoveAsyncReader(FWindowsAsyncFileReader* Reader)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
AsyncReaders.erase(std::remove(AsyncReaders.begin(), AsyncReaders.end(), Reader), AsyncReaders.end());
|
|
}
|
|
|
|
bool
|
|
FWindowsFile::OpenFileHandle(EFileMode InMode)
|
|
{
|
|
FCreateFileInfo Info(InMode);
|
|
Info.FileFlags |= FILE_FLAG_OVERLAPPED;
|
|
if (EnumHasAnyFlags(InMode, EFileMode::Unbuffered) && !GForceBufferedFiles)
|
|
{
|
|
Info.FileFlags |= FILE_FLAG_NO_BUFFERING;
|
|
}
|
|
|
|
FileHandle = CreateFileW(Filename.c_str(), Info.FileAccess, Info.Share, nullptr, Info.Disposition, Info.FileFlags, nullptr);
|
|
|
|
if (FileHandle == INVALID_HANDLE_VALUE)
|
|
{
|
|
LastError = GetLastError();
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool
|
|
FWindowsFile::IsValid()
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
return FileHandle != INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
void
|
|
FWindowsFile::Close()
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
|
|
FlushAsyncReaders();
|
|
|
|
if (FileHandle != INVALID_HANDLE_VALUE)
|
|
{
|
|
CloseHandle(FileHandle);
|
|
FileHandle = INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
for (FWindowsAsyncFileReader* It : AsyncReaders)
|
|
{
|
|
It->bClosed = true;
|
|
}
|
|
}
|
|
|
|
inline void
|
|
SetOverlappedOffset(OVERLAPPED& Overlapped, uint64 Offset)
|
|
{
|
|
LARGE_INTEGER Pos;
|
|
Pos.QuadPart = Offset;
|
|
|
|
Overlapped.Offset = Pos.LowPart;
|
|
Overlapped.OffsetHigh = Pos.HighPart;
|
|
}
|
|
|
|
bool
|
|
FWindowsAsyncFileReader::FinishReadingSegment(FOverlappedCommand& Cmd)
|
|
{
|
|
UNSYNC_ASSERT(Cmd.bIoActive);
|
|
|
|
DWORD ReadBytes = 0;
|
|
|
|
const BOOL OverlappedResultOk = GetOverlappedResult(FileHandle, &Cmd.Overlapped, &ReadBytes, true);
|
|
|
|
Cmd.bIoActive = false;
|
|
Cmd.Transferred += ReadBytes;
|
|
|
|
if (OverlappedResultOk)
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Cmd.ErrorCode = GetLastError();
|
|
ErrorCode.Set(SystemError(L"GetOverlappedResult failed", Cmd.ErrorCode));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool
|
|
FWindowsAsyncFileReader::BeginReadingNextSegment(FOverlappedCommand& Cmd)
|
|
{
|
|
UNSYNC_ASSERT(!Cmd.bIoActive);
|
|
|
|
if (Cmd.Transferred >= Cmd.RequestedSize)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (EnumHasAnyFlags(Inner.Mode, EFileMode::Unbuffered))
|
|
{
|
|
Cmd.Transferred = AlignDownToMultiplePow2(Cmd.Transferred, FWindowsFile::UNBUFFERED_READ_ALIGNMENT);
|
|
}
|
|
|
|
const uint64 NextReadSize = Cmd.AlignedSize - Cmd.Transferred;
|
|
|
|
uint8* BufferMemory = reinterpret_cast<uint8*>(Cmd.Buffer.GetMemory());
|
|
UNSYNC_ASSERT(Cmd.Transferred + NextReadSize <= Cmd.Buffer.GetMemorySize());
|
|
|
|
ResetEvent(Cmd.Overlapped.hEvent);
|
|
|
|
SetOverlappedOffset(Cmd.Overlapped, Cmd.AlignedOffset + Cmd.Transferred);
|
|
|
|
if (!ReadFile(FileHandle, BufferMemory + Cmd.Transferred, CheckedNarrow(NextReadSize), nullptr, &Cmd.Overlapped))
|
|
{
|
|
const DWORD LastError = GetLastError();
|
|
if (LastError != ERROR_IO_PENDING)
|
|
{
|
|
Cmd.ErrorCode = LastError;
|
|
ErrorCode.Set(SystemError(L"ReadFile failed", LastError));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Cmd.bIoActive = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
FWindowsAsyncFileReader::CompleteReadCommand(FOverlappedCommand& Cmd)
|
|
{
|
|
UNSYNC_ASSERT(!Cmd.bComplete);
|
|
|
|
while (Cmd.bIoActive)
|
|
{
|
|
if (FinishReadingSegment(Cmd))
|
|
{
|
|
BeginReadingNextSegment(Cmd);
|
|
}
|
|
}
|
|
|
|
UNSYNC_ASSERTF(
|
|
Cmd.RequestedSize <= Cmd.Transferred,
|
|
L"Expected to read at least %llu bytes, but read %llu [FileSize=%llu, Cmd.AlignedOffset=%llu, Cmd.AlignedSize=%llu, Cmd.ErrorCode=%u]",
|
|
llu(Cmd.RequestedSize),
|
|
llu(Cmd.Transferred),
|
|
llu(FileSize),
|
|
llu(Cmd.AlignedOffset),
|
|
llu(Cmd.AlignedSize),
|
|
Cmd.ErrorCode);
|
|
|
|
const uint64 ReadBytesClamped = std::min<uint64>(Cmd.Buffer.GetSize(), Cmd.Transferred);
|
|
|
|
if (Cmd.Callback)
|
|
{
|
|
Cmd.Callback(std::move(Cmd.Buffer), Cmd.RequestedOffset, ReadBytesClamped, Cmd.UserData);
|
|
}
|
|
|
|
Cmd.bComplete = true;
|
|
}
|
|
|
|
uint64
|
|
FWindowsFile::Write(const void* Data, uint64 DestOffset, uint64 TotalSize)
|
|
{
|
|
// TODO: !!!!! fire-and-forget asynchronous writes !!!!!
|
|
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
|
|
UNSYNC_ASSERT(IsWritable(Mode));
|
|
|
|
if (!IsWriteOnly(Mode))
|
|
{
|
|
FlushAsyncReaders(); // flush any outstanding read requests before writing
|
|
}
|
|
|
|
LARGE_INTEGER Pos;
|
|
Pos.QuadPart = DestOffset;
|
|
|
|
uint64 WrittenBytes = 0;
|
|
static constexpr uint64 ChunkSize = 128_MB;
|
|
uint64 NumChunks = DivUp(TotalSize, ChunkSize);
|
|
|
|
uint64 SourceOffset = 0;
|
|
|
|
for (uint64 I = 0; I < NumChunks; ++I)
|
|
{
|
|
int32 ThisChunkSize = CheckedNarrow(CalcChunkSize(I, ChunkSize, TotalSize));
|
|
OVERLAPPED Overlapped = {};
|
|
|
|
Overlapped.Offset = Pos.LowPart;
|
|
Overlapped.OffsetHigh = Pos.HighPart;
|
|
|
|
BOOL WriteOk = WriteFile(FileHandle, reinterpret_cast<const uint8*>(Data) + SourceOffset, ThisChunkSize, nullptr, &Overlapped);
|
|
if (!WriteOk && GetLastError() != ERROR_IO_PENDING)
|
|
{
|
|
LastError = GetLastError();
|
|
return 0;
|
|
}
|
|
|
|
DWORD ChunkWrittenBytes = 0;
|
|
BOOL OverlappedResultOk = TRUE;
|
|
|
|
uint32 MaxAttempts = 100000;
|
|
uint32 Attempt = 0;
|
|
for (Attempt = 0; Attempt < MaxAttempts; ++Attempt)
|
|
{
|
|
OverlappedResultOk = GetOverlappedResult(FileHandle, &Overlapped, &ChunkWrittenBytes, true);
|
|
if (!OverlappedResultOk || ChunkWrittenBytes != 0)
|
|
{
|
|
break;
|
|
}
|
|
if (ChunkWrittenBytes == 0)
|
|
{
|
|
SchedulerSleep(1);
|
|
}
|
|
}
|
|
if (Attempt == MaxAttempts)
|
|
{
|
|
UNSYNC_ERROR(L"Overlapped file write timed out");
|
|
}
|
|
|
|
if (!OverlappedResultOk)
|
|
{
|
|
LastError = GetLastError();
|
|
break;
|
|
}
|
|
|
|
WrittenBytes += ChunkWrittenBytes;
|
|
Pos.QuadPart += ChunkWrittenBytes;
|
|
SourceOffset += ChunkWrittenBytes;
|
|
}
|
|
|
|
return WrittenBytes;
|
|
}
|
|
|
|
uint64
|
|
FWindowsFile::Read(void* Dest, uint64 SourceOffset, uint64 ReadSize)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
|
|
UNSYNC_ASSERTF(((Mode & EFileMode::Unbuffered) == 0) ||
|
|
((SourceOffset % UNBUFFERED_READ_ALIGNMENT == 0) && (ReadSize % UNBUFFERED_READ_ALIGNMENT == 0)),
|
|
L"Unbuffered files only support Read when offset and size are aligned to 4KB");
|
|
|
|
UNSYNC_ASSERT(IsReadable(Mode));
|
|
|
|
LARGE_INTEGER Pos;
|
|
Pos.QuadPart = SourceOffset;
|
|
|
|
uint64 ReadBytes = 0;
|
|
static constexpr uint64 ChunkSize = 128_MB;
|
|
uint64 NumChunks = DivUp(ReadSize, ChunkSize);
|
|
|
|
uint64 DestOffset = 0;
|
|
|
|
for (uint64 I = 0; I < NumChunks; ++I)
|
|
{
|
|
uint32 ThisChunkSize = CheckedNarrow(CalcChunkSize(I, ChunkSize, ReadSize));
|
|
OVERLAPPED Overlapped = {};
|
|
|
|
Overlapped.Offset = Pos.LowPart;
|
|
Overlapped.OffsetHigh = Pos.HighPart;
|
|
|
|
BOOL ReadOk =
|
|
ReadFile(FileHandle, reinterpret_cast<uint8*>(Dest) + DestOffset + I * ChunkSize, ThisChunkSize, nullptr, &Overlapped);
|
|
if (!ReadOk && GetLastError() != ERROR_IO_PENDING)
|
|
{
|
|
LastError = GetLastError();
|
|
return 0;
|
|
}
|
|
|
|
DWORD ChunkReadBytes = 0;
|
|
BOOL OverlappedResultOk = GetOverlappedResult(FileHandle, &Overlapped, &ChunkReadBytes, true);
|
|
if (!OverlappedResultOk)
|
|
{
|
|
LastError = GetLastError();
|
|
break;
|
|
}
|
|
|
|
ReadBytes += ChunkReadBytes;
|
|
Pos.QuadPart += ChunkReadBytes;
|
|
SourceOffset += ChunkReadBytes;
|
|
}
|
|
|
|
return ReadBytes;
|
|
}
|
|
|
|
bool
|
|
FWindowsAsyncFileReader::EnqueueRead(uint64 SourceOffset, uint64 Size, uint64 UserData, IOCallback Callback)
|
|
{
|
|
if (!IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Clamp size to the end of the file
|
|
Size = std::min(FileSize, SourceOffset + Size) - SourceOffset;
|
|
|
|
const EFileMode FileMode = Inner.Mode;
|
|
|
|
UNSYNC_ASSERT(IsReadable(FileMode));
|
|
|
|
uint32 CmdIdx = ~0u;
|
|
|
|
// Async commands are always strictly ordered
|
|
const uint32 WaitSlotIndex = uint32(NumCommandsIssued % MaxQueueDepth);
|
|
const DWORD WaitResult = WaitForSingleObject(OverlappedEvents[WaitSlotIndex], INFINITE);
|
|
UNSYNC_ASSERT(WaitResult == WAIT_OBJECT_0);
|
|
CmdIdx = WaitSlotIndex;
|
|
|
|
UNSYNC_ASSERT(CmdIdx < MaxQueueDepth);
|
|
|
|
FOverlappedCommand& Cmd = Commands[CmdIdx];
|
|
|
|
if (!Cmd.bComplete)
|
|
{
|
|
CompleteReadCommand(Cmd);
|
|
}
|
|
|
|
Cmd.RequestedOffset = SourceOffset;
|
|
Cmd.RequestedSize = Size;
|
|
Cmd.UserData = UserData;
|
|
Cmd.Callback = Callback;
|
|
Cmd.Transferred = 0;
|
|
Cmd.ErrorCode = 0;
|
|
Cmd.bComplete = false;
|
|
|
|
if (EnumHasAnyFlags(FileMode, EFileMode::Unbuffered))
|
|
{
|
|
uint64 OriginalSize = Size;
|
|
uint64 OriginalBegin = SourceOffset;
|
|
uint64 OriginalEnd = SourceOffset + Size;
|
|
|
|
uint64 AlignedBegin = AlignDownToMultiplePow2(OriginalBegin, FWindowsFile::UNBUFFERED_READ_ALIGNMENT);
|
|
uint64 AlignedEnd = AlignUpToMultiplePow2(OriginalEnd, FWindowsFile::UNBUFFERED_READ_ALIGNMENT);
|
|
|
|
uint64 AlignedSize = AlignedEnd - AlignedBegin;
|
|
|
|
Cmd.Buffer = FIOBuffer::Alloc(AlignedSize, L"WindowsFile::ReadAsync_aligned");
|
|
|
|
Cmd.Buffer.SetDataRange(OriginalBegin - AlignedBegin, OriginalSize);
|
|
|
|
Cmd.AlignedOffset = AlignedBegin;
|
|
Cmd.AlignedSize = AlignedSize;
|
|
}
|
|
else
|
|
{
|
|
Cmd.Buffer = FIOBuffer::Alloc(Size, L"WindowsFile::ReadAsync");
|
|
|
|
Cmd.AlignedOffset = SourceOffset;
|
|
Cmd.AlignedSize = Size;
|
|
}
|
|
|
|
if (BeginReadingNextSegment(Cmd))
|
|
{
|
|
++NumCommandsIssued;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void
|
|
FWindowsAsyncFileReader::Flush()
|
|
{
|
|
for (uint32 I = 0; I < MaxQueueDepth; ++I)
|
|
{
|
|
const uint64 CommandIndex = (NumCommandsIssued + I) % MaxQueueDepth;
|
|
if (!Commands[CommandIndex].bComplete)
|
|
{
|
|
WaitForSingleObject(OverlappedEvents[CommandIndex], INFINITE);
|
|
CompleteReadCommand(Commands[CommandIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
FWindowsFile::FlushAsyncReaders()
|
|
{
|
|
for (FWindowsAsyncFileReader* It : AsyncReaders)
|
|
{
|
|
It->Flush();
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<FAsyncReader>
|
|
FWindowsFile::CreateAsyncReader(uint32 MaxPipelineDepth)
|
|
{
|
|
UNSYNC_ASSERT(IsValid());
|
|
|
|
MaxPipelineDepth = std::min(MaxPipelineDepth, FWindowsAsyncFileReader::MAX_OVERLAPPED_COMMANDS);
|
|
return std::unique_ptr<FAsyncReader>(new FWindowsAsyncFileReader(*this, MaxPipelineDepth));
|
|
}
|
|
|
|
FFileAttributes
|
|
GetFileAttrib(const FPath& Path, FFileAttributeCache* AttribCache)
|
|
{
|
|
FFileAttributes Result;
|
|
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
|
|
if (AttribCache)
|
|
{
|
|
auto It = AttribCache->Map.find(ExtendedPath);
|
|
if (It != AttribCache->Map.end())
|
|
{
|
|
Result = It->second;
|
|
return Result;
|
|
}
|
|
}
|
|
|
|
WIN32_FILE_ATTRIBUTE_DATA AttributeData;
|
|
BOOL Ok = GetFileAttributesExW(ExtendedPath.c_str(), GetFileExInfoStandard, &AttributeData);
|
|
if (Ok)
|
|
{
|
|
Result.bDirectory = !!(AttributeData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
|
|
Result.Size = MakeU64(AttributeData.nFileSizeHigh, AttributeData.nFileSizeLow);
|
|
Result.Mtime = MakeU64(AttributeData.ftLastWriteTime);
|
|
Result.bReadOnly = (AttributeData.dwFileAttributes & FILE_ATTRIBUTE_READONLY);
|
|
Result.bValid = true;
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
uint64
|
|
ToWindowsFileTime(const std::filesystem::file_time_type& T)
|
|
{
|
|
return T.time_since_epoch().count();
|
|
}
|
|
|
|
uint64
|
|
GetAvailableDiskSpace(const FPath& Path)
|
|
{
|
|
ULARGE_INTEGER AvailableBytes = {};
|
|
ULARGE_INTEGER TotalBytes = {};
|
|
ULARGE_INTEGER FreeBytes = {};
|
|
|
|
BOOL bOk = GetDiskFreeSpaceExW(Path.native().c_str(), &AvailableBytes, &TotalBytes, &FreeBytes);
|
|
|
|
if (bOk)
|
|
{
|
|
return AvailableBytes.QuadPart;
|
|
}
|
|
else
|
|
{
|
|
return ~0ull;
|
|
}
|
|
}
|
|
|
|
#endif // UNSYNC_PLATFORM_WINDOWS
|
|
|
|
#if UNSYNC_PLATFORM_UNIX
|
|
FUnixFile::FUnixFile(const FPath& InFilename, EFileMode InMode, uint64 in_size) : Filename(InFilename), Mode(InMode)
|
|
{
|
|
FileHandle = fopen(InFilename.native().c_str(), IsReadOnly(Mode) ? "rb" : "w+b");
|
|
if (FileHandle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FileDescriptor = fileno(FileHandle);
|
|
|
|
if (IsReadOnly(Mode))
|
|
{
|
|
struct stat stat_buf = {};
|
|
LastError = fstat(FileDescriptor, &stat_buf);
|
|
UNSYNC_ASSERT(LastError == 0);
|
|
FileSize = stat_buf.st_size;
|
|
}
|
|
else
|
|
{
|
|
LastError = ftruncate(FileDescriptor, in_size);
|
|
UNSYNC_ASSERT(LastError == 0);
|
|
FileSize = in_size;
|
|
}
|
|
}
|
|
|
|
FUnixFile::~FUnixFile()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
void
|
|
FUnixFile::Close()
|
|
{
|
|
if (FileHandle)
|
|
{
|
|
fclose(FileHandle);
|
|
FileHandle = nullptr;
|
|
}
|
|
}
|
|
|
|
uint64
|
|
FUnixFile::Read(void* dest, uint64 SourceOffset, uint64 ReadSize)
|
|
{
|
|
// TODO: handle partial reads (pread returning 0 < x < ReadSize)
|
|
uint64 read_bytes = pread(FileDescriptor, dest, ReadSize, SourceOffset);
|
|
if (read_bytes != ReadSize)
|
|
{
|
|
LastError = errno;
|
|
}
|
|
return read_bytes;
|
|
}
|
|
|
|
uint64
|
|
FUnixFile::Write(const void* data, uint64 DestOffset, uint64 WriteSize)
|
|
{
|
|
UNSYNC_ASSERT(IsWritable(Mode));
|
|
|
|
// TODO: handle partial writes (pwrite returning 0 < x < WriteSize)
|
|
uint64 wrote_bytes = pwrite(FileDescriptor, data, WriteSize, DestOffset);
|
|
if (wrote_bytes != WriteSize)
|
|
{
|
|
LastError = errno;
|
|
}
|
|
return wrote_bytes;
|
|
}
|
|
|
|
FFileAttributes
|
|
GetFileAttrib(const FPath& Path, FFileAttributeCache* AttribCache)
|
|
{
|
|
FFileAttributes Result;
|
|
|
|
if (AttribCache)
|
|
{
|
|
auto it = AttribCache->Map.find(Path);
|
|
if (it != AttribCache->Map.end())
|
|
{
|
|
Result = it->second;
|
|
return Result;
|
|
}
|
|
}
|
|
|
|
// TODO: could potentially use std::filesystem::directory_entry for this on all platforms
|
|
|
|
std::error_code ErrorCode = {};
|
|
auto Entry = std::filesystem::directory_entry(Path, ErrorCode);
|
|
|
|
if (!ErrorCode)
|
|
{
|
|
std::filesystem::file_status Status = Entry.status(ErrorCode);
|
|
if (ErrorCode)
|
|
{
|
|
return Result;
|
|
}
|
|
|
|
std::filesystem::perms Perms = Status.permissions();
|
|
|
|
Result.bDirectory = Entry.is_directory();
|
|
Result.Size = Result.bDirectory ? 0 : Entry.file_size();
|
|
Result.Mtime = ToWindowsFileTime(Entry.last_write_time());
|
|
Result.bReadOnly = IsReadOnly(Perms);
|
|
Result.bIsExecutable = IsExecutable(Perms);
|
|
Result.bValid = true;
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
uint64
|
|
ToWindowsFileTime(const std::filesystem::file_time_type& FileTime)
|
|
{
|
|
std::chrono::duration FullDuration = FileTime.time_since_epoch();
|
|
|
|
uint64 FullSeconds = std::chrono::floor<std::chrono::seconds>(FullDuration).count();
|
|
|
|
auto SubsecondDuration = FullDuration - std::chrono::seconds(FullSeconds);
|
|
auto SubsecondNanos = std::chrono::duration_cast<std::chrono::nanoseconds>(SubsecondDuration).count();
|
|
|
|
uint64 Ticks = (FullSeconds + SECONDS_BETWEEN_WINDOWS_AND_UNIX) * WINDOWS_TICKS_PER_SECOND + (SubsecondNanos / NANOS_PER_WINDOWS_TICK);
|
|
|
|
return Ticks;
|
|
}
|
|
|
|
uint64
|
|
GetAvailableDiskSpace(const FPath& Path)
|
|
{
|
|
return ~0ull; // TODO: query available space via statvfs()
|
|
}
|
|
|
|
#endif // UNSYNC_PLATFORM_UNIX
|
|
|
|
bool
|
|
SetFileMtime(const FPath& Path, uint64 Mtime, bool bAllowInDryRun)
|
|
{
|
|
UNSYNC_ASSERT(!GDryRun || bAllowInDryRun);
|
|
UNSYNC_ASSERT(Mtime != 0);
|
|
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
|
|
std::filesystem::file_time_type FileTime = FromWindowsFileTime(Mtime);
|
|
|
|
std::error_code ErrorCode;
|
|
std::filesystem::last_write_time(ExtendedPath, FileTime, ErrorCode);
|
|
|
|
return !ErrorCode;
|
|
}
|
|
|
|
bool
|
|
SetFileReadOnly(const FPath& Path, bool bReadOnly)
|
|
{
|
|
UNSYNC_ASSERT(!GDryRun);
|
|
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
|
|
std::error_code ErrorCode;
|
|
|
|
if (bReadOnly)
|
|
{
|
|
std::filesystem::permissions(
|
|
ExtendedPath,
|
|
std::filesystem::perms::owner_write | std::filesystem::perms::group_write | std::filesystem::perms::others_write,
|
|
std::filesystem::perm_options::remove,
|
|
ErrorCode);
|
|
}
|
|
else
|
|
{
|
|
std::filesystem::permissions(ExtendedPath, std::filesystem::perms::owner_write, std::filesystem::perm_options::add, ErrorCode);
|
|
}
|
|
|
|
return !ErrorCode;
|
|
}
|
|
|
|
bool
|
|
SetFileExecutable(const FPath& Path, bool Executable)
|
|
{
|
|
UNSYNC_ASSERT(!GDryRun);
|
|
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
|
|
std::error_code ErrorCode;
|
|
// TODO: In general if the +x bit is set, it set for all, this isn't always true though
|
|
// so we should at some point respect what the original file had set. For now though,
|
|
// this is sufficient.
|
|
if (Executable)
|
|
{
|
|
std::filesystem::permissions(
|
|
ExtendedPath,
|
|
std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec,
|
|
std::filesystem::perm_options::add,
|
|
ErrorCode);
|
|
}
|
|
else
|
|
{
|
|
std::filesystem::permissions(
|
|
ExtendedPath,
|
|
std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec,
|
|
std::filesystem::perm_options::remove,
|
|
ErrorCode);
|
|
}
|
|
|
|
return !ErrorCode;
|
|
}
|
|
|
|
FBuffer
|
|
ReadFileToBuffer(const FPath& Filename)
|
|
{
|
|
FBuffer Result;
|
|
FNativeFile File(Filename, EFileMode::ReadOnly);
|
|
if (File.IsValid())
|
|
{
|
|
Result.Resize(File.GetSize());
|
|
uint64 ReadBytes = File.Read(Result.Data(), 0, Result.Size());
|
|
Result.Resize(ReadBytes);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
bool
|
|
WriteBufferToFile(const FPath& Filename, const uint8* Data, uint64 Size, EFileMode FileMode)
|
|
{
|
|
UNSYNC_LOG_INDENT;
|
|
|
|
if (Data == nullptr)
|
|
{
|
|
UNSYNC_ERROR(L"WriteBufferToFile called with null buffer");
|
|
return false;
|
|
}
|
|
if (Size == 0)
|
|
{
|
|
UNSYNC_ERROR(L"WriteBufferToFile called with zero size buffer");
|
|
return false;
|
|
}
|
|
if (GDryRun && !EnumHasAnyFlags(FileMode, EFileMode::IgnoreDryRun))
|
|
{
|
|
UNSYNC_ERROR(L"WriteBufferToFile called in dry run mode");
|
|
return false;
|
|
}
|
|
|
|
FNativeFile File(Filename, FileMode, Size);
|
|
|
|
if (File.IsValid())
|
|
{
|
|
uint64 WroteBytes = File.Write(Data, 0, Size);
|
|
if (WroteBytes != Size)
|
|
{
|
|
UNSYNC_ERROR(L"Failed to write complete file '%ls'. Expected to write %llu bytes, actually written %llu bytes",
|
|
Filename.wstring().c_str(),
|
|
llu(Size),
|
|
llu(WroteBytes));
|
|
}
|
|
return WroteBytes == Size;
|
|
}
|
|
else
|
|
{
|
|
UNSYNC_ERROR(L"Failed to open file '%ls' for writing. %hs",
|
|
Filename.wstring().c_str(),
|
|
FormatSystemErrorMessage(File.GetError()).c_str());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool
|
|
WriteBufferToFile(const FPath& Filename, const FBuffer& Buffer, EFileMode FileMode)
|
|
{
|
|
return WriteBufferToFile(Filename, Buffer.Data(), Buffer.Size(), FileMode);
|
|
}
|
|
|
|
bool
|
|
WriteBufferToFile(const FPath& Filename, const std::string& Buffer, EFileMode FileMode)
|
|
{
|
|
return WriteBufferToFile(Filename, (const uint8*)Buffer.data(), Buffer.length(), FileMode);
|
|
}
|
|
|
|
struct FIOBufferCache
|
|
{
|
|
std::mutex Mutex;
|
|
|
|
struct FAllocation
|
|
{
|
|
const wchar_t* DebugName = nullptr;
|
|
uint8* Memory;
|
|
uint64 Size;
|
|
};
|
|
|
|
std::vector<FAllocation> AllocatedBlocks; // todo: hash table (though current numbers are very low, so...)
|
|
std::vector<FAllocation> AvailableBlocks;
|
|
uint64 CurrentCacheSize = 0;
|
|
uint64 CurrentAllocatedSize = 0;
|
|
|
|
static constexpr uint64 MAX_CACHED_ALLOC_SIZE = 32_MB;
|
|
static constexpr uint64 MAX_TOTAL_CACHE_SIZE = 4_GB;
|
|
|
|
~FIOBufferCache()
|
|
{
|
|
for (FAllocation& X : AllocatedBlocks)
|
|
{
|
|
UnsyncFree(X.Memory);
|
|
}
|
|
for (FAllocation& X : AvailableBlocks)
|
|
{
|
|
UnsyncFree(X.Memory);
|
|
}
|
|
}
|
|
|
|
uint8* Alloc(uint64 Size, const wchar_t* DebugName)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
|
|
uint64 BestBlockIndex = ~0ull;
|
|
uint64 BestBlockSize = ~0ull;
|
|
|
|
if (Size <= MAX_CACHED_ALLOC_SIZE)
|
|
{
|
|
Size = NextPow2(CheckedNarrow(Size));
|
|
for (uint64 I = 0; I < AvailableBlocks.size(); ++I)
|
|
{
|
|
FAllocation& Candidate = AvailableBlocks[I];
|
|
if (Candidate.Size < BestBlockSize && Candidate.Size >= Size)
|
|
{
|
|
BestBlockSize = Candidate.Size;
|
|
BestBlockIndex = I;
|
|
}
|
|
}
|
|
}
|
|
|
|
FAllocation Allocation = {};
|
|
|
|
if (BestBlockSize != ~0ull)
|
|
{
|
|
FAllocation Candidate = AvailableBlocks[BestBlockIndex];
|
|
AllocatedBlocks.push_back(Candidate);
|
|
AvailableBlocks[BestBlockIndex] = AvailableBlocks.back();
|
|
AvailableBlocks.pop_back();
|
|
|
|
Allocation = Candidate;
|
|
}
|
|
else
|
|
{
|
|
CurrentAllocatedSize += Size;
|
|
|
|
Allocation.Memory = (uint8*)UnsyncMalloc(Size);
|
|
UNSYNC_ASSERT(Allocation.Memory);
|
|
|
|
Allocation.Size = Size;
|
|
AllocatedBlocks.push_back(Allocation);
|
|
|
|
if (Size <= MAX_CACHED_ALLOC_SIZE)
|
|
{
|
|
CurrentCacheSize += Allocation.Size;
|
|
}
|
|
}
|
|
|
|
return Allocation.Memory;
|
|
}
|
|
|
|
void Free(uint8* Ptr)
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
|
|
uint64 AllocationIndex = ~0u;
|
|
for (uint64 I = 0; I < AllocatedBlocks.size(); ++I)
|
|
{
|
|
if (AllocatedBlocks[I].Memory == Ptr)
|
|
{
|
|
AllocationIndex = I;
|
|
break;
|
|
}
|
|
}
|
|
|
|
UNSYNC_ASSERTF(AllocationIndex != ~0u, L"Trying to free an unknown IOBuffer.");
|
|
|
|
FAllocation FreedBlock = AllocatedBlocks[AllocationIndex];
|
|
|
|
if (FreedBlock.Size <= MAX_CACHED_ALLOC_SIZE)
|
|
{
|
|
AvailableBlocks.push_back(FreedBlock);
|
|
}
|
|
else
|
|
{
|
|
UnsyncFree(FreedBlock.Memory);
|
|
CurrentAllocatedSize -= FreedBlock.Size;
|
|
}
|
|
|
|
while (CurrentCacheSize > MAX_TOTAL_CACHE_SIZE && !AvailableBlocks.empty())
|
|
{
|
|
FAllocation& LastBlock = AvailableBlocks.back();
|
|
UnsyncFree(LastBlock.Memory);
|
|
|
|
UNSYNC_ASSERT(CurrentCacheSize >= LastBlock.Size);
|
|
CurrentCacheSize -= LastBlock.Size;
|
|
|
|
CurrentAllocatedSize -= LastBlock.Size;
|
|
AvailableBlocks.pop_back();
|
|
}
|
|
|
|
AllocatedBlocks[AllocationIndex] = AllocatedBlocks.back();
|
|
AllocatedBlocks.pop_back();
|
|
}
|
|
|
|
uint64 GetCurrentSize()
|
|
{
|
|
std::lock_guard<std::mutex> LockGuard(Mutex);
|
|
return CurrentCacheSize;
|
|
}
|
|
};
|
|
|
|
static FIOBufferCache GIoBufferCache;
|
|
|
|
uint8*
|
|
AllocIoBuffer(uint64 Size, const wchar_t* DebugName)
|
|
{
|
|
return GIoBufferCache.Alloc(Size, DebugName);
|
|
}
|
|
|
|
void
|
|
FreeIoBuffer(uint8* Ptr)
|
|
{
|
|
GIoBufferCache.Free(Ptr);
|
|
}
|
|
|
|
uint64
|
|
GetCurrentIoCacheSize()
|
|
{
|
|
return GIoBufferCache.GetCurrentSize();
|
|
}
|
|
|
|
FFileAttributeCache
|
|
CreateFileAttributeCache(const FPath& Root, const FSyncFilter* SyncFilter)
|
|
{
|
|
FFileAttributeCache Result;
|
|
|
|
FTimePoint NextProgressLogTime = TimePointNow() + std::chrono::seconds(1);
|
|
|
|
auto ReportProgress = [&NextProgressLogTime, &Result]()
|
|
{
|
|
FTimePoint TimeNow = TimePointNow();
|
|
if (TimeNow >= NextProgressLogTime)
|
|
{
|
|
LogPrintf(ELogLevel::Debug, L"Found files: %d\r", (int)Result.Map.size());
|
|
NextProgressLogTime = TimeNow + std::chrono::seconds(1);
|
|
}
|
|
};
|
|
|
|
FPath ResolvedRoot = SyncFilter ? SyncFilter->Resolve(Root) : Root;
|
|
|
|
for (const std::filesystem::directory_entry& Dir : RecursiveDirectoryScan(ResolvedRoot))
|
|
{
|
|
if (Dir.is_directory())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (SyncFilter && !SyncFilter->ShouldSync(Dir.path().native()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FFileAttributes Attr = {};
|
|
std::filesystem::perms Perms = Dir.status().permissions();
|
|
|
|
Attr.Mtime = ToWindowsFileTime(Dir.last_write_time());
|
|
Attr.Size = Dir.file_size();
|
|
Attr.bValid = true;
|
|
Attr.bReadOnly = IsReadOnly(Perms);
|
|
Attr.bIsExecutable = IsExecutable(Perms);
|
|
|
|
Result.Map[Dir.path().native()] = Attr;
|
|
|
|
ReportProgress();
|
|
}
|
|
|
|
ReportProgress();
|
|
|
|
return Result;
|
|
}
|
|
|
|
bool
|
|
IsDirectory(const FPath& Path)
|
|
{
|
|
FFileAttributes Attr = GetFileAttrib(Path);
|
|
return Attr.bValid && Attr.bDirectory;
|
|
}
|
|
|
|
bool
|
|
PathExists(const FPath& Path)
|
|
{
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
return std::filesystem::exists(ExtendedPath);
|
|
}
|
|
|
|
bool
|
|
PathExists(const FPath& Path, std::error_code& OutErrorCode)
|
|
{
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
return std::filesystem::exists(ExtendedPath, OutErrorCode);
|
|
}
|
|
|
|
bool
|
|
CreateDirectories(const FPath& Path)
|
|
{
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
return std::filesystem::create_directories(ExtendedPath);
|
|
}
|
|
|
|
bool
|
|
EnsureDirectoryExists(const FPath& Path)
|
|
{
|
|
return (PathExists(Path) && IsDirectory(Path)) || CreateDirectories(Path);
|
|
}
|
|
|
|
bool
|
|
FileRename(const FPath& From, const FPath& To, std::error_code& OutErrorCode)
|
|
{
|
|
FPath ExtendedFrom = MakeExtendedAbsolutePath(From);
|
|
FPath ExtendedTo = MakeExtendedAbsolutePath(To);
|
|
std::filesystem::rename(ExtendedFrom, ExtendedTo, OutErrorCode);
|
|
return OutErrorCode.value() == 0;
|
|
}
|
|
|
|
bool
|
|
FileCopy(const FPath& From, const FPath& To, std::error_code& OutErrorCode)
|
|
{
|
|
FPath ExtendedFrom = MakeExtendedAbsolutePath(From);
|
|
FPath ExtendedTo = MakeExtendedAbsolutePath(To);
|
|
return std::filesystem::copy_file(ExtendedFrom, ExtendedTo, OutErrorCode);
|
|
}
|
|
|
|
bool
|
|
FileCopyOverwrite(const FPath& From, const FPath& To, std::error_code& OutErrorCode)
|
|
{
|
|
FPath ExtendedFrom = MakeExtendedAbsolutePath(From);
|
|
FPath ExtendedTo = MakeExtendedAbsolutePath(To);
|
|
return std::filesystem::copy_file(ExtendedFrom, ExtendedTo, std::filesystem::copy_options::overwrite_existing, OutErrorCode);
|
|
}
|
|
|
|
bool
|
|
FileRemove(const FPath& Path, std::error_code& OutErrorCode)
|
|
{
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
return std::filesystem::remove(ExtendedPath, OutErrorCode);
|
|
}
|
|
|
|
std::filesystem::recursive_directory_iterator
|
|
RecursiveDirectoryScan(const FPath& Path)
|
|
{
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
return std::filesystem::recursive_directory_iterator(ExtendedPath);
|
|
}
|
|
|
|
std::filesystem::directory_iterator
|
|
DirectoryScan(const FPath& Path)
|
|
{
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
return std::filesystem::directory_iterator(ExtendedPath);
|
|
}
|
|
|
|
FMemReader::FMemReader(const uint8* InData, uint64 InDataSize) : Data(InData), Size(InDataSize)
|
|
{
|
|
}
|
|
|
|
uint64
|
|
FMemReader::Read(void* Dest, uint64 SourceOffset, uint64 ReadSize)
|
|
{
|
|
uint64 ReadEndOffset = std::min(SourceOffset + ReadSize, Size);
|
|
uint64 ClampedReadSize = ReadEndOffset - SourceOffset;
|
|
memcpy(Dest, Data + SourceOffset, ClampedReadSize);
|
|
return ClampedReadSize;
|
|
}
|
|
|
|
FMemReaderWriter::FMemReaderWriter(uint8* InData, uint64 InDataSize) : FMemReader(InData, InDataSize), DataRw(InData)
|
|
{
|
|
}
|
|
|
|
uint64
|
|
FMemReaderWriter::Write(const void* InData, uint64 DestOffset, uint64 WriteSize)
|
|
{
|
|
uint64 WriteEndOffset = std::min(DestOffset + WriteSize, Size);
|
|
uint64 ClampedWriteSize = WriteEndOffset - DestOffset;
|
|
if (ClampedWriteSize && DataRw)
|
|
{
|
|
memcpy(DataRw + DestOffset, InData, ClampedWriteSize);
|
|
}
|
|
return ClampedWriteSize;
|
|
}
|
|
|
|
FIOBuffer
|
|
FIOBuffer::Alloc(uint64 Size, const wchar_t* DebugName)
|
|
{
|
|
UNSYNC_ASSERT(Size);
|
|
|
|
FIOBuffer Result;
|
|
|
|
Result.MemoryPtr = AllocIoBuffer(Size, DebugName);
|
|
Result.MemorySize = Size;
|
|
|
|
Result.DataPtr = Result.MemoryPtr;
|
|
Result.DataSize = Size;
|
|
|
|
Result.DebugName = DebugName;
|
|
|
|
return Result;
|
|
}
|
|
|
|
FIOBuffer::~FIOBuffer()
|
|
{
|
|
Clear();
|
|
UNSYNC_CLOBBER(Canary);
|
|
}
|
|
|
|
FIOBuffer::FIOBuffer(FIOBuffer&& Rhs)
|
|
{
|
|
UNSYNC_ASSERT(Rhs.Canary == CANARY);
|
|
|
|
std::swap(MemoryPtr, Rhs.MemoryPtr);
|
|
std::swap(MemorySize, Rhs.MemorySize);
|
|
|
|
std::swap(DataPtr, Rhs.DataPtr);
|
|
std::swap(DataSize, Rhs.DataSize);
|
|
}
|
|
|
|
void
|
|
FIOBuffer::Clear()
|
|
{
|
|
UNSYNC_ASSERT(Canary == CANARY);
|
|
if (MemoryPtr)
|
|
{
|
|
FreeIoBuffer(MemoryPtr);
|
|
|
|
MemoryPtr = nullptr;
|
|
MemorySize = 0;
|
|
|
|
DataPtr = nullptr;
|
|
DataSize = 0;
|
|
}
|
|
}
|
|
|
|
FIOBuffer&
|
|
FIOBuffer::operator=(FIOBuffer&& Rhs)
|
|
{
|
|
UNSYNC_ASSERT(Canary == CANARY);
|
|
UNSYNC_ASSERT(Rhs.Canary == CANARY);
|
|
if (this != &Rhs)
|
|
{
|
|
std::swap(MemoryPtr, Rhs.MemoryPtr);
|
|
std::swap(MemorySize, Rhs.MemorySize);
|
|
|
|
std::swap(DataPtr, Rhs.DataPtr);
|
|
std::swap(DataSize, Rhs.DataSize);
|
|
|
|
Rhs.Clear();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
void
|
|
TestFileTime()
|
|
{
|
|
UNSYNC_LOG(L"TestFileTime()");
|
|
UNSYNC_LOG_INDENT;
|
|
|
|
// 20231024004826Z - 2023 October 24 12:48:26
|
|
// unix 1698108506
|
|
// windows 133425821060000000
|
|
const uint64 BaseExpectedWindowsTime = 133425821060000000ull;
|
|
|
|
// Check basic conversion functionality at maximum
|
|
{
|
|
UNSYNC_LOG(L"File time precision estimate:");
|
|
UNSYNC_LOG_INDENT;
|
|
|
|
uint64 ExpectedWindowsTime = BaseExpectedWindowsTime + 9999999;
|
|
|
|
std::filesystem::file_time_type FileTime = FromWindowsFileTime(ExpectedWindowsTime);
|
|
|
|
uint64 RoundTripWindowsTime = ToWindowsFileTime(FileTime);
|
|
uint64 NativeCount = FileTime.time_since_epoch().count();
|
|
|
|
uint64 Delta = ExpectedWindowsTime > RoundTripWindowsTime ? ExpectedWindowsTime - RoundTripWindowsTime
|
|
: RoundTripWindowsTime - ExpectedWindowsTime;
|
|
|
|
UNSYNC_LOG(L"ExpectedWindowsTime = %llu", llu(ExpectedWindowsTime));
|
|
UNSYNC_LOG(L"RoundTripWindowsTime = %llu", llu(RoundTripWindowsTime));
|
|
UNSYNC_LOG(L"NativeCount = %llu, Delta = %llu", llu(NativeCount), llu(Delta));
|
|
}
|
|
|
|
// Check basic conversion functionality at 1 second precision
|
|
{
|
|
uint64 ExpectedWindowsTime = BaseExpectedWindowsTime;
|
|
|
|
std::filesystem::file_time_type FileTime = FromWindowsFileTime(ExpectedWindowsTime);
|
|
|
|
uint64 RoundTripWindowsTime = ToWindowsFileTime(FileTime);
|
|
uint64 NativeCount = FileTime.time_since_epoch().count();
|
|
|
|
UNSYNC_ASSERTF(RoundTripWindowsTime == ExpectedWindowsTime,
|
|
L"RoundTripWindowsTime is %llu, but expected to be %llu. Native count: %llu",
|
|
llu(RoundTripWindowsTime),
|
|
llu(ExpectedWindowsTime),
|
|
llu(NativeCount));
|
|
}
|
|
}
|
|
|
|
uint64
|
|
BlockingReadLarge(FIOReader& InReader, uint64 Offset, uint64 Size, uint8* OutputBuffer, uint64 OutputBufferSize)
|
|
{
|
|
const uint64 BytesPerRead = 2_MB;
|
|
const uint64 ReadEnd = std::min(Offset + Size, InReader.GetSize());
|
|
const uint64 ClampedSize = ReadEnd - Offset;
|
|
|
|
std::unique_ptr<FAsyncReader> AsyncReader = InReader.CreateAsyncReader();
|
|
|
|
std::atomic<uint64> TotalReadSize = 0;
|
|
|
|
if (ClampedSize == 0)
|
|
{
|
|
return TotalReadSize;
|
|
}
|
|
|
|
FSchedulerSemaphore IoSemaphore(*GScheduler, 16);
|
|
FTaskGroup CopyTasks = GScheduler->CreateTaskGroup(&IoSemaphore);
|
|
|
|
uint64 NumReads = DivUp(ClampedSize, BytesPerRead);
|
|
for (uint64 ReadIndex = 0; ReadIndex < NumReads; ++ReadIndex)
|
|
{
|
|
const uint64 ThisBatchSize = CalcChunkSize(ReadIndex, BytesPerRead, ClampedSize);
|
|
const uint64 OutputOffset = BytesPerRead * ReadIndex;
|
|
const uint64 ThisReadOffset = Offset + OutputOffset;
|
|
|
|
auto ReadCallback = [OutputBuffer, OutputBufferSize, &TotalReadSize, &CopyTasks](FIOBuffer CmdBuffer,
|
|
uint64 CmdSourceOffset,
|
|
uint64 CmdReadSize,
|
|
uint64 OutputOffset)
|
|
{
|
|
UNSYNC_ASSERT(OutputOffset + CmdReadSize <= OutputBufferSize);
|
|
|
|
CopyTasks.run(
|
|
[OutputBuffer, OutputOffset, CmdReadSize, CmdBuffer = MakeShared(std::move(CmdBuffer)), &TotalReadSize]()
|
|
{
|
|
memcpy(OutputBuffer + OutputOffset, CmdBuffer->GetData(), CmdReadSize);
|
|
TotalReadSize += CmdReadSize;
|
|
});
|
|
};
|
|
|
|
AsyncReader->EnqueueRead(ThisReadOffset, ThisBatchSize, OutputOffset, ReadCallback);
|
|
}
|
|
|
|
AsyncReader->Flush();
|
|
CopyTasks.wait();
|
|
|
|
return TotalReadSize;
|
|
}
|
|
|
|
void
|
|
TestFileAttrib()
|
|
{
|
|
UNSYNC_LOG(L"TestFileAttrib()");
|
|
UNSYNC_LOG_INDENT;
|
|
|
|
FPath TempDirPath = std::filesystem::temp_directory_path() / "unsync_test";
|
|
CreateDirectories(TempDirPath);
|
|
|
|
const bool bDirectoryExists = PathExists(TempDirPath) && IsDirectory(TempDirPath);
|
|
UNSYNC_ASSERT(bDirectoryExists);
|
|
|
|
const FPath TestFilename = TempDirPath / "attrib.txt";
|
|
UNSYNC_LOG(L"Test file name: %ls", TestFilename.wstring().c_str());
|
|
|
|
if (PathExists(TestFilename))
|
|
{
|
|
SetFileReadOnly(TestFilename, false);
|
|
}
|
|
|
|
const bool bFileWritten = WriteBufferToFile(TestFilename, "unsync test file");
|
|
UNSYNC_ASSERT(bFileWritten);
|
|
|
|
const uint64 ExpectedFileTime = 133425821060000000ull;
|
|
|
|
const bool bMtimeSet = SetFileMtime(TestFilename, ExpectedFileTime);
|
|
UNSYNC_ASSERT(bMtimeSet);
|
|
|
|
const FFileAttributes FileAttrib = GetFileAttrib(TestFilename);
|
|
UNSYNC_ASSERT(!FileAttrib.bReadOnly);
|
|
|
|
UNSYNC_ASSERT(FileAttrib.Mtime == ExpectedFileTime);
|
|
|
|
const bool bReadOnlySet = SetFileReadOnly(TestFilename, true);
|
|
UNSYNC_ASSERT(bReadOnlySet);
|
|
|
|
const FFileAttributes FileAttribReadOnly = GetFileAttrib(TestFilename);
|
|
UNSYNC_ASSERT(FileAttribReadOnly.bReadOnly);
|
|
|
|
const bool bReadOnlyReset = SetFileReadOnly(TestFilename, false);
|
|
UNSYNC_ASSERT(bReadOnlyReset);
|
|
|
|
const FFileAttributes FileAttribNonReadOnly = GetFileAttrib(TestFilename);
|
|
UNSYNC_ASSERT(!FileAttribNonReadOnly.bReadOnly);
|
|
|
|
#if UNSYNC_PLATFORM_UNIX
|
|
const bool bIsExecutable = SetFileExecutable(TestFilename, true);
|
|
UNSYNC_ASSERT(bIsExecutable);
|
|
|
|
const FFileAttributes FileAttribExecutable = GetFileAttrib(TestFilename);
|
|
UNSYNC_ASSERT(FileAttribExecutable.bIsExecutable);
|
|
|
|
const bool bIsNotExecutable = SetFileExecutable(TestFilename, false);
|
|
UNSYNC_ASSERT(bIsNotExecutable);
|
|
|
|
// This part of the test will fail on Windows platforms as the +x bit
|
|
// means nothing there, so bIsExecutable will never be set to true.
|
|
const FFileAttributes FileAttribNotExecutable = GetFileAttrib(TestFilename);
|
|
UNSYNC_ASSERT(!FileAttribNotExecutable.bIsExecutable);
|
|
#endif // UNSYNC_PLATFORM_UNIX
|
|
|
|
std::error_code ErrorCode;
|
|
const bool bFileDeleted = FileRemove(TestFilename, ErrorCode);
|
|
UNSYNC_ASSERT(bFileDeleted);
|
|
}
|
|
|
|
void
|
|
TestPathUtil()
|
|
{
|
|
#if UNSYNC_PLATFORM_WINDOWS
|
|
|
|
UNSYNC_LOG(L"TestPathUtil()");
|
|
UNSYNC_LOG_INDENT;
|
|
|
|
// Test path manipulation helpers
|
|
|
|
{
|
|
FPath Simple = FPath("\\\\?\\UNC\\server\\subdir\\a\\b\\c");
|
|
FPath Extended = MakeExtendedAbsolutePath(Simple);
|
|
UNSYNC_ASSERT(Simple == Extended);
|
|
}
|
|
|
|
{
|
|
FPath Simple = FPath("\\\\?\\d:\\local\\subdir\\a\\b\\c");
|
|
FPath Extended = MakeExtendedAbsolutePath(Simple);
|
|
UNSYNC_ASSERT(Simple == Extended);
|
|
}
|
|
|
|
{
|
|
FPath Simple = FPath("d:\\local\\subdir\\a\\b\\c");
|
|
FPath Extended = MakeExtendedAbsolutePath(Simple);
|
|
FPath Stripped = RemoveExtendedPathPrefix(Extended);
|
|
UNSYNC_ASSERT(Stripped == Simple);
|
|
}
|
|
|
|
{
|
|
FPath Simple = FPath("\\\\server\\local\\subdir\\a\\b\\c");
|
|
FPath Extended = MakeExtendedAbsolutePath(Simple);
|
|
FPath Stripped = RemoveExtendedPathPrefix(Extended);
|
|
UNSYNC_ASSERT(Stripped == Simple);
|
|
}
|
|
|
|
{
|
|
FPath Base = FPath("d:\\local\\subdir");
|
|
FPath Full = FPath("d:\\local\\subdir\\a\\b\\c");
|
|
FPath Relative = GetRelativePath(Full, Base);
|
|
UNSYNC_ASSERT(Relative == FPath("a\\b\\c"));
|
|
}
|
|
|
|
{
|
|
FPath Base = FPath("\\\\server\\subdir");
|
|
FPath Full = FPath("\\\\server\\subdir\\a\\b\\c");
|
|
FPath Relative = GetRelativePath(Full, Base);
|
|
UNSYNC_ASSERT(Relative == FPath("a\\b\\c"));
|
|
}
|
|
|
|
{
|
|
FPath Base = FPath("\\\\?\\d:\\local\\subdir");
|
|
FPath Full = FPath("\\\\?\\d:\\local\\subdir\\a\\b\\c");
|
|
FPath Relative = GetRelativePath(Full, Base);
|
|
UNSYNC_ASSERT(Relative == FPath("a\\b\\c"));
|
|
}
|
|
|
|
{
|
|
FPath Base = FPath("\\\\?\\d:\\local\\subdir");
|
|
FPath Full = FPath("d:\\local\\subdir\\a\\b\\c");
|
|
FPath Relative = GetRelativePath(Full, Base);
|
|
UNSYNC_ASSERT(Relative == FPath("a\\b\\c"));
|
|
}
|
|
|
|
{
|
|
FPath Base = FPath("d:\\local\\subdir");
|
|
FPath Full = FPath("\\\\?\\d:\\local\\subdir\\a\\b\\c");
|
|
FPath Relative = GetRelativePath(Full, Base);
|
|
UNSYNC_ASSERT(Relative == FPath("a\\b\\c"));
|
|
}
|
|
|
|
{
|
|
FPath Base = FPath("d:\\local\\subdir");
|
|
FPath Full = FPath("\\\\?\\e:\\local\\subdir\\a\\b\\c");
|
|
FPath Relative = GetRelativePath(Full, Base);
|
|
UNSYNC_ASSERT(Relative.empty());
|
|
}
|
|
|
|
{
|
|
FPath Base = FPath("d:\\local\\subdir");
|
|
FPath Full = FPath("d:\\local\\a\\b\\c");
|
|
FPath Relative = GetRelativePath(Full, Base);
|
|
UNSYNC_ASSERT(Relative.empty());
|
|
}
|
|
|
|
#endif // UNSYNC_PLATFORM_WINDOWS
|
|
}
|
|
|
|
void
|
|
DeleteOldFilesInDirectory(const FPath& Path, uint32 MaxFilesToKeep, bool bAllowInDryRun, const FPathFilterCallback& Filter)
|
|
{
|
|
struct FEntry
|
|
{
|
|
FPath Path;
|
|
uint64 Mtime;
|
|
};
|
|
|
|
std::vector<FEntry> Entries;
|
|
|
|
FPath ExtendedPath = MakeExtendedAbsolutePath(Path);
|
|
for (const std::filesystem::directory_entry& It : std::filesystem::directory_iterator(ExtendedPath))
|
|
{
|
|
if (Filter && !Filter(It.path()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (It.is_regular_file())
|
|
{
|
|
FEntry Entry;
|
|
Entry.Mtime = ToWindowsFileTime(It.last_write_time());
|
|
Entry.Path = It.path();
|
|
Entries.push_back(Entry);
|
|
}
|
|
}
|
|
|
|
// reverse sort
|
|
std::sort(Entries.begin(), Entries.end(), [](const FEntry& A, const FEntry& B) { return A.Mtime > B.Mtime; });
|
|
|
|
while (Entries.size() > MaxFilesToKeep)
|
|
{
|
|
const FEntry& Oldest = Entries.back();
|
|
|
|
std::wstring PathStr = RemoveExtendedPathPrefix(Oldest.Path).wstring();
|
|
|
|
if (GDryRun && !bAllowInDryRun)
|
|
{
|
|
UNSYNC_VERBOSE(L"Deleting '%ls'(skipped due to dry run mode)", PathStr.c_str());
|
|
}
|
|
else
|
|
{
|
|
UNSYNC_VERBOSE(L"Deleting '%ls'", PathStr.c_str());
|
|
std::error_code ErrorCode = {};
|
|
FileRemove(Oldest.Path, ErrorCode);
|
|
}
|
|
|
|
Entries.pop_back();
|
|
}
|
|
}
|
|
|
|
std::chrono::system_clock::time_point
|
|
SystemTimeFromFileTime(std::filesystem::file_time_type FileTime)
|
|
{
|
|
#if 0 // TODO: use clock_cast() when it's supported in all target compilers
|
|
|
|
return std::chrono::clock_cast<std::chrono::system_clock>(FileTime);
|
|
|
|
#else
|
|
|
|
# if UNSYNC_PLATFORM_WINDOWS
|
|
std::chrono::seconds Offset(SECONDS_BETWEEN_WINDOWS_AND_UNIX);
|
|
# else // UNSYNC_PLATFORM_WINDOWS
|
|
std::chrono::seconds Offset(0);
|
|
# endif // UNSYNC_PLATFORM_WINDOWS
|
|
|
|
std::chrono::duration SinceEpoch = FileTime.time_since_epoch() - Offset;
|
|
return std::chrono::system_clock::time_point(std::chrono::duration_cast<std::chrono::system_clock::duration>(SinceEpoch));
|
|
|
|
#endif // 0
|
|
}
|
|
|
|
FDummyAsyncReader::FDummyAsyncReader(FIOReader& InReader) : Inner(InReader)
|
|
{
|
|
}
|
|
|
|
bool
|
|
FDummyAsyncReader::EnqueueRead(uint64 SourceOffset, uint64 Size, uint64 UserData, IOCallback Callback)
|
|
{
|
|
if (Size != 0)
|
|
{
|
|
FIOBuffer Buffer = FIOBuffer::Alloc(Size, L"FDummyAsyncReader::ReadAsync");
|
|
uint64 ReadSize = Inner.Read(Buffer.GetData(), SourceOffset, Size);
|
|
Callback(std::move(Buffer), SourceOffset, ReadSize, UserData);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<FAsyncReader>
|
|
FIOReader::CreateAsyncReader(uint32 MaxPipelineDepth)
|
|
{
|
|
return std::unique_ptr<FDummyAsyncReader>(new FDummyAsyncReader(*this));
|
|
}
|
|
|
|
void
|
|
TestFileAsyncRead()
|
|
{
|
|
UNSYNC_LOG(L"TestFileAsyncRead()");
|
|
UNSYNC_LOG_INDENT;
|
|
|
|
UNSYNC_LOG(L"Initializing test data");
|
|
|
|
const FPath TempDirPath = std::filesystem::temp_directory_path() / "unsync_test";
|
|
CreateDirectories(TempDirPath);
|
|
|
|
const bool bDirectoryExists = PathExists(TempDirPath) && IsDirectory(TempDirPath);
|
|
UNSYNC_ASSERT(bDirectoryExists);
|
|
|
|
const FPath TestFilename = TempDirPath / "ordered_integers.bin";
|
|
constexpr uint64 TestFileSize = 1_GB;
|
|
|
|
FBuffer TempBuffer;
|
|
TempBuffer.Resize(TestFileSize);
|
|
uint32* TempBufferData = reinterpret_cast<uint32*>(TempBuffer.Data());
|
|
|
|
const uint32 NumElements = uint32(TestFileSize / sizeof(uint32));
|
|
|
|
for (uint32 i = 0; i < NumElements; ++i)
|
|
{
|
|
TempBufferData[i] = i;
|
|
}
|
|
|
|
const FHash256 ExpectedHash = HashBlake3Bytes<FHash256>(reinterpret_cast<const uint8*>(TempBufferData), TestFileSize);
|
|
|
|
if (!PathExists(TestFilename))
|
|
{
|
|
UNSYNC_LOG(L"Writing test file '%ls'", TestFilename.wstring().c_str());
|
|
const bool bFileWritten = WriteBufferToFile(TestFilename, TempBuffer);
|
|
UNSYNC_ASSERT(bFileWritten);
|
|
}
|
|
|
|
UNSYNC_LOG(L"ReadOnlyUnbuffered");
|
|
|
|
{
|
|
UNSYNC_LOG_INDENT;
|
|
|
|
memset(TempBufferData, 0, TestFileSize);
|
|
|
|
FNativeFile TestFile(TestFilename, EFileMode::ReadOnlyUnbuffered);
|
|
std::unique_ptr<FAsyncReader> TestFileReader = TestFile.CreateAsyncReader();
|
|
|
|
constexpr uint64 ChunkSize = 8_MB;
|
|
static_assert(TestFileSize % ChunkSize == 0);
|
|
|
|
UNSYNC_LOG(L"Reading test data");
|
|
|
|
const FTimePoint ReadStartTime = TimePointNow();
|
|
|
|
const uint64 NumChunks = TestFileSize / ChunkSize;
|
|
|
|
auto IoCallback = [TempBufferData](FIOBuffer Buffer, uint64 SourceOffset, uint64 ReadSize, uint64 UserData)
|
|
{ memcpy(reinterpret_cast<uint8*>(TempBufferData) + SourceOffset, Buffer.GetData(), Buffer.GetSize()); };
|
|
|
|
for (uint32 ChunkIndex = 0; ChunkIndex < NumChunks; ++ChunkIndex)
|
|
{
|
|
uint64 ChunkOffset = ChunkIndex * ChunkSize;
|
|
|
|
TestFileReader->EnqueueRead(ChunkOffset, ChunkSize, 0, IoCallback);
|
|
}
|
|
|
|
TestFileReader->Flush();
|
|
|
|
const FTimePoint ReadDoneTime = TimePointNow();
|
|
|
|
UNSYNC_LOG(L"Hashing test data");
|
|
|
|
const FHash256 ActualHash = HashBlake3Bytes<FHash256>(reinterpret_cast<const uint8*>(TempBufferData), TestFileSize);
|
|
|
|
const FTimePoint HashDoneTime = TimePointNow();
|
|
|
|
const double ReadDuration = DurationSec(ReadStartTime, ReadDoneTime);
|
|
const double HashDuration = DurationSec(ReadDoneTime, HashDoneTime);
|
|
const double TotalDuration = DurationSec(ReadStartTime, HashDoneTime);
|
|
|
|
UNSYNC_LOG(L"Read rate: %.2f MB / sec", SizeMb(TestFileSize) / ReadDuration);
|
|
UNSYNC_LOG(L"Hash rate: %.2f MB / sec", SizeMb(TestFileSize) / HashDuration);
|
|
UNSYNC_LOG(L"Total rate: %.2f MB / sec", SizeMb(TestFileSize) / TotalDuration);
|
|
UNSYNC_LOG(L"Total time: %.3f sec", TotalDuration);
|
|
|
|
UNSYNC_ASSERT(ActualHash == ExpectedHash);
|
|
}
|
|
|
|
UNSYNC_LOG(L"ReadOnlyUnbufferedStreaming");
|
|
|
|
{
|
|
UNSYNC_LOG_INDENT;
|
|
|
|
memset(TempBufferData, 0, TestFileSize);
|
|
|
|
FNativeFile TestFile(TestFilename, EFileMode::ReadOnlyUnbuffered);
|
|
std::unique_ptr<FAsyncReader> TestFileReader = TestFile.CreateAsyncReader();
|
|
|
|
constexpr uint64 ChunkSize = 1_MB;
|
|
static_assert(TestFileSize % ChunkSize == 0);
|
|
|
|
UNSYNC_LOG(L"Reading test data");
|
|
|
|
const FTimePoint ReadStartTime = TimePointNow();
|
|
|
|
const uint64 NumChunks = TestFileSize / ChunkSize;
|
|
|
|
FBlake3Hasher Hasher;
|
|
|
|
uint64 CurrentOffset = 0;
|
|
|
|
auto IoCallback = [TempBufferData, &Hasher, &CurrentOffset](FIOBuffer Buffer, uint64 SourceOffset, uint64 ReadSize, uint64 UserData)
|
|
{
|
|
UNSYNC_ASSERT(CurrentOffset == SourceOffset);
|
|
UNSYNC_ASSERT(ReadSize == Buffer.GetSize());
|
|
CurrentOffset += ReadSize;
|
|
Hasher.Update(Buffer.GetData(), Buffer.GetSize());
|
|
};
|
|
|
|
for (uint32 ChunkIndex = 0; ChunkIndex < NumChunks; ++ChunkIndex)
|
|
{
|
|
uint64 ChunkOffset = ChunkIndex * ChunkSize;
|
|
TestFileReader->EnqueueRead(ChunkOffset, ChunkSize, 0, IoCallback);
|
|
}
|
|
|
|
TestFileReader->Flush();
|
|
const FTimePoint ReadDoneTime = TimePointNow();
|
|
|
|
FHash256 ActualHash = Hasher.Finalize();
|
|
|
|
const FTimePoint HashDoneTime = TimePointNow();
|
|
|
|
const double TotalDuration = DurationSec(ReadStartTime, HashDoneTime);
|
|
|
|
UNSYNC_LOG(L"Read + Hash rate: %.2f MB / sec", SizeMb(TestFileSize) / TotalDuration);
|
|
UNSYNC_LOG(L"Total time: %.3f sec", TotalDuration);
|
|
|
|
UNSYNC_ASSERT(ActualHash == ExpectedHash);
|
|
}
|
|
}
|
|
|
|
} // namespace unsync
|