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

624 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#define UBA_IS_DETOURED_INCLUDE 1
#include "UbaDetoursShared.h"
#include "UbaDetoursFileMappingTable.h"
#include "UbaDirectoryTable.h"
#include "UbaTimer.h"
namespace uba
{
VARIABLE_MEM(StringBuffer<512>, g_virtualApplication);
VARIABLE_MEM(StringBuffer<512>, g_virtualApplicationDir);
VARIABLE_MEM(ProcessStats, g_stats);
VARIABLE_MEM(KernelStats, g_kernelStats);
VARIABLE_MEM(ReaderWriterLock, g_communicationLock);
VARIABLE_MEM(StringBuffer<256>, g_logName);
VARIABLE_MEM(StringBuffer<512>, g_virtualWorkingDir);
VARIABLE_MEM(StringBuffer<128>, g_systemRoot);
VARIABLE_MEM(StringBuffer<128>, g_systemTemp);
VARIABLE_MEM(MemoryBlock, g_memoryBlock);
VARIABLE_MEM(DirectoryTable, g_directoryTable);
VARIABLE_MEM(MappedFileTable, g_mappedFileTable);
VARIABLE_MEM(ReaderWriterLock, g_consoleStringCs);
bool g_echoOn = true;
u32 g_rulesIndex;
ApplicationRules* g_rules;
bool g_runningRemote;
bool g_isChild;
bool g_allowKeepFilesInMemory = IsWindows;
bool g_allowOutputFiles = IsWindows;
bool g_suppressLogging = false;
void InitSharedVariables()
{
g_virtualApplicationMem.Create();
g_virtualApplicationDirMem.Create();
g_statsMem.Create();
g_kernelStatsMem.Create();
g_communicationLockMem.Create();
g_logNameMem.Create();
g_virtualWorkingDirMem.Create();
g_systemRootMem.Create();
g_systemTempMem.Create();
u64 reserveSizeMb = IsWindows ? 256 : 1024; // The sync primitives on linux/macos is much bigger
g_memoryBlockMem.Create(reserveSizeMb * 1024 * 1024);
g_directoryTableMem.Create(g_memoryBlock);
g_mappedFileTableMem.Create(g_memoryBlock);
g_consoleStringCsMem.Create();
}
#if UBA_DEBUG_LOG_ENABLED
FileHandle g_debugFile = InvalidFileHandle;
void WriteDebug(const char* str, u32 strLen);
constexpr const char g_emptyString[] = " ";
constexpr const char* g_emptyStringEnd = ((const char*)g_emptyString) + sizeof_array(g_emptyString) - 1;
thread_local StringBuffer<LogBufSize> t_a;
thread_local char t_b[LogBufSize];
thread_local u32 t_b_size;
thread_local u32 t_logScopeCount;
Futex g_logScopeLock;
void GetPrefixExtra(StringBufferBase& out)
{
#if 0
static u64 startTime = GetTime();
u64 timeMs = TimeToMs(GetTime() - startTime);
u64 ms = timeMs % 1000;
u64 s = timeMs / 1000;
out.Appendf(TC("[%5llu.%03llu]"), s, ms);
#endif
//out.Appendf(TC("[%7u]"), GetCurrentThreadId());
}
void FlushDebug()
{
WriteDebug(t_b, t_b_size);
t_b_size = 0;
t_b[0] = 0;
}
void WriteDebugLogWithPrefix(const char* prefix, LogScope& scope, const tchar* command, const tchar* format, ...)
{
#if PLATFORM_MAC
static locale_t safeLocale = newlocale(LC_NUMERIC_MASK, "C", duplocale(LC_GLOBAL_LOCALE));
locale_t oldLocale = uselocale(safeLocale);
#endif
t_a.Clear().Append(command).Append(' ');
if (*format)
{
va_list arg;
va_start(arg, format);
t_a.Append(format, arg);
va_end(arg);
}
t_a.Append(TCV("\n"));
u32 size__ = t_b_size;
StringBuffer<128> extra;
GetPrefixExtra(extra);
#if PLATFORM_WINDOWS
u32 res__ = sprintf_s(t_b + size__, LogBufSize - size__, "%s %S %s%S", prefix, extra.data, g_emptyStringEnd - t_logScopeCount * 2, t_a.data);
#else
u32 res__ = snprintf(t_b + size__, LogBufSize - size__, "%s %s %s%s", prefix, extra.data, g_emptyStringEnd - t_logScopeCount * 2, t_a.data);
#endif
if (res__ != -1)
t_b_size += res__;
scope.Flush();
#if PLATFORM_MAC
uselocale(oldLocale);
#endif
}
void WriteDebugLog(const tchar* format, ...)
{
t_a.Clear();
if (*format)
{
va_list arg;
va_start(arg, format);
t_a.Append(format, arg);
va_end(arg);
}
t_a.Append(TCV("\n"));
#if PLATFORM_WINDOWS
if (t_b_size)
FlushDebug();
t_b_size = sprintf_s(t_b, LogBufSize, "%S", t_a.data);
FlushDebug();
#else
WriteDebug(t_a.data, t_a.count);
#endif
}
LogScope::LogScope()
{
if (++t_logScopeCount > 1)
return;
//g_logScopeLock.Enter(); // Deadlocks in a few places
}
LogScope::~LogScope()
{
if (--t_logScopeCount)
return;
if (t_b_size)
Flush();
//g_logScopeLock.Leave();
}
void LogScope::Flush()
{
FlushDebug();
}
#endif
#if UBA_DEBUG_VALIDATE
bool g_validateFileAccess = false;
#endif
thread_local u32 t_disallowDetour = 0; // Set this to 1 to disallow all detouring of I/O interaction
SuppressDetourScope::SuppressDetourScope() { ++t_disallowDetour; }
SuppressDetourScope::~SuppressDetourScope() { --t_disallowDetour; }
bool FixPath(StringBufferBase& out, const tchar* path)
{
return FixPath2(path, g_virtualWorkingDir.data, g_virtualWorkingDir.count, out.data, out.capacity, &out.count);
}
struct VfsEntry { StringView vfs; StringView local; VfsEntry() : vfs(NoInit), local(NoInit) {}; };
VfsEntry g_vfsEntries[32];
u32 g_vfsEntryCount;
u32 g_vfsMatchingLength;
void PopulateVfs(BinaryReader& vfsReader)
{
while (vfsReader.GetLeft())
{
vfsReader.ReadByte(); // Index, unused
StringBuffer<> str;
vfsReader.ReadString(str);
if (!str.count)
{
vfsReader.SkipString();
continue;
}
#if PLATFORM_WINDOWS
str.Replace('/', '\\');
#endif
u32 index = g_vfsEntryCount++;
UBA_ASSERT(index < sizeof_array(g_vfsEntries));
VfsEntry& vfsEntry = g_vfsEntries[index];
vfsEntry.vfs = g_memoryBlock.Strdup(str);
if (index == 0)
g_vfsMatchingLength = vfsEntry.vfs.count;
else
{
u32 shortest = Min(g_vfsMatchingLength, vfsEntry.vfs.count);
for (u32 i=0; i!=shortest; ++i)
{
if (g_vfsEntries[0].vfs.data[i] == vfsEntry.vfs.data[i])
continue;
shortest = i;
break;
}
g_vfsMatchingLength = shortest;
}
vfsReader.ReadString(str.Clear());
vfsEntry.local = g_memoryBlock.Strdup(str);
}
}
bool IsVfsEnabled()
{
return g_vfsEntryCount > 0;
}
bool DevirtualizePath(StringBufferBase& path)
{
if (!g_vfsEntryCount)
return false;
if (!Equals(path.data, g_vfsEntries[0].vfs.data, Min(path.count, g_vfsMatchingLength), CaseInsensitiveFs))
return false;
// TODO: This is not great, the dirs above the vfs root should be empty except the dir to the roots
if (path.count < g_vfsMatchingLength)
{
path.Clear().Append(g_vfsEntries[0].local);
return true;
}
for (u32 i=0, e=g_vfsEntryCount; i!=e; ++i)
{
VfsEntry& entry = g_vfsEntries[i];
if (!path.StartsWith(entry.vfs.data))
continue;
StringBuffer<MaxPath> temp2(path.data + entry.vfs.count);
path.Clear().Append(entry.local).Append(temp2);
return true;
}
return false;
}
bool VirtualizePath(StringBufferBase& path)
{
if (!g_vfsEntryCount)
return false;
for (u32 i=0, e=g_vfsEntryCount; i!=e; ++i)
{
VfsEntry& entry = g_vfsEntries[i];
if (path.count < entry.local.count || !path.StartsWith(entry.local.data))
continue;
StringBuffer<MaxPath> temp2(path.data + entry.local.count);
path.Clear().Append(entry.vfs).Append(temp2);
return true;
}
return false;
}
void LogVfsInfo()
{
for (u32 i=0; i!=g_vfsEntryCount; ++i)
{
DEBUG_LOG(TC("Vfs: %s -> %s"), g_vfsEntries[i].vfs.data, g_vfsEntries[i].local.data);
}
}
const tchar* GetApplicationShortName()
{
const tchar* lastBackslash = TStrrchr(g_virtualApplication.data, '\\');
const tchar* lastSlash = TStrrchr(g_virtualApplication.data, '/');
if (lastBackslash || lastSlash)
return (lastBackslash > lastSlash ? lastBackslash : lastSlash) + 1;
return g_virtualApplication.data;
}
ANALYSIS_NORETURN void FatalError(u32 code, const tchar* format, ...)
{
va_list arg;
va_start(arg, format);
tchar buffer[1024];
if (Tvsprintf_s(buffer, sizeof_array(buffer), format, arg) <= 0)
TStrcpy_s(buffer, sizeof_array(buffer), format);
va_end(arg);
StringBuffer<2048> sb;
sb.Append(GetApplicationShortName()).Append(TCV(" ERROR: ")).Append(buffer);
Rpc_WriteLog(sb.data, sb.count, true, true);
#if PLATFORM_WINDOWS // Maybe all platforms should call exit()?
ExitProcess(code);
#else
exit(code);
#endif
}
void Rpc_WriteLog(const tchar* text, u64 textCharLength, bool printInSession, bool isError)
{
DEBUG_LOG(TC("LOG %.*s"), u32(textCharLength), text); // TODO: Investigate, deadlocks on non-windows
// DEBUG_LOG(TC("LOG [%7u] %.*s"), GetCurrentThreadId(), u32(textCharLength), text);
RPC_MESSAGE(Log, log)
writer.WriteBool(printInSession);
writer.WriteBool(isError);
writer.WriteString(text, textCharLength);
writer.Flush();
}
void Rpc_WriteLogf(const tchar* format, ...)
{
va_list arg;
va_start(arg, format);
tchar buffer[1024];
int count = Tvsprintf_s(buffer, 1024, format, arg);
if (count <= 0)
{
TStrcpy_s(buffer, 1024, format);
count = int(TStrlen(buffer));
}
va_end(arg);
Rpc_WriteLog(buffer, u32(count), false, false);
}
UBA_NOINLINE void Rpc_ResolveCallstack(StringBufferBase& out, u32 skipCallstackCount, void* context)
{
u32 tryCount = 0;
bool hasLock = false;
while (tryCount++ < 5)
{
hasLock = g_communicationLock.TryEnter();
if (hasLock)
break;
Sleep(100);
}
BinaryWriter writer;
writer.WriteByte(MessageType_ResolveCallstack);
auto written = (u32*)writer.AllocWrite(4);
if (WriteCallstackInfo(writer, skipCallstackCount, context))
{
*written = u32(writer.GetPosition()) - 5;
writer.Flush();
BinaryReader reader;
reader.ReadString(out);
}
else
{
out.Append(TCV("\n Failed to resolve callstack\n"));
}
// Note, we leave the lock even though we might not have it because we want to be able to report
g_communicationLock.Leave();
}
//TODO: Implement SetConsoleTextAttribute.. clang is using it to color errors
tchar g_consoleString[4096];
u32 g_consoleStringIndex;
template<typename CharType>
void Shared_WriteConsoleT(const CharType* chars, u32 charCount, bool isError)
{
if (!g_echoOn || g_suppressLogging)
return;
SCOPED_WRITE_LOCK(g_consoleStringCs, lock);
const CharType* read = chars;
tchar* write = g_consoleString + g_consoleStringIndex;
int left = sizeof_array(g_consoleString) - g_consoleStringIndex - 1;
int available = charCount;
while (available)
{
if (*read == '\n' || !left)
{
*write = 0;
u32 strLen = u32(write - g_consoleString);
if (!g_rules->SuppressLogLine(g_consoleString, strLen))
Rpc_WriteLog(g_consoleString, strLen, false, isError);
write = g_consoleString;
left = sizeof_array(g_consoleString) - 1;
}
else
{
*write = *read;
++write;
}
++read;
--left;
--available;
}
g_consoleStringIndex = u32(write - g_consoleString);
}
void Shared_WriteConsole(const char* chars, u32 charCount, bool isError) { Shared_WriteConsoleT(chars, charCount, isError); }
#if PLATFORM_WINDOWS
void Shared_WriteConsole(const wchar_t* chars, u32 charCount, bool isError) { Shared_WriteConsoleT(chars, charCount, isError); }
#endif
const tchar* Shared_GetFileAttributes(FileAttributes& outAttr, StringView fileName, bool checkIfDir)
{
StringBuffer<MaxPath> fileNameForKey;
fileNameForKey.Append(fileName);
if (CaseInsensitiveFs)
fileNameForKey.MakeLower();
UBA_ASSERT(fileNameForKey.count);
CHECK_PATH(fileNameForKey);
StringKey fileNameKey = ToStringKey(fileNameForKey);
memset(&outAttr.data, 0, sizeof(outAttr.data));
bool isInsideSystemTemp = fileName.StartsWith(g_systemTemp.data);
bool keepInMemory = KeepInMemory(fileName, false);
if (keepInMemory)
{
SCOPED_READ_LOCK(g_mappedFileTable.m_lookupLock, lock);
auto it = g_mappedFileTable.m_lookup.find(fileNameKey);
if (it == g_mappedFileTable.m_lookup.end() || it->second.deleted)
{
if (isInsideSystemTemp)
{
outAttr.useCache = false;
return fileName.data;
}
outAttr.useCache = true;
outAttr.exists = false;
outAttr.lastError = ErrorFileNotFound;
}
else
{
outAttr.useCache = true;
outAttr.exists = true;
outAttr.lastError = ErrorSuccess;
#if PLATFORM_WINDOWS
LARGE_INTEGER li = ToLargeInteger(it->second.size);
outAttr.data.nFileSizeLow = li.LowPart;
outAttr.data.nFileSizeHigh = li.HighPart;
outAttr.data.dwFileAttributes = FILE_ATTRIBUTE_NORMAL;
#else
UBA_ASSERT(false);
#endif
// TODO: Currently only used for waccess... need to implement below
//outAttr.data.ftLastWriteTime = ;
//outAttr.volumeSerial =
//outAttr.fileIndex =
}
}
#if PLATFORM_WINDOWS
else if (fileName[1] == ':' && fileName[3] == 0 && (ToLower(fileName[0]) == ToLower(g_virtualWorkingDir[0]) || ToLower(fileName[0]) == g_systemRoot[0]))
{
// This is the root of the drive.. let's just return it as a directory
outAttr.useCache = true;
outAttr.exists = true;
outAttr.lastError = ErrorSuccess;
outAttr.data.dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY;
}
#else
//else if (StartsWith(fileName, g_applicationDir))
//{
// outAttr.useCache = false;
// return fileName;
//}
#endif
else if (g_allowDirectoryCache)
{
// This is an optimization where we populate directory table and use that to figure out if file exists or not..
// .. in msvc's case it doesn't matter much because these tables are already up to date when msvc use CreateFile.
// .. clang otoh is using CreateFile with tooons of different paths trying to open files.. in remote worker case this becomes super expensive
if (!isInsideSystemTemp) // We need to skip SystemTemp.. lots of stuff going on there.
{
u32 dirTableOffset = Rpc_GetEntryOffset(fileNameKey, fileName, checkIfDir);
if (dirTableOffset == ~u32(0))
{
// This could be a newly written file but process has not fetched latest directory table
SCOPED_READ_LOCK(g_mappedFileTable.m_lookupLock, lock);
auto findIt = g_mappedFileTable.m_lookup.find(fileNameKey);
if (findIt != g_mappedFileTable.m_lookup.end() && !findIt->second.deleted)
{
outAttr.exists = true;
outAttr.lastError = ErrorSuccess;
outAttr.useCache = false;
if (g_runningRemote)
{
FileInfo& info = findIt->second;
outAttr.useCache = true;
// TODO: This is missing lots of information..
#if PLATFORM_WINDOWS
LARGE_INTEGER li = ToLargeInteger(info.size);
outAttr.data.dwFileAttributes = FILE_ATTRIBUTE_NORMAL;
outAttr.data.nFileSizeLow = li.LowPart;
outAttr.data.nFileSizeHigh = li.HighPart;
#else
outAttr.data.st_mode = (mode_t)(S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
outAttr.data.st_size = info.size;
#endif
}
return findIt->second.name;
}
outAttr.useCache = true;
outAttr.exists = false;
outAttr.lastError = ErrorFileNotFound;
}
else
{
DirectoryTable::EntryInformation info;
g_directoryTable.GetEntryInformation(info, dirTableOffset);
if (info.attributes)
{
u64 fileSize = info.size;
// Could be compressed and then directory table size is wrong
if (CouldBeCompressedFile(fileName))
{
SCOPED_READ_LOCK(g_mappedFileTable.m_lookupLock, lock);
auto findIt = g_mappedFileTable.m_lookup.find(fileNameKey);
if (findIt == g_mappedFileTable.m_lookup.end())
{
// If file is output file we accept wrong size because size is not supposed to be used anyway.
// We don't want to trigger unnecessary download/decompress of file
if (!g_rules->IsOutputFile(fileName))
{
StringBuffer<> temp;
u32 closeId;
Rpc_CreateFileW(fileName, fileNameKey, AccessFlag_Read, temp.data, temp.capacity, fileSize, closeId, false);
}
}
else
{
UBA_ASSERT(!findIt->second.deleted);
fileSize = findIt->second.size;
}
}
outAttr.useCache = true;
outAttr.exists = true;
outAttr.lastError = ErrorSuccess;
UBA_ASSERT(info.fileIndex);
outAttr.fileIndex = info.fileIndex;
outAttr.volumeSerial = info.volumeSerial;
#if PLATFORM_WINDOWS
LARGE_INTEGER li = ToLargeInteger(fileSize);
outAttr.data.dwFileAttributes = info.attributes;
outAttr.data.nFileSizeLow = li.LowPart;
outAttr.data.nFileSizeHigh = li.HighPart;
(u64&)outAttr.data.ftCreationTime = info.lastWrite;
(u64&)outAttr.data.ftLastAccessTime = info.lastWrite;
(u64&)outAttr.data.ftLastWriteTime = info.lastWrite;
#else
outAttr.data.st_mtimespec = ToTimeSpec(info.lastWrite);
outAttr.data.st_mode = (mode_t)info.attributes;
outAttr.data.st_dev = info.volumeSerial;
outAttr.data.st_ino = info.fileIndex;
outAttr.data.st_size = fileSize;
#endif
}
else
{
// File used to exist but was deleted
outAttr.useCache = true;
outAttr.exists = false;
outAttr.lastError = ErrorFileNotFound;
}
}
}
else
{
outAttr.useCache = false;
return fileName.data;
}
}
else
{
outAttr.useCache = false;
return fileName.data;
}
#if 0//UBA_DEBUG_VALIDATE
if (g_validateFileAccess && !keepInMemory)
{
WIN32_FILE_ATTRIBUTE_DATA validate;
memset(&validate, 0, sizeof(validate));
SuppressDetourScope _;
BOOL res = True_GetFileAttributesExW(fileName, GetFileExInfoStandard, &validate); (void)res;
if (outAttr.exists)
{
UBA_ASSERTF(res != 0, L"File %ls exists even though uba claims it is not..", fileName.data);
if (validate.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
UBA_ASSERTF((outAttr.data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY), L"File attributes are wrong for %ls", fileName.data);
else
{
validate.ftCreationTime = outAttr.data.ftCreationTime; // Creation time is not really important
validate.ftLastAccessTime = outAttr.data.ftLastAccessTime; // Access time is not really important
validate.ftLastWriteTime = outAttr.data.ftLastWriteTime; // Write time is important, revisit this
UBA_ASSERTF(memcmp(&validate, &outAttr.data, sizeof(WIN32_FILE_ATTRIBUTE_DATA)) == 0, L"File %ls is not up-to-date in cache", fileName.data);
}
}
else
{
UBA_ASSERTF(res == 0, L"Can't find file %ls but validation checked that it is there", fileName.data); // This means most likely that Uba did not update attribute table for added files.
DWORD lastError2 = GetLastError();
if (lastError2 == ERROR_PATH_NOT_FOUND || lastError2 == ERROR_INVALID_NAME)
lastError2 = ERROR_FILE_NOT_FOUND;
UBA_ASSERT(outAttr.lastError == lastError2);
}
}
#endif
return fileName.data;
}
}